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