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