]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
import 15.2.1 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / pool-form / pool-form.component.ts
1 import { Component, EventEmitter, OnInit, ViewChild } from '@angular/core';
2 import { FormControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import { BsModalService } from 'ngx-bootstrap/modal';
8 import { TabsetComponent } from 'ngx-bootstrap/tabs';
9 import { TooltipDirective } from 'ngx-bootstrap/tooltip';
10 import { Subscription } from 'rxjs';
11
12 import { CrushRuleService } from '../../../shared/api/crush-rule.service';
13 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
14 import { PoolService } from '../../../shared/api/pool.service';
15 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
16 import { SelectOption } from '../../../shared/components/select/select-option.model';
17 import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
18 import { Icons } from '../../../shared/enum/icons.enum';
19 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
20 import { CdValidators } from '../../../shared/forms/cd-validators';
21 import {
22 RbdConfigurationEntry,
23 RbdConfigurationSourceField
24 } from '../../../shared/models/configuration';
25 import { CrushRule, CrushRuleConfig } from '../../../shared/models/crush-rule';
26 import { CrushStep } from '../../../shared/models/crush-step';
27 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
28 import { FinishedTask } from '../../../shared/models/finished-task';
29 import { Permission } from '../../../shared/models/permissions';
30 import { PoolFormInfo } from '../../../shared/models/pool-form-info';
31 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
32 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
33 import { FormatterService } from '../../../shared/services/formatter.service';
34 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
35 import { CrushRuleFormModalComponent } from '../crush-rule-form-modal/crush-rule-form-modal.component';
36 import { ErasureCodeProfileFormModalComponent } from '../erasure-code-profile-form/erasure-code-profile-form-modal.component';
37 import { Pool } from '../pool';
38 import { PoolFormData } from './pool-form-data';
39
40 interface FormFieldDescription {
41 externalFieldName: string;
42 formControlName: string;
43 attr?: string;
44 replaceFn?: Function;
45 editable?: boolean;
46 resetValue?: any;
47 }
48
49 @Component({
50 selector: 'cd-pool-form',
51 templateUrl: './pool-form.component.html',
52 styleUrls: ['./pool-form.component.scss']
53 })
54 export class PoolFormComponent implements OnInit {
55 @ViewChild('crushInfoTabs', { static: false }) crushInfoTabs: TabsetComponent;
56 @ViewChild('crushDeletionBtn', { static: false }) crushDeletionBtn: TooltipDirective;
57
58 permission: Permission;
59 form: CdFormGroup;
60 ecProfiles: ErasureCodeProfile[];
61 info: PoolFormInfo;
62 routeParamsSubscribe: any;
63 editing = false;
64 isReplicated = false;
65 isErasure = false;
66 data = new PoolFormData(this.i18n);
67 externalPgChange = false;
68 current: Record<string, any> = {
69 rules: []
70 };
71 initializeConfigData = new EventEmitter<{
72 initialData: RbdConfigurationEntry[];
73 sourceType: RbdConfigurationSourceField;
74 }>();
75 currentConfigurationValues: { [configKey: string]: any } = {};
76 action: string;
77 resource: string;
78 icons = Icons;
79 pgAutoscaleModes: string[];
80 crushUsage: string[] = undefined; // Will only be set if a rule is used by some pool
81
82 private modalSubscription: Subscription;
83
84 constructor(
85 private dimlessBinaryPipe: DimlessBinaryPipe,
86 private route: ActivatedRoute,
87 private router: Router,
88 private modalService: BsModalService,
89 private poolService: PoolService,
90 private authStorageService: AuthStorageService,
91 private formatter: FormatterService,
92 private bsModalService: BsModalService,
93 private taskWrapper: TaskWrapperService,
94 private ecpService: ErasureCodeProfileService,
95 private crushRuleService: CrushRuleService,
96 private i18n: I18n,
97 public actionLabels: ActionLabelsI18n
98 ) {
99 this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`);
100 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
101 this.resource = this.i18n('pool');
102 this.authenticate();
103 this.createForm();
104 }
105
106 authenticate() {
107 this.permission = this.authStorageService.getPermissions().pool;
108 if (
109 !this.permission.read ||
110 (!this.permission.update && this.editing) ||
111 (!this.permission.create && !this.editing)
112 ) {
113 this.router.navigate(['/404']);
114 }
115 }
116
117 private createForm() {
118 const compressionForm = new CdFormGroup({
119 mode: new FormControl('none'),
120 algorithm: new FormControl(''),
121 minBlobSize: new FormControl('', {
122 updateOn: 'blur'
123 }),
124 maxBlobSize: new FormControl('', {
125 updateOn: 'blur'
126 }),
127 ratio: new FormControl('', {
128 updateOn: 'blur'
129 })
130 });
131
132 this.form = new CdFormGroup(
133 {
134 name: new FormControl('', {
135 validators: [
136 Validators.pattern(/^[.A-Za-z0-9_/-]+$/),
137 Validators.required,
138 CdValidators.custom('rbdPool', () => {
139 return (
140 this.form &&
141 this.form.getValue('name').includes('/') &&
142 this.data &&
143 this.data.applications.selected.indexOf('rbd') !== -1
144 );
145 })
146 ]
147 }),
148 poolType: new FormControl('', {
149 validators: [Validators.required]
150 }),
151 crushRule: new FormControl(null, {
152 validators: [
153 CdValidators.custom(
154 'tooFewOsds',
155 (rule: any) => this.info && rule && this.info.osd_count < rule.min_size
156 ),
157 CdValidators.custom(
158 'required',
159 (rule: CrushRule) =>
160 this.isReplicated && this.info.crush_rules_replicated.length > 0 && !rule
161 )
162 ]
163 }),
164 size: new FormControl('', {
165 updateOn: 'blur'
166 }),
167 erasureProfile: new FormControl(null),
168 pgNum: new FormControl('', {
169 validators: [Validators.required, Validators.min(1)]
170 }),
171 pgAutoscaleMode: new FormControl(null),
172 ecOverwrites: new FormControl(false),
173 compression: compressionForm,
174 max_bytes: new FormControl(''),
175 max_objects: new FormControl(0, {
176 validators: [Validators.min(0)]
177 })
178 },
179 [CdValidators.custom('form', (): null => null)]
180 );
181 }
182
183 ngOnInit() {
184 this.poolService.getInfo().subscribe((info: PoolFormInfo) => {
185 this.initInfo(info);
186 if (this.editing) {
187 this.initEditMode();
188 } else {
189 this.setAvailableApps();
190 }
191 this.listenToChanges();
192 this.setComplexValidators();
193 });
194 }
195
196 private initInfo(info: PoolFormInfo) {
197 this.pgAutoscaleModes = info.pg_autoscale_modes;
198 this.form.silentSet('pgAutoscaleMode', info.pg_autoscale_default_mode);
199 this.form.silentSet('algorithm', info.bluestore_compression_algorithm);
200 this.info = info;
201 this.initEcp(info.erasure_code_profiles);
202 }
203
204 private initEcp(ecProfiles: ErasureCodeProfile[]) {
205 this.setListControlStatus('erasureProfile', ecProfiles);
206 this.ecProfiles = ecProfiles;
207 }
208
209 /**
210 * Used to update the crush rule or erasure code profile listings.
211 *
212 * If only one rule or profile exists it will be selected.
213 * If nothing exists null will be selected.
214 * If more than one rule or profile exists the listing will be enabled,
215 * otherwise disabled.
216 */
217 private setListControlStatus(controlName: string, arr: any[]) {
218 const control = this.form.get(controlName);
219 const value = control.value;
220 if (arr.length === 1 && (!value || !_.isEqual(value, arr[0]))) {
221 control.setValue(arr[0]);
222 } else if (arr.length === 0 && value) {
223 control.setValue(null);
224 }
225 if (arr.length <= 1) {
226 if (control.enabled) {
227 control.disable();
228 }
229 } else if (control.disabled) {
230 control.enable();
231 }
232 }
233
234 private initEditMode() {
235 this.disableForEdit();
236 this.routeParamsSubscribe = this.route.params.subscribe((param: { name: string }) =>
237 this.poolService.get(param.name).subscribe((pool: Pool) => {
238 this.data.pool = pool;
239 this.initEditFormData(pool);
240 })
241 );
242 }
243
244 private disableForEdit() {
245 ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((controlName) =>
246 this.form.get(controlName).disable()
247 );
248 }
249
250 private initEditFormData(pool: Pool) {
251 this.initializeConfigData.emit({
252 initialData: pool.configuration,
253 sourceType: RbdConfigurationSourceField.pool
254 });
255 this.poolTypeChange(pool.type);
256 const rules = this.info.crush_rules_replicated.concat(this.info.crush_rules_erasure);
257 const dataMap = {
258 name: pool.pool_name,
259 poolType: pool.type,
260 crushRule: rules.find((rule: CrushRule) => rule.rule_name === pool.crush_rule),
261 size: pool.size,
262 erasureProfile: this.ecProfiles.find((ecp) => ecp.name === pool.erasure_code_profile),
263 pgAutoscaleMode: pool.pg_autoscale_mode,
264 pgNum: pool.pg_num,
265 ecOverwrites: pool.flags_names.includes('ec_overwrites'),
266 mode: pool.options.compression_mode,
267 algorithm: pool.options.compression_algorithm,
268 minBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_min_blob_size),
269 maxBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_max_blob_size),
270 ratio: pool.options.compression_required_ratio,
271 max_bytes: this.dimlessBinaryPipe.transform(pool.quota_max_bytes),
272 max_objects: pool.quota_max_objects
273 };
274 Object.keys(dataMap).forEach((controlName: string) => {
275 const value = dataMap[controlName];
276 if (!_.isUndefined(value) && value !== '') {
277 this.form.silentSet(controlName, value);
278 }
279 });
280 this.data.pgs = this.form.getValue('pgNum');
281 this.setAvailableApps(this.data.applications.default.concat(pool.application_metadata));
282 this.data.applications.selected = pool.application_metadata;
283 }
284
285 private setAvailableApps(apps: string[] = this.data.applications.default) {
286 this.data.applications.available = _.uniq(apps.sort()).map(
287 (x: string) => new SelectOption(false, x, '')
288 );
289 }
290
291 private listenToChanges() {
292 this.listenToChangesDuringAddEdit();
293 if (!this.editing) {
294 this.listenToChangesDuringAdd();
295 }
296 }
297
298 private listenToChangesDuringAddEdit() {
299 this.form.get('pgNum').valueChanges.subscribe((pgs) => {
300 const change = pgs - this.data.pgs;
301 if (Math.abs(change) !== 1 || pgs === 2) {
302 this.data.pgs = pgs;
303 return;
304 }
305 this.doPgPowerJump(change as 1 | -1);
306 });
307 }
308
309 private doPgPowerJump(jump: 1 | -1) {
310 const power = this.calculatePgPower() + jump;
311 this.setPgs(jump === -1 ? Math.round(power) : Math.floor(power));
312 }
313
314 private calculatePgPower(pgs = this.form.getValue('pgNum')): number {
315 return Math.log(pgs) / Math.log(2);
316 }
317
318 private setPgs(power: number) {
319 const pgs = Math.pow(2, power < 0 ? 0 : power); // Set size the nearest accurate size.
320 this.data.pgs = pgs;
321 this.form.silentSet('pgNum', pgs);
322 }
323
324 private listenToChangesDuringAdd() {
325 this.form.get('poolType').valueChanges.subscribe((poolType) => {
326 this.poolTypeChange(poolType);
327 });
328 this.form.get('crushRule').valueChanges.subscribe((rule) => {
329 // The crush rule can only be changed if type 'replicated' is set.
330 if (this.crushDeletionBtn && this.crushDeletionBtn.isOpen) {
331 this.crushDeletionBtn.hide();
332 }
333 if (!rule) {
334 return;
335 }
336 this.crushRuleIsUsedBy(rule.rule_name);
337 this.replicatedRuleChange();
338 this.pgCalc();
339 });
340 this.form.get('size').valueChanges.subscribe(() => {
341 // The size can only be changed if type 'replicated' is set.
342 this.pgCalc();
343 });
344 this.form.get('erasureProfile').valueChanges.subscribe(() => {
345 // The ec profile can only be changed if type 'erasure' is set.
346 this.pgCalc();
347 });
348 this.form.get('mode').valueChanges.subscribe(() => {
349 ['minBlobSize', 'maxBlobSize', 'ratio'].forEach((name) => {
350 this.form.get(name).updateValueAndValidity({ emitEvent: false });
351 });
352 });
353 this.form.get('minBlobSize').valueChanges.subscribe(() => {
354 this.form.get('maxBlobSize').updateValueAndValidity({ emitEvent: false });
355 });
356 this.form.get('maxBlobSize').valueChanges.subscribe(() => {
357 this.form.get('minBlobSize').updateValueAndValidity({ emitEvent: false });
358 });
359 }
360
361 private poolTypeChange(poolType: string) {
362 if (poolType === 'replicated') {
363 this.setTypeBooleans(true, false);
364 } else if (poolType === 'erasure') {
365 this.setTypeBooleans(false, true);
366 } else {
367 this.setTypeBooleans(false, false);
368 }
369 if (!poolType || !this.info) {
370 this.current.rules = [];
371 return;
372 }
373 const rules = this.info['crush_rules_' + poolType] || [];
374 this.current.rules = rules;
375 if (this.editing) {
376 return;
377 }
378 if (this.isReplicated) {
379 this.setListControlStatus('crushRule', rules);
380 }
381 this.replicatedRuleChange();
382 this.pgCalc();
383 }
384
385 private setTypeBooleans(replicated: boolean, erasure: boolean) {
386 this.isReplicated = replicated;
387 this.isErasure = erasure;
388 }
389
390 private replicatedRuleChange() {
391 if (!this.isReplicated) {
392 return;
393 }
394 const control = this.form.get('size');
395 let size = this.form.getValue('size') || 3;
396 const min = this.getMinSize();
397 const max = this.getMaxSize();
398 if (size < min) {
399 size = min;
400 } else if (size > max) {
401 size = max;
402 }
403 if (size !== control.value) {
404 this.form.silentSet('size', size);
405 }
406 }
407
408 getMinSize(): number {
409 if (!this.info || this.info.osd_count < 1) {
410 return 0;
411 }
412 const rule = this.form.getValue('crushRule');
413 if (rule) {
414 return rule.min_size;
415 }
416 return 1;
417 }
418
419 getMaxSize(): number {
420 if (!this.info || this.info.osd_count < 1) {
421 return 0;
422 }
423 const osds: number = this.info.osd_count;
424 if (this.form.getValue('crushRule')) {
425 const max: number = this.form.get('crushRule').value.max_size;
426 if (max < osds) {
427 return max;
428 }
429 }
430 return osds;
431 }
432
433 private pgCalc() {
434 const poolType = this.form.getValue('poolType');
435 if (!this.info || this.form.get('pgNum').dirty || !poolType) {
436 return;
437 }
438 const pgMax = this.info.osd_count * 100;
439 const pgs = this.isReplicated ? this.replicatedPgCalc(pgMax) : this.erasurePgCalc(pgMax);
440 if (!pgs) {
441 return;
442 }
443 const oldValue = this.data.pgs;
444 this.alignPgs(pgs);
445 const newValue = this.data.pgs;
446 if (!this.externalPgChange) {
447 this.externalPgChange = oldValue !== newValue;
448 }
449 }
450
451 private replicatedPgCalc(pgs: number): number {
452 const sizeControl = this.form.get('size');
453 const size = sizeControl.value;
454 return sizeControl.valid && size > 0 ? pgs / size : 0;
455 }
456
457 private erasurePgCalc(pgs: number): number {
458 const ecpControl = this.form.get('erasureProfile');
459 const ecp = ecpControl.value;
460 return (ecpControl.valid || ecpControl.disabled) && ecp ? pgs / (ecp.k + ecp.m) : 0;
461 }
462
463 private alignPgs(pgs = this.form.getValue('pgNum')) {
464 this.setPgs(Math.round(this.calculatePgPower(pgs < 1 ? 1 : pgs)));
465 }
466
467 private setComplexValidators() {
468 if (this.editing) {
469 this.form
470 .get('name')
471 .setValidators([
472 this.form.get('name').validator,
473 CdValidators.custom(
474 'uniqueName',
475 (name: string) =>
476 this.data.pool &&
477 this.info &&
478 this.info.pool_names.indexOf(name) !== -1 &&
479 this.info.pool_names.indexOf(name) !==
480 this.info.pool_names.indexOf(this.data.pool.pool_name)
481 )
482 ]);
483 } else {
484 CdValidators.validateIf(this.form.get('size'), () => this.isReplicated, [
485 CdValidators.custom(
486 'min',
487 (value: number) => this.form.getValue('size') && value < this.getMinSize()
488 ),
489 CdValidators.custom(
490 'max',
491 (value: number) => this.form.getValue('size') && this.getMaxSize() < value
492 )
493 ]);
494 this.form
495 .get('name')
496 .setValidators([
497 this.form.get('name').validator,
498 CdValidators.custom(
499 'uniqueName',
500 (name: string) => this.info && this.info.pool_names.indexOf(name) !== -1
501 )
502 ]);
503 }
504 this.setCompressionValidators();
505 }
506
507 private setCompressionValidators() {
508 CdValidators.validateIf(this.form.get('minBlobSize'), () => this.hasCompressionEnabled(), [
509 Validators.min(0),
510 CdValidators.custom('maximum', (size: string) =>
511 this.oddBlobSize(size, this.form.getValue('maxBlobSize'))
512 )
513 ]);
514 CdValidators.validateIf(this.form.get('maxBlobSize'), () => this.hasCompressionEnabled(), [
515 Validators.min(0),
516 CdValidators.custom('minimum', (size: string) =>
517 this.oddBlobSize(this.form.getValue('minBlobSize'), size)
518 )
519 ]);
520 CdValidators.validateIf(this.form.get('ratio'), () => this.hasCompressionEnabled(), [
521 Validators.min(0),
522 Validators.max(1)
523 ]);
524 }
525
526 private oddBlobSize(minimum: string, maximum: string) {
527 const min = this.formatter.toBytes(minimum);
528 const max = this.formatter.toBytes(maximum);
529 return Boolean(min && max && min >= max);
530 }
531
532 hasCompressionEnabled() {
533 return this.form.getValue('mode') && this.form.get('mode').value.toLowerCase() !== 'none';
534 }
535
536 describeCrushStep(step: CrushStep) {
537 return [
538 step.op.replace('_', ' '),
539 step.item_name || '',
540 step.type ? step.num + ' type ' + step.type : ''
541 ].join(' ');
542 }
543
544 addErasureCodeProfile() {
545 this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs());
546 this.bsModalService.show(ErasureCodeProfileFormModalComponent);
547 }
548
549 private reloadECPs() {
550 this.ecpService.list().subscribe((profiles: ErasureCodeProfile[]) => this.initEcp(profiles));
551 this.modalSubscription.unsubscribe();
552 }
553
554 deleteErasureCodeProfile() {
555 const ecp = this.form.getValue('erasureProfile');
556 if (!ecp) {
557 return;
558 }
559 const name = ecp.name;
560 this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs());
561 this.modalService.show(CriticalConfirmationModalComponent, {
562 initialState: {
563 itemDescription: this.i18n('erasure code profile'),
564 itemNames: [name],
565 submitActionObservable: () =>
566 this.taskWrapper.wrapTaskAroundCall({
567 task: new FinishedTask('ecp/delete', { name: name }),
568 call: this.ecpService.delete(name)
569 })
570 }
571 });
572 }
573
574 addCrushRule() {
575 if (this.crushDeletionBtn.isOpen) {
576 this.crushDeletionBtn.hide();
577 }
578 const modalRef = this.bsModalService.show(CrushRuleFormModalComponent);
579 modalRef.content.submitAction.subscribe((rule: CrushRuleConfig) => {
580 this.reloadCrushRules(rule.name);
581 });
582 }
583
584 private reloadCrushRules(ruleName?: string) {
585 if (this.modalSubscription) {
586 this.modalSubscription.unsubscribe();
587 }
588 this.poolService.getInfo().subscribe((info: PoolFormInfo) => {
589 this.initInfo(info);
590 this.poolTypeChange('replicated');
591 if (!ruleName) {
592 return;
593 }
594 const newRule = this.info.crush_rules_replicated.find((rule) => rule.rule_name === ruleName);
595 if (newRule) {
596 this.form.get('crushRule').setValue(newRule);
597 }
598 });
599 }
600
601 deleteCrushRule() {
602 const rule = this.form.getValue('crushRule');
603 if (!rule) {
604 return;
605 }
606 if (this.crushUsage) {
607 this.crushDeletionBtn.toggle();
608 this.data.crushInfo = true;
609 setTimeout(() => {
610 if (this.crushInfoTabs) {
611 this.crushInfoTabs.tabs[2].active = true;
612 }
613 }, 50);
614 return;
615 }
616 const name = rule.rule_name;
617 this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadCrushRules());
618 this.modalService.show(CriticalConfirmationModalComponent, {
619 initialState: {
620 itemDescription: this.i18n('crush rule'),
621 itemNames: [name],
622 submitActionObservable: () =>
623 this.taskWrapper.wrapTaskAroundCall({
624 task: new FinishedTask('crushRule/delete', { name: name }),
625 call: this.crushRuleService.delete(name)
626 })
627 }
628 });
629 }
630
631 crushRuleIsUsedBy(ruleName: string) {
632 this.crushUsage = ruleName ? this.info.used_rules[ruleName] : undefined;
633 }
634
635 submit() {
636 if (this.form.invalid) {
637 this.form.setErrors({ cdSubmitButton: true });
638 return;
639 }
640
641 const pool = { pool: this.form.getValue('name') };
642
643 this.assignFormFields(pool, [
644 { externalFieldName: 'pool_type', formControlName: 'poolType' },
645 {
646 externalFieldName: 'pg_autoscale_mode',
647 formControlName: 'pgAutoscaleMode',
648 editable: true
649 },
650 {
651 externalFieldName: 'pg_num',
652 formControlName: 'pgNum',
653 replaceFn: (value: number) => (this.form.getValue('pgAutoscaleMode') === 'on' ? 1 : value),
654 editable: true
655 },
656 this.isReplicated
657 ? { externalFieldName: 'size', formControlName: 'size' }
658 : {
659 externalFieldName: 'erasure_code_profile',
660 formControlName: 'erasureProfile',
661 attr: 'name'
662 },
663 {
664 externalFieldName: 'rule_name',
665 formControlName: 'crushRule',
666 replaceFn: (value: CrushRule) => (this.isReplicated ? value && value.rule_name : undefined)
667 },
668 {
669 externalFieldName: 'quota_max_bytes',
670 formControlName: 'max_bytes',
671 replaceFn: this.formatter.toBytes,
672 editable: true,
673 resetValue: this.editing ? 0 : undefined
674 },
675 {
676 externalFieldName: 'quota_max_objects',
677 formControlName: 'max_objects',
678 editable: true,
679 resetValue: this.editing ? 0 : undefined
680 }
681 ]);
682
683 if (this.info.is_all_bluestore) {
684 this.assignFormField(pool, {
685 externalFieldName: 'flags',
686 formControlName: 'ecOverwrites',
687 replaceFn: () => (this.isErasure ? ['ec_overwrites'] : undefined)
688 });
689
690 if (this.form.getValue('mode') !== 'none') {
691 this.assignFormFields(pool, [
692 {
693 externalFieldName: 'compression_mode',
694 formControlName: 'mode',
695 editable: true,
696 replaceFn: (value: boolean) => this.hasCompressionEnabled() && value
697 },
698 {
699 externalFieldName: 'compression_algorithm',
700 formControlName: 'algorithm',
701 editable: true
702 },
703 {
704 externalFieldName: 'compression_min_blob_size',
705 formControlName: 'minBlobSize',
706 replaceFn: this.formatter.toBytes,
707 editable: true,
708 resetValue: 0
709 },
710 {
711 externalFieldName: 'compression_max_blob_size',
712 formControlName: 'maxBlobSize',
713 replaceFn: this.formatter.toBytes,
714 editable: true,
715 resetValue: 0
716 },
717 {
718 externalFieldName: 'compression_required_ratio',
719 formControlName: 'ratio',
720 editable: true,
721 resetValue: 0
722 }
723 ]);
724 } else if (this.editing) {
725 this.assignFormFields(pool, [
726 {
727 externalFieldName: 'compression_mode',
728 formControlName: 'mode',
729 editable: true,
730 replaceFn: () => 'unset' // Is used if no compression is set
731 },
732 {
733 externalFieldName: 'srcpool',
734 formControlName: 'name',
735 editable: true,
736 replaceFn: () => this.data.pool.pool_name
737 }
738 ]);
739 }
740 }
741
742 const apps = this.data.applications.selected;
743 if (apps.length > 0 || this.editing) {
744 pool['application_metadata'] = apps;
745 }
746
747 // Only collect configuration data for replicated pools, as QoS cannot be configured on EC
748 // pools. EC data pools inherit their settings from the corresponding replicated metadata pool.
749 if (this.isReplicated && !_.isEmpty(this.currentConfigurationValues)) {
750 pool['configuration'] = this.currentConfigurationValues;
751 }
752
753 this.triggerApiTask(pool);
754 }
755
756 /**
757 * Retrieves the values for the given form field descriptions and assigns the values to the given
758 * object. This method differentiates between `add` and `edit` mode and acts differently on one or
759 * the other.
760 */
761 private assignFormFields(pool: object, formFieldDescription: FormFieldDescription[]): void {
762 formFieldDescription.forEach((item) => this.assignFormField(pool, item));
763 }
764
765 /**
766 * Retrieves the value for the given form field description and assigns the values to the given
767 * object. This method differentiates between `add` and `edit` mode and acts differently on one or
768 * the other.
769 */
770 private assignFormField(
771 pool: object,
772 {
773 externalFieldName,
774 formControlName,
775 attr,
776 replaceFn,
777 editable,
778 resetValue
779 }: FormFieldDescription
780 ): void {
781 if (this.editing && (!editable || this.form.get(formControlName).pristine)) {
782 return;
783 }
784 const value = this.form.getValue(formControlName);
785 let apiValue = replaceFn ? replaceFn(value) : attr ? _.get(value, attr) : value;
786 if (!value || !apiValue) {
787 if (editable && !_.isUndefined(resetValue)) {
788 apiValue = resetValue;
789 } else {
790 return;
791 }
792 }
793 pool[externalFieldName] = apiValue;
794 }
795
796 private triggerApiTask(pool: Record<string, any>) {
797 this.taskWrapper
798 .wrapTaskAroundCall({
799 task: new FinishedTask('pool/' + (this.editing ? URLVerbs.EDIT : URLVerbs.CREATE), {
800 pool_name: pool.hasOwnProperty('srcpool') ? pool.srcpool : pool.pool
801 }),
802 call: this.poolService[this.editing ? URLVerbs.UPDATE : URLVerbs.CREATE](pool)
803 })
804 .subscribe(
805 undefined,
806 (resp) => {
807 if (_.isObject(resp.error) && resp.error.code === '34') {
808 this.form.get('pgNum').setErrors({ '34': true });
809 }
810 this.form.setErrors({ cdSubmitButton: true });
811 },
812 () => this.router.navigate(['/pool'])
813 );
814 }
815
816 appSelection() {
817 this.form.get('name').updateValueAndValidity({ emitEvent: false, onlySelf: true });
818 }
819 }