1 import { Component, EventEmitter, OnInit, Output } from '@angular/core';
2 import { Validators } from '@angular/forms';
4 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
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';
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']
23 export class ErasureCodeProfileFormModalComponent
24 extends CrushNodeSelectionClass
27 submitAction = new EventEmitter();
29 tooltips = this.ecpService.formTooltips;
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
37 plugin = this.PLUGIN.JERASURE;
51 private formBuilder: CdFormBuilder,
52 public activeModal: NgbActiveModal,
53 private taskWrapper: TaskWrapperService,
54 private ecpService: ErasureCodeProfileService,
55 public actionLabels: ActionLabelsI18n
58 this.action = this.actionLabels.CREATE;
59 this.resource = $localize`EC Profile`;
61 this.setJerasureDefaults();
65 this.form = this.formBuilder.group({
70 Validators.pattern('[A-Za-z0-9_-]+'),
73 (value: string) => this.names && this.names.indexOf(value) !== -1
77 plugin: [this.PLUGIN.JERASURE, [Validators.required]],
79 4, // Will be overwritten with plugin defaults
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))
89 2, // Will be overwritten with plugin defaults
93 CdValidators.custom('max', () => this.baseValueValidation())
96 crushFailureDomain: '', // Will be preselected
97 crushRoot: null, // Will be preselected
98 crushDeviceClass: '', // Will be preselected
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
106 3, // Will be overwritten with plugin defaults
110 CdValidators.custom('unequal', (v: number) => this.lrcLocalityValidation(v))
113 crushLocality: '', // set to none at the end (same list as for failure domains)
114 // Only for 'shec' use
116 2, // Will be overwritten with plugin defaults
120 CdValidators.custom('cGreaterM', (v: number) => this.shecDurabilityValidation(v))
123 // Only for 'clay' use
125 5, // Will be overwritten with plugin defaults (k+m-1) = k+1 <= d <= k+m-1
128 CdValidators.custom('dMin', (v: number) => this.dMinValidation(v)),
129 CdValidators.custom('dMax', (v: number) => this.dMaxValidation(v))
132 scalar_mds: [this.PLUGIN.JERASURE, [Validators.required]] // jerasure or isa or shec
135 this.form.get('k').valueChanges.subscribe(() => this.updateValidityOnChange(['m', 'l', 'd']));
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());
144 private baseValueValidation(dataChunk: boolean = false): boolean {
145 return this.validValidation(() => {
147 this.getKMSum() > this.deviceCount &&
148 this.form.getValue('k') > this.form.getValue('m') === dataChunk
153 private validValidation(fn: () => boolean, plugin?: string): boolean {
154 if (!this.form || plugin ? this.plugin !== plugin : false) {
160 private getKMSum(): number {
161 return this.form.getValue('k') + this.form.getValue('m');
164 private lrcDataValidation(k: number): boolean {
165 return this.validValidation(() => {
166 const m = this.form.getValue('m');
167 const l = this.form.getValue('l');
169 this.lrcMultiK = k / (km / l);
170 return k % (km / l) !== 0;
174 private shecDataValidation(k: number): boolean {
175 return this.validValidation(() => {
176 const m = this.form.getValue('m');
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;
189 private shecDurabilityValidation(c: number): boolean {
190 return this.validValidation(() => {
191 const m = this.form.getValue('m');
196 private dMinValidation(d: number): boolean {
197 return this.validValidation(() => this.getDMin() > d, 'clay');
201 return this.form.getValue('k') + 1;
204 private dMaxValidation(d: number): boolean {
205 return this.validValidation(() => d > this.getDMax(), 'clay');
209 const m = this.form.getValue('m');
210 const k = this.form.getValue('k');
215 this.dCalc = !this.dCalc;
216 this.form.get('d')[this.dCalc ? 'disable' : 'enable']();
220 private calculateD() {
221 if (this.plugin !== this.PLUGIN.CLAY || !this.dCalc) {
224 this.form.silentSet('d', this.getDMax());
227 private updateValidityOnChange(names: string[]) {
228 names.forEach((name) => {
232 this.form.get(name).updateValueAndValidity({ emitEvent: false });
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();
249 this.updateValidityOnChange(['m']); // Triggers k, m, c, d and l
252 private setJerasureDefaults() {
265 technique: 'reed_sol_van'
269 private setLrcDefaults() {
277 private setIsaDefaults() {
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.
283 this.techniques = ['reed_sol_van', 'cauchy'];
287 technique: 'reed_sol_van'
291 private setShecDefaults() {
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.
304 private setClayDefaults() {
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.
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).
316 // d: 5, <- Will be automatically update to 5
317 scalar_mds: this.PLUGIN.JERASURE
319 this.setClayDefaultsForScalar();
322 private setClayDefaultsForScalar() {
323 const plugin = this.form.getValue('scalar_mds');
324 let defaultTechnique = 'reed_sol_van';
325 if (plugin === this.PLUGIN.JERASURE) {
333 } else if (plugin === this.PLUGIN.ISA) {
334 this.techniques = ['reed_sol_van', 'cauchy'];
337 defaultTechnique = 'single';
338 this.techniques = ['single', 'multiple'];
340 this.setDefaults({ technique: defaultTechnique });
343 private setDefaults(defaults: object) {
344 Object.keys(defaults).forEach((controlName) => {
345 const control = this.form.get(controlName);
346 const value = control.value;
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.
355 (controlName === 'technique' && !this.techniques.includes(value)) ||
356 (controlName === 'k' && [4, 7].includes(value)) ||
357 (controlName === 'm' && [2, 3].includes(value));
359 control.setValue(defaults[controlName]); // also validates new value
361 control.updateValueAndValidity();
381 this.initCrushNodeSelection(
383 this.form.get('crushRoot'),
384 this.form.get('crushFailureDomain'),
385 this.form.get('crushDeviceClass')
387 this.plugins = plugins;
389 this.form.silentSet('directory', directory);
390 this.preValidateNumericInputFields();
396 * This allows k, m, l and c to be validated instantly on change, before the
397 * fields got changed before by the user.
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();
405 kml[1].updateValueAndValidity(); // Update validity of k, m, c, d and l
409 if (this.form.invalid) {
410 this.form.setErrors({ cdSubmitButton: true });
413 const profile = this.createJson();
415 .wrapTaskAroundCall({
416 task: new FinishedTask('ecp/create', { name: profile.name }),
417 call: this.ecpService.create(profile)
421 this.form.setErrors({ cdSubmitButton: true });
424 this.activeModal.close();
425 this.submitAction.emit(profile);
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]
440 const ecp = new ErasureCodeProfile();
441 const plugin = this.form.getValue('plugin');
442 Object.keys(this.form.controls)
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 !== '';
450 this.extendJson(name, ecp);
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'
463 const value = this.form.getValue(name);
464 ecp[differentApiAttributes[name] || name] = name === 'crushRoot' ? value.name : value;