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