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