]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
import 15.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / erasure-code-profile-form / erasure-code-profile-form-modal.component.ts
CommitLineData
11fdf7f2
TL
1import { Component, EventEmitter, OnInit, Output } from '@angular/core';
2import { Validators } from '@angular/forms';
3
4import { I18n } from '@ngx-translate/i18n-polyfill';
5import { BsModalRef } from 'ngx-bootstrap/modal';
6
7import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
e306af50 8import { CrushNodeSelectionClass } from '../../../shared/classes/crush.node.selection.class';
11fdf7f2
TL
9import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
10import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
11import { CdFormGroup } from '../../../shared/forms/cd-form-group';
12import { CdValidators } from '../../../shared/forms/cd-validators';
e306af50 13import { CrushNode } from '../../../shared/models/crush-node';
11fdf7f2
TL
14import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
15import { FinishedTask } from '../../../shared/models/finished-task';
16import { 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
23export 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}