]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
import 15.2.9
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / erasure-code-profile-form / erasure-code-profile-form-modal.component.ts
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 { Icons } from '../../../shared/enum/icons.enum';
11 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
12 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
13 import { CdValidators } from '../../../shared/forms/cd-validators';
14 import { CrushNode } from '../../../shared/models/crush-node';
15 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
16 import { FinishedTask } from '../../../shared/models/finished-task';
17 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
18
19 @Component({
20 selector: 'cd-erasure-code-profile-form-modal',
21 templateUrl: './erasure-code-profile-form-modal.component.html',
22 styleUrls: ['./erasure-code-profile-form-modal.component.scss']
23 })
24 export class ErasureCodeProfileFormModalComponent 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 bsModalRef: BsModalRef,
53 private taskWrapper: TaskWrapperService,
54 private ecpService: ErasureCodeProfileService,
55 private i18n: I18n,
56 public actionLabels: ActionLabelsI18n
57 ) {
58 super();
59 this.action = this.actionLabels.CREATE;
60 this.resource = this.i18n('EC Profile');
61 this.createForm();
62 this.setJerasureDefaults();
63 }
64
65 createForm() {
66 this.form = this.formBuilder.group({
67 name: [
68 null,
69 [
70 Validators.required,
71 Validators.pattern('[A-Za-z0-9_-]+'),
72 CdValidators.custom(
73 'uniqueName',
74 (value: string) => this.names && this.names.indexOf(value) !== -1
75 )
76 ]
77 ],
78 plugin: [this.PLUGIN.JERASURE, [Validators.required]],
79 k: [
80 4, // Will be overwritten with plugin defaults
81 [
82 Validators.required,
83 Validators.min(2),
84 CdValidators.custom('max', () => this.baseValueValidation(true)),
85 CdValidators.custom('unequal', (v: number) => this.lrcDataValidation(v)),
86 CdValidators.custom('kLowerM', (v: number) => this.shecDataValidation(v))
87 ]
88 ],
89 m: [
90 2, // Will be overwritten with plugin defaults
91 [
92 Validators.required,
93 Validators.min(1),
94 CdValidators.custom('max', () => this.baseValueValidation())
95 ]
96 ],
97 crushFailureDomain: '', // Will be preselected
98 crushRoot: null, // Will be preselected
99 crushDeviceClass: '', // Will be preselected
100 directory: '',
101 // Only for 'jerasure', 'clay' and 'isa' use
102 technique: 'reed_sol_van',
103 // Only for 'jerasure' use
104 packetSize: [2048, [Validators.min(1)]],
105 // Only for 'lrc' use
106 l: [
107 3, // Will be overwritten with plugin defaults
108 [
109 Validators.required,
110 Validators.min(1),
111 CdValidators.custom('unequal', (v: number) => this.lrcLocalityValidation(v))
112 ]
113 ],
114 crushLocality: '', // set to none at the end (same list as for failure domains)
115 // Only for 'shec' use
116 c: [
117 2, // Will be overwritten with plugin defaults
118 [
119 Validators.required,
120 Validators.min(1),
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 );
388 this.plugins = plugins;
389 this.names = names;
390 this.form.silentSet('directory', directory);
391 this.preValidateNumericInputFields();
392 }
393 );
394 }
395
396 /**
397 * This allows k, m, l and c to be validated instantly on change, before the
398 * fields got changed before by the user.
399 */
400 private preValidateNumericInputFields() {
401 const kml = ['k', 'm', 'l', 'c', 'd'].map((name) => this.form.get(name));
402 kml.forEach((control) => {
403 control.markAsTouched();
404 control.markAsDirty();
405 });
406 kml[1].updateValueAndValidity(); // Update validity of k, m, c, d and l
407 }
408
409 onSubmit() {
410 if (this.form.invalid) {
411 this.form.setErrors({ cdSubmitButton: true });
412 return;
413 }
414 const profile = this.createJson();
415 this.taskWrapper
416 .wrapTaskAroundCall({
417 task: new FinishedTask('ecp/create', { name: profile.name }),
418 call: this.ecpService.create(profile)
419 })
420 .subscribe(
421 undefined,
422 () => {
423 this.form.setErrors({ cdSubmitButton: true });
424 },
425 () => {
426 this.bsModalRef.hide();
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 crushRoot: 'crush-root',
461 crushDeviceClass: 'crush-device-class',
462 packetSize: 'packetsize',
463 crushLocality: 'crush-locality'
464 };
465 const value = this.form.getValue(name);
466 ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value;
467 }
468 }