]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
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'; | |
e306af50 | 8 | import { CrushNodeSelectionClass } from '../../../shared/classes/crush.node.selection.class'; |
11fdf7f2 TL |
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'; | |
e306af50 | 13 | import { CrushNode } from '../../../shared/models/crush-node'; |
11fdf7f2 TL |
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({ | |
9f95a23c TL |
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'] | |
11fdf7f2 | 22 | }) |
e306af50 TL |
23 | export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClass |
24 | implements OnInit { | |
11fdf7f2 TL |
25 | @Output() |
26 | submitAction = new EventEmitter(); | |
27 | ||
11fdf7f2 | 28 | tooltips = this.ecpService.formTooltips; |
11fdf7f2 TL |
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; | |
e306af50 TL |
36 | |
37 | form: CdFormGroup; | |
38 | plugins: string[]; | |
39 | names: string[]; | |
40 | techniques: string[]; | |
11fdf7f2 TL |
41 | action: string; |
42 | resource: string; | |
e306af50 TL |
43 | lrcGroups: number; |
44 | lrcMultiK: number; | |
11fdf7f2 TL |
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 | ) { | |
e306af50 | 54 | super(); |
11fdf7f2 TL |
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', | |
9f95a23c | 70 | (value: string) => this.names && this.names.indexOf(value) !== -1 |
11fdf7f2 TL |
71 | ) |
72 | ] | |
73 | ], | |
74 | plugin: [this.PLUGIN.JERASURE, [Validators.required]], | |
e306af50 TL |
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: '', | |
11fdf7f2 | 97 | // Only for 'jerasure' and 'isa' use |
e306af50 | 98 | technique: 'reed_sol_van', |
11fdf7f2 TL |
99 | // Only for 'jerasure' use |
100 | packetSize: [2048, [Validators.min(1)]], | |
101 | // Only for 'lrc' use | |
e306af50 TL |
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) | |
11fdf7f2 | 111 | // Only for 'shec' use |
e306af50 TL |
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 | ] | |
11fdf7f2 | 120 | }); |
e306af50 TL |
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'])); | |
11fdf7f2 TL |
124 | this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin)); |
125 | } | |
126 | ||
e306af50 TL |
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) { | |
11fdf7f2 TL |
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 | } | |
e306af50 | 194 | this.updateValidityOnChange(['m']); // Triggers k, m, c and l |
11fdf7f2 TL |
195 | } |
196 | ||
197 | private setJerasureDefaults() { | |
11fdf7f2 TL |
198 | this.setDefaults({ |
199 | k: 4, | |
200 | m: 2 | |
201 | }); | |
11fdf7f2 TL |
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() { | |
11fdf7f2 TL |
214 | this.setDefaults({ |
215 | k: 4, | |
216 | m: 2, | |
217 | l: 3 | |
218 | }); | |
219 | } | |
220 | ||
221 | private setIsaDefaults() { | |
e306af50 TL |
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 | */ | |
11fdf7f2 TL |
227 | this.setDefaults({ |
228 | k: 7, | |
229 | m: 3 | |
230 | }); | |
231 | this.techniques = ['reed_sol_van', 'cauchy']; | |
232 | } | |
233 | ||
234 | private setShecDefaults() { | |
e306af50 TL |
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 | */ | |
11fdf7f2 TL |
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) => { | |
e306af50 TL |
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]); | |
11fdf7f2 TL |
265 | } |
266 | }); | |
267 | } | |
268 | ||
269 | ngOnInit() { | |
270 | this.ecpService | |
271 | .getInfo() | |
272 | .subscribe( | |
273 | ({ | |
11fdf7f2 TL |
274 | plugins, |
275 | names, | |
276 | directory, | |
e306af50 | 277 | nodes |
11fdf7f2 | 278 | }: { |
11fdf7f2 TL |
279 | plugins: string[]; |
280 | names: string[]; | |
281 | directory: string; | |
e306af50 | 282 | nodes: CrushNode[]; |
11fdf7f2 | 283 | }) => { |
e306af50 TL |
284 | this.initCrushNodeSelection( |
285 | nodes, | |
286 | this.form.get('crushRoot'), | |
287 | this.form.get('crushFailureDomain'), | |
288 | this.form.get('crushDeviceClass') | |
289 | ); | |
11fdf7f2 TL |
290 | this.plugins = plugins; |
291 | this.names = names; | |
11fdf7f2 | 292 | this.form.silentSet('directory', directory); |
e306af50 TL |
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); | |
11fdf7f2 TL |
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]; | |
e306af50 | 347 | const value = this.form.getValue(name); |
11fdf7f2 | 348 | const usable = (pluginControl && pluginControl.includes(plugin)) || !pluginControl; |
e306af50 | 349 | return usable && value && value !== ''; |
11fdf7f2 TL |
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 | }; | |
e306af50 TL |
365 | const value = this.form.getValue(name); |
366 | ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value; | |
11fdf7f2 TL |
367 | } |
368 | } |