]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
1521ae83f1b20ca56deaa6ce4cf8604183e9d592
[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           CdValidators.custom('max', () => this.baseValueValidation(true)),
83           CdValidators.custom('unequal', (v: number) => this.lrcDataValidation(v)),
84           CdValidators.custom('kLowerM', (v: number) => this.shecDataValidation(v))
85         ]
86       ],
87       m: [
88         2, // Will be overwritten with plugin defaults
89         [Validators.required, CdValidators.custom('max', () => this.baseValueValidation())]
90       ],
91       crushFailureDomain: '', // Will be preselected
92       crushNumFailureDomains: [
93         0,
94         CdValidators.requiredIf({ crushOsdsPerFailureDomain: { op: 'minValue', arg1: 1 } })
95       ],
96       crushOsdsPerFailureDomain: [
97         0,
98         CdValidators.requiredIf({ crushNumFailureDomains: { op: 'minValue', arg1: 1 } })
99       ],
100       crushRoot: null, // Will be preselected
101       crushDeviceClass: '', // Will be preselected
102       directory: '',
103       // Only for 'jerasure', 'clay' and 'isa' use
104       technique: 'reed_sol_van',
105       // Only for 'jerasure' use
106       packetSize: [2048],
107       // Only for 'lrc' use
108       l: [
109         3, // Will be overwritten with plugin defaults
110         [
111           Validators.required,
112           CdValidators.custom('unequal', (v: number) => this.lrcLocalityValidation(v))
113         ]
114       ],
115       crushLocality: '', // set to none at the end (same list as for failure domains)
116       // Only for 'shec' use
117       c: [
118         2, // Will be overwritten with plugin defaults
119         [
120           Validators.required,
121           CdValidators.custom('cGreaterM', (v: number) => this.shecDurabilityValidation(v))
122         ]
123       ],
124       // Only for 'clay' use
125       d: [
126         5, // Will be overwritten with plugin defaults (k+m-1) = k+1 <= d <= k+m-1
127         [
128           Validators.required,
129           CdValidators.custom('dMin', (v: number) => this.dMinValidation(v)),
130           CdValidators.custom('dMax', (v: number) => this.dMaxValidation(v))
131         ]
132       ],
133       scalar_mds: [this.PLUGIN.JERASURE, [Validators.required]] // jerasure or isa or shec
134     });
135     this.toggleDCalc();
136     this.form.get('k').valueChanges.subscribe(() => this.updateValidityOnChange(['m', 'l', 'd']));
137     this.form
138       .get('m')
139       .valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'l', 'c', 'd']));
140     this.form.get('l').valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'm']));
141     this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin));
142     this.form.get('scalar_mds').valueChanges.subscribe(() => this.setClayDefaultsForScalar());
143   }
144
145   private baseValueValidation(dataChunk: boolean = false): boolean {
146     return this.validValidation(() => {
147       return (
148         this.getKMSum() > this.deviceCount &&
149         this.form.getValue('k') > this.form.getValue('m') === dataChunk
150       );
151     });
152   }
153
154   private validValidation(fn: () => boolean, plugin?: string): boolean {
155     if (!this.form || plugin ? this.plugin !== plugin : false) {
156       return false;
157     }
158     return fn();
159   }
160
161   private getKMSum(): number {
162     return this.form.getValue('k') + this.form.getValue('m');
163   }
164
165   private lrcDataValidation(k: number): boolean {
166     return this.validValidation(() => {
167       const m = this.form.getValue('m');
168       const l = this.form.getValue('l');
169       const km = k + m;
170       this.lrcMultiK = k / (km / l);
171       return k % (km / l) !== 0;
172     }, 'lrc');
173   }
174
175   private shecDataValidation(k: number): boolean {
176     return this.validValidation(() => {
177       const m = this.form.getValue('m');
178       return m > k;
179     }, 'shec');
180   }
181
182   private lrcLocalityValidation(l: number) {
183     return this.validValidation(() => {
184       const value = this.getKMSum();
185       this.lrcGroups = l > 0 ? value / l : 0;
186       return l > 0 && value % l !== 0;
187     }, 'lrc');
188   }
189
190   private shecDurabilityValidation(c: number): boolean {
191     return this.validValidation(() => {
192       const m = this.form.getValue('m');
193       return c > m;
194     }, 'shec');
195   }
196
197   private dMinValidation(d: number): boolean {
198     return this.validValidation(() => this.getDMin() > d, 'clay');
199   }
200
201   getDMin(): number {
202     return this.form.getValue('k') + 1;
203   }
204
205   private dMaxValidation(d: number): boolean {
206     return this.validValidation(() => d > this.getDMax(), 'clay');
207   }
208
209   getDMax(): number {
210     const m = this.form.getValue('m');
211     const k = this.form.getValue('k');
212     return k + m - 1;
213   }
214
215   toggleDCalc() {
216     this.dCalc = !this.dCalc;
217     this.form.get('d')[this.dCalc ? 'disable' : 'enable']();
218     this.calculateD();
219   }
220
221   private calculateD() {
222     if (this.plugin !== this.PLUGIN.CLAY || !this.dCalc) {
223       return;
224     }
225     this.form.silentSet('d', this.getDMax());
226   }
227
228   private updateValidityOnChange(names: string[]) {
229     names.forEach((name) => {
230       if (name === 'd') {
231         this.calculateD();
232       }
233       this.form.get(name).updateValueAndValidity({ emitEvent: false });
234     });
235   }
236
237   private onPluginChange(plugin: string) {
238     this.plugin = plugin;
239     if (plugin === this.PLUGIN.JERASURE) {
240       this.setJerasureDefaults();
241     } else if (plugin === this.PLUGIN.LRC) {
242       this.setLrcDefaults();
243     } else if (plugin === this.PLUGIN.ISA) {
244       this.setIsaDefaults();
245     } else if (plugin === this.PLUGIN.SHEC) {
246       this.setShecDefaults();
247     } else if (plugin === this.PLUGIN.CLAY) {
248       this.setClayDefaults();
249     }
250     this.updateValidityOnChange(['m']); // Triggers k, m, c, d and l
251   }
252
253   private setJerasureDefaults() {
254     this.techniques = [
255       'reed_sol_van',
256       'reed_sol_r6_op',
257       'cauchy_orig',
258       'cauchy_good',
259       'liberation',
260       'blaum_roth',
261       'liber8tion'
262     ];
263     this.setDefaults({
264       k: 4,
265       m: 2,
266       technique: 'reed_sol_van'
267     });
268   }
269
270   private setLrcDefaults() {
271     this.setDefaults({
272       k: 4,
273       m: 2,
274       l: 3
275     });
276   }
277
278   private setIsaDefaults() {
279     /**
280      * Actually k and m are not required - but they will be set to the default values in case
281      * if they are not set, therefore it's fine to mark them as required in order to get
282      * strange values that weren't set.
283      */
284     this.techniques = ['reed_sol_van', 'cauchy'];
285     this.setDefaults({
286       k: 7,
287       m: 3,
288       technique: 'reed_sol_van'
289     });
290   }
291
292   private setShecDefaults() {
293     /**
294      * Actually k, c and m are not required - but they will be set to the default values in case
295      * if they are not set, therefore it's fine to mark them as required in order to get
296      * strange values that weren't set.
297      */
298     this.setDefaults({
299       k: 4,
300       m: 3,
301       c: 2
302     });
303   }
304
305   private setClayDefaults() {
306     /**
307      * Actually d and scalar_mds are not required - but they will be set to show the default values
308      * in case if they are not set, therefore it's fine to mark them as required in order to not get
309      * strange values that weren't set.
310      *
311      * As d would be set to the value k+m-1 for the greatest savings, the form will
312      * automatically update d if the automatic calculation is activated (default).
313      */
314     this.setDefaults({
315       k: 4,
316       m: 2,
317       // d: 5, <- Will be automatically update to 5
318       scalar_mds: this.PLUGIN.JERASURE
319     });
320     this.setClayDefaultsForScalar();
321   }
322
323   private setClayDefaultsForScalar() {
324     const plugin = this.form.getValue('scalar_mds');
325     let defaultTechnique = 'reed_sol_van';
326     if (plugin === this.PLUGIN.JERASURE) {
327       this.techniques = [
328         'reed_sol_van',
329         'reed_sol_r6_op',
330         'cauchy_orig',
331         'cauchy_good',
332         'liber8tion'
333       ];
334     } else if (plugin === this.PLUGIN.ISA) {
335       this.techniques = ['reed_sol_van', 'cauchy'];
336     } else {
337       // this.PLUGIN.SHEC
338       defaultTechnique = 'single';
339       this.techniques = ['single', 'multiple'];
340     }
341     this.setDefaults({ technique: defaultTechnique });
342   }
343
344   private setDefaults(defaults: object) {
345     Object.keys(defaults).forEach((controlName) => {
346       const control = this.form.get(controlName);
347       const value = control.value;
348       /**
349        * As k, m, c and l are now set touched and dirty on the beginning, plugin change will
350        * overwrite their values as we can't determine if the user has changed anything.
351        * k and m can have two default values where as l and c can only have one,
352        * so there is no need to overwrite them.
353        */
354       const overwrite =
355         control.pristine ||
356         (controlName === 'technique' && !this.techniques.includes(value)) ||
357         (controlName === 'k' && [4, 7].includes(value)) ||
358         (controlName === 'm' && [2, 3].includes(value));
359       if (overwrite) {
360         control.setValue(defaults[controlName]); // also validates new value
361       } else {
362         control.updateValueAndValidity();
363       }
364     });
365   }
366
367   ngOnInit() {
368     this.ecpService
369       .getInfo()
370       .subscribe(
371         ({
372           plugins,
373           names,
374           directory,
375           nodes
376         }: {
377           plugins: string[];
378           names: string[];
379           directory: string;
380           nodes: CrushNode[];
381         }) => {
382           this.initCrushNodeSelection(
383             nodes,
384             this.form.get('crushRoot'),
385             this.form.get('crushFailureDomain'),
386             this.form.get('crushDeviceClass'),
387             false
388           );
389           this.plugins = plugins;
390           this.names = names;
391           this.form.silentSet('directory', directory);
392           this.preValidateNumericInputFields();
393         }
394       );
395   }
396
397   /**
398    * This allows k, m, l and c to be validated instantly on change, before the
399    * fields got changed before by the user.
400    */
401   private preValidateNumericInputFields() {
402     const kml = ['k', 'm', 'l', 'c', 'd'].map((name) => this.form.get(name));
403     kml.forEach((control) => {
404       control.markAsTouched();
405       control.markAsDirty();
406     });
407     kml[1].updateValueAndValidity(); // Update validity of k, m, c, d and l
408   }
409
410   onSubmit() {
411     if (this.form.invalid) {
412       this.form.setErrors({ cdSubmitButton: true });
413       return;
414     }
415     const profile = this.createJson();
416     this.taskWrapper
417       .wrapTaskAroundCall({
418         task: new FinishedTask('ecp/create', { name: profile.name }),
419         call: this.ecpService.create(profile)
420       })
421       .subscribe({
422         error: () => {
423           this.form.setErrors({ cdSubmitButton: true });
424         },
425         complete: () => {
426           this.activeModal.close();
427           this.submitAction.emit(profile);
428         }
429       });
430   }
431
432   private createJson() {
433     const pluginControls = {
434       technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE, this.PLUGIN.CLAY],
435       packetSize: [this.PLUGIN.JERASURE],
436       l: [this.PLUGIN.LRC],
437       crushLocality: [this.PLUGIN.LRC],
438       c: [this.PLUGIN.SHEC],
439       d: [this.PLUGIN.CLAY],
440       scalar_mds: [this.PLUGIN.CLAY]
441     };
442     const ecp = new ErasureCodeProfile();
443     const plugin = this.form.getValue('plugin');
444     Object.keys(this.form.controls)
445       .filter((name) => {
446         const pluginControl = pluginControls[name];
447         const value = this.form.getValue(name);
448         const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl;
449         return usable && value && value !== '';
450       })
451       .forEach((name) => {
452         this.extendJson(name, ecp);
453       });
454     return ecp;
455   }
456
457   private extendJson(name: string, ecp: ErasureCodeProfile) {
458     const differentApiAttributes = {
459       crushFailureDomain: 'crush-failure-domain',
460       crushNumFailureDomains: 'crush-num-failure-domains',
461       crushOsdsPerFailureDomain: 'crush-osds-per-failure-domain',
462       crushRoot: 'crush-root',
463       crushDeviceClass: 'crush-device-class',
464       packetSize: 'packetsize',
465       crushLocality: 'crush-locality'
466     };
467     const value = this.form.getValue(name);
468     ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value;
469   }
470 }