]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
4cda73509f5856bbb4a7eb3b6fc845e9df02cef1
[ceph.git] /
1 import { Component, EventEmitter, OnInit, Output } from '@angular/core';
2 import { Validators } from '@angular/forms';
3
4 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
5
6 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
7 import { CrushNodeSelectionClass } from '../../../shared/classes/crush.node.selection.class';
8 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
9 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
10 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
11 import { CdValidators } from '../../../shared/forms/cd-validators';
12 import { CrushNode } from '../../../shared/models/crush-node';
13 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
14 import { FinishedTask } from '../../../shared/models/finished-task';
15 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
16
17 @Component({
18   selector: 'cd-erasure-code-profile-form-modal',
19   templateUrl: './erasure-code-profile-form-modal.component.html',
20   styleUrls: ['./erasure-code-profile-form-modal.component.scss']
21 })
22 export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClass
23   implements OnInit {
24   @Output()
25   submitAction = new EventEmitter();
26
27   tooltips = this.ecpService.formTooltips;
28   PLUGIN = {
29     LRC: 'lrc', // Locally Repairable Erasure Code
30     SHEC: 'shec', // Shingled Erasure Code
31     JERASURE: 'jerasure', // default
32     ISA: 'isa' // Intel Storage Acceleration
33   };
34   plugin = this.PLUGIN.JERASURE;
35
36   form: CdFormGroup;
37   plugins: string[];
38   names: string[];
39   techniques: string[];
40   action: string;
41   resource: string;
42   lrcGroups: number;
43   lrcMultiK: number;
44
45   constructor(
46     private formBuilder: CdFormBuilder,
47     public activeModal: NgbActiveModal,
48     private taskWrapper: TaskWrapperService,
49     private ecpService: ErasureCodeProfileService,
50     public actionLabels: ActionLabelsI18n
51   ) {
52     super();
53     this.action = this.actionLabels.CREATE;
54     this.resource = $localize`EC Profile`;
55     this.createForm();
56     this.setJerasureDefaults();
57   }
58
59   createForm() {
60     this.form = this.formBuilder.group({
61       name: [
62         null,
63         [
64           Validators.required,
65           Validators.pattern('[A-Za-z0-9_-]+'),
66           CdValidators.custom(
67             'uniqueName',
68             (value: string) => this.names && this.names.indexOf(value) !== -1
69           )
70         ]
71       ],
72       plugin: [this.PLUGIN.JERASURE, [Validators.required]],
73       k: [
74         4, // Will be overwritten with plugin defaults
75         [
76           Validators.required,
77           Validators.min(2),
78           CdValidators.custom('max', () => this.baseValueValidation(true)),
79           CdValidators.custom('unequal', (v: number) => this.lrcDataValidation(v)),
80           CdValidators.custom('kLowerM', (v: number) => this.shecDataValidation(v))
81         ]
82       ],
83       m: [
84         2, // Will be overwritten with plugin defaults
85         [
86           Validators.required,
87           Validators.min(1),
88           CdValidators.custom('max', () => this.baseValueValidation())
89         ]
90       ],
91       crushFailureDomain: '', // Will be preselected
92       crushRoot: null, // Will be preselected
93       crushDeviceClass: '', // Will be preselected
94       directory: '',
95       // Only for 'jerasure' and 'isa' use
96       technique: 'reed_sol_van',
97       // Only for 'jerasure' use
98       packetSize: [2048, [Validators.min(1)]],
99       // Only for 'lrc' use
100       l: [
101         3, // Will be overwritten with plugin defaults
102         [
103           Validators.required,
104           Validators.min(1),
105           CdValidators.custom('unequal', (v: number) => this.lrcLocalityValidation(v))
106         ]
107       ],
108       crushLocality: '', // set to none at the end (same list as for failure domains)
109       // Only for 'shec' use
110       c: [
111         2, // Will be overwritten with plugin defaults
112         [
113           Validators.required,
114           Validators.min(1),
115           CdValidators.custom('cGreaterM', (v: number) => this.shecDurabilityValidation(v))
116         ]
117       ]
118     });
119     this.form.get('k').valueChanges.subscribe(() => this.updateValidityOnChange(['m', 'l']));
120     this.form.get('m').valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'l', 'c']));
121     this.form.get('l').valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'm']));
122     this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin));
123   }
124
125   private baseValueValidation(dataChunk: boolean = false): boolean {
126     return this.validValidation(() => {
127       return (
128         this.getKMSum() > this.deviceCount &&
129         this.form.getValue('k') > this.form.getValue('m') === dataChunk
130       );
131     });
132   }
133
134   private validValidation(fn: () => boolean, plugin?: string): boolean {
135     if (!this.form || plugin ? this.plugin !== plugin : false) {
136       return false;
137     }
138     return fn();
139   }
140
141   private getKMSum(): number {
142     return this.form.getValue('k') + this.form.getValue('m');
143   }
144
145   private lrcDataValidation(k: number): boolean {
146     return this.validValidation(() => {
147       const m = this.form.getValue('m');
148       const l = this.form.getValue('l');
149       const km = k + m;
150       this.lrcMultiK = k / (km / l);
151       return k % (km / l) !== 0;
152     }, 'lrc');
153   }
154
155   private shecDataValidation(k: number): boolean {
156     return this.validValidation(() => {
157       const m = this.form.getValue('m');
158       return m > k;
159     }, 'shec');
160   }
161
162   private lrcLocalityValidation(l: number) {
163     return this.validValidation(() => {
164       const value = this.getKMSum();
165       this.lrcGroups = l > 0 ? value / l : 0;
166       return l > 0 && value % l !== 0;
167     }, 'lrc');
168   }
169
170   private shecDurabilityValidation(c: number): boolean {
171     return this.validValidation(() => {
172       const m = this.form.getValue('m');
173       return c > m;
174     }, 'shec');
175   }
176
177   private updateValidityOnChange(names: string[]) {
178     names.forEach((name) => this.form.get(name).updateValueAndValidity({ emitEvent: false }));
179   }
180
181   private onPluginChange(plugin: string) {
182     this.plugin = plugin;
183     if (plugin === this.PLUGIN.JERASURE) {
184       this.setJerasureDefaults();
185     } else if (plugin === this.PLUGIN.LRC) {
186       this.setLrcDefaults();
187     } else if (plugin === this.PLUGIN.ISA) {
188       this.setIsaDefaults();
189     } else if (plugin === this.PLUGIN.SHEC) {
190       this.setShecDefaults();
191     }
192     this.updateValidityOnChange(['m']); // Triggers k, m, c and l
193   }
194
195   private setJerasureDefaults() {
196     this.setDefaults({
197       k: 4,
198       m: 2
199     });
200     this.techniques = [
201       'reed_sol_van',
202       'reed_sol_r6_op',
203       'cauchy_orig',
204       'cauchy_good',
205       'liberation',
206       'blaum_roth',
207       'liber8tion'
208     ];
209   }
210
211   private setLrcDefaults() {
212     this.setDefaults({
213       k: 4,
214       m: 2,
215       l: 3
216     });
217   }
218
219   private setIsaDefaults() {
220     /**
221      * Actually k and m are not required - but they will be set to the default values in case
222      * if they are not set, therefore it's fine to mark them as required in order to get
223      * strange values that weren't set.
224      */
225     this.setDefaults({
226       k: 7,
227       m: 3
228     });
229     this.techniques = ['reed_sol_van', 'cauchy'];
230   }
231
232   private setShecDefaults() {
233     /**
234      * Actually k, c and m are not required - but they will be set to the default values in case
235      * if they are not set, therefore it's fine to mark them as required in order to get
236      * strange values that weren't set.
237      */
238     this.setDefaults({
239       k: 4,
240       m: 3,
241       c: 2
242     });
243   }
244
245   private setDefaults(defaults: object) {
246     Object.keys(defaults).forEach((controlName) => {
247       const control = this.form.get(controlName);
248       const value = control.value;
249       let overwrite = control.pristine;
250       /**
251        * As k, m, c and l are now set touched and dirty on the beginning, plugin change will
252        * overwrite their values as we can't determine if the user has changed anything.
253        * k and m can have two default values where as l and c can only have one,
254        * so there is no need to overwrite them.
255        */
256       if ('k' === controlName) {
257         overwrite = [4, 7].includes(value);
258       } else if ('m' === controlName) {
259         overwrite = [2, 3].includes(value);
260       }
261       if (overwrite) {
262         this.form.get(controlName).setValue(defaults[controlName]);
263       }
264     });
265   }
266
267   ngOnInit() {
268     this.ecpService
269       .getInfo()
270       .subscribe(
271         ({
272           plugins,
273           names,
274           directory,
275           nodes
276         }: {
277           plugins: string[];
278           names: string[];
279           directory: string;
280           nodes: CrushNode[];
281         }) => {
282           this.initCrushNodeSelection(
283             nodes,
284             this.form.get('crushRoot'),
285             this.form.get('crushFailureDomain'),
286             this.form.get('crushDeviceClass')
287           );
288           this.plugins = plugins;
289           this.names = names;
290           this.form.silentSet('directory', directory);
291           this.preValidateNumericInputFields();
292         }
293       );
294   }
295
296   /**
297    * This allows k, m, l and c to be validated instantly on change, before the
298    * fields got changed before by the user.
299    */
300   private preValidateNumericInputFields() {
301     const kml = ['k', 'm', 'l', 'c'].map((name) => this.form.get(name));
302     kml.forEach((control) => {
303       control.markAsTouched();
304       control.markAsDirty();
305     });
306     kml[1].updateValueAndValidity(); // Update validity of k, m, c and l
307   }
308
309   onSubmit() {
310     if (this.form.invalid) {
311       this.form.setErrors({ cdSubmitButton: true });
312       return;
313     }
314     const profile = this.createJson();
315     this.taskWrapper
316       .wrapTaskAroundCall({
317         task: new FinishedTask('ecp/create', { name: profile.name }),
318         call: this.ecpService.create(profile)
319       })
320       .subscribe({
321         error: () => {
322           this.form.setErrors({ cdSubmitButton: true });
323         },
324         complete: () => {
325           this.activeModal.close();
326           this.submitAction.emit(profile);
327         }
328       });
329   }
330
331   private createJson() {
332     const pluginControls = {
333       technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE],
334       packetSize: [this.PLUGIN.JERASURE],
335       l: [this.PLUGIN.LRC],
336       crushLocality: [this.PLUGIN.LRC],
337       c: [this.PLUGIN.SHEC]
338     };
339     const ecp = new ErasureCodeProfile();
340     const plugin = this.form.getValue('plugin');
341     Object.keys(this.form.controls)
342       .filter((name) => {
343         const pluginControl = pluginControls[name];
344         const value = this.form.getValue(name);
345         const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl;
346         return usable && value && value !== '';
347       })
348       .forEach((name) => {
349         this.extendJson(name, ecp);
350       });
351     return ecp;
352   }
353
354   private extendJson(name: string, ecp: ErasureCodeProfile) {
355     const differentApiAttributes = {
356       crushFailureDomain: 'crush-failure-domain',
357       crushRoot: 'crush-root',
358       crushDeviceClass: 'crush-device-class',
359       packetSize: 'packetsize',
360       crushLocality: 'crush-locality'
361     };
362     const value = this.form.getValue(name);
363     ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value;
364   }
365 }