]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
import 15.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / pool-form / pool-form.component.ts
CommitLineData
e306af50 1import { Component, EventEmitter, OnInit, Type, ViewChild } from '@angular/core';
11fdf7f2
TL
2import { FormControl, Validators } from '@angular/forms';
3import { ActivatedRoute, Router } from '@angular/router';
4
5import { I18n } from '@ngx-translate/i18n-polyfill';
6import * as _ from 'lodash';
7import { BsModalService } from 'ngx-bootstrap/modal';
9f95a23c
TL
8import { TabsetComponent } from 'ngx-bootstrap/tabs';
9import { TooltipDirective } from 'ngx-bootstrap/tooltip';
e306af50 10import { Observable, Subscription } from 'rxjs';
11fdf7f2 11
9f95a23c 12import { CrushRuleService } from '../../../shared/api/crush-rule.service';
11fdf7f2
TL
13import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
14import { PoolService } from '../../../shared/api/pool.service';
15import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
9f95a23c 16import { SelectOption } from '../../../shared/components/select/select-option.model';
11fdf7f2 17import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
9f95a23c 18import { Icons } from '../../../shared/enum/icons.enum';
11fdf7f2
TL
19import { CdFormGroup } from '../../../shared/forms/cd-form-group';
20import { CdValidators } from '../../../shared/forms/cd-validators';
21import {
22 RbdConfigurationEntry,
23 RbdConfigurationSourceField
24} from '../../../shared/models/configuration';
e306af50 25import { CrushRule } from '../../../shared/models/crush-rule';
11fdf7f2
TL
26import { CrushStep } from '../../../shared/models/crush-step';
27import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
28import { FinishedTask } from '../../../shared/models/finished-task';
29import { Permission } from '../../../shared/models/permissions';
30import { PoolFormInfo } from '../../../shared/models/pool-form-info';
31import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
32import { AuthStorageService } from '../../../shared/services/auth-storage.service';
33import { FormatterService } from '../../../shared/services/formatter.service';
34import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
9f95a23c
TL
35import { CrushRuleFormModalComponent } from '../crush-rule-form-modal/crush-rule-form-modal.component';
36import { ErasureCodeProfileFormModalComponent } from '../erasure-code-profile-form/erasure-code-profile-form-modal.component';
11fdf7f2
TL
37import { Pool } from '../pool';
38import { PoolFormData } from './pool-form-data';
39
40interface 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})
54export class PoolFormComponent implements OnInit {
9f95a23c
TL
55 @ViewChild('crushInfoTabs', { static: false }) crushInfoTabs: TabsetComponent;
56 @ViewChild('crushDeletionBtn', { static: false }) crushDeletionBtn: TooltipDirective;
e306af50
TL
57 @ViewChild('ecpInfoTabs', { static: false }) ecpInfoTabs: TabsetComponent;
58 @ViewChild('ecpDeletionBtn', { static: false }) ecpDeletionBtn: TooltipDirective;
9f95a23c 59
11fdf7f2
TL
60 permission: Permission;
61 form: CdFormGroup;
62 ecProfiles: ErasureCodeProfile[];
63 info: PoolFormInfo;
64 routeParamsSubscribe: any;
65 editing = false;
9f95a23c
TL
66 isReplicated = false;
67 isErasure = false;
11fdf7f2
TL
68 data = new PoolFormData(this.i18n);
69 externalPgChange = false;
9f95a23c 70 current: Record<string, any> = {
11fdf7f2
TL
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;
9f95a23c
TL
80 icons = Icons;
81 pgAutoscaleModes: string[];
82 crushUsage: string[] = undefined; // Will only be set if a rule is used by some pool
e306af50 83 ecpUsage: string[] = undefined; // Will only be set if a rule is used by some pool
9f95a23c
TL
84
85 private modalSubscription: Subscription;
11fdf7f2
TL
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,
9f95a23c 98 private crushRuleService: CrushRuleService,
11fdf7f2
TL
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 ||
801d1391
TL
113 (!this.permission.update && this.editing) ||
114 (!this.permission.create && !this.editing)
11fdf7f2
TL
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('', {
9f95a23c
TL
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 ]
11fdf7f2
TL
150 }),
151 poolType: new FormControl('', {
152 validators: [Validators.required]
153 }),
154 crushRule: new FormControl(null, {
155 validators: [
156 CdValidators.custom(
157 'tooFewOsds',
9f95a23c
TL
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
11fdf7f2
TL
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 }),
9f95a23c 174 pgAutoscaleMode: new FormControl(null),
11fdf7f2 175 ecOverwrites: new FormControl(false),
9f95a23c
TL
176 compression: compressionForm,
177 max_bytes: new FormControl(''),
178 max_objects: new FormControl(0, {
179 validators: [Validators.min(0)]
11fdf7f2 180 })
9f95a23c
TL
181 },
182 [CdValidators.custom('form', (): null => null)]
11fdf7f2
TL
183 );
184 }
185
186 ngOnInit() {
9f95a23c
TL
187 this.poolService.getInfo().subscribe((info: PoolFormInfo) => {
188 this.initInfo(info);
189 if (this.editing) {
190 this.initEditMode();
191 } else {
192 this.setAvailableApps();
11fdf7f2 193 }
9f95a23c
TL
194 this.listenToChanges();
195 this.setComplexValidators();
196 });
11fdf7f2
TL
197 }
198
199 private initInfo(info: PoolFormInfo) {
9f95a23c
TL
200 this.pgAutoscaleModes = info.pg_autoscale_modes;
201 this.form.silentSet('pgAutoscaleMode', info.pg_autoscale_default_mode);
11fdf7f2 202 this.form.silentSet('algorithm', info.bluestore_compression_algorithm);
11fdf7f2 203 this.info = info;
9f95a23c 204 this.initEcp(info.erasure_code_profiles);
11fdf7f2
TL
205 }
206
207 private initEcp(ecProfiles: ErasureCodeProfile[]) {
9f95a23c
TL
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);
11fdf7f2 227 }
9f95a23c
TL
228 if (arr.length <= 1) {
229 if (control.enabled) {
230 control.disable();
231 }
232 } else if (control.disabled) {
11fdf7f2
TL
233 control.enable();
234 }
11fdf7f2
TL
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 });
9f95a23c
TL
258 this.poolTypeChange(pool.type);
259 const rules = this.info.crush_rules_replicated.concat(this.info.crush_rules_erasure);
11fdf7f2
TL
260 const dataMap = {
261 name: pool.pool_name,
262 poolType: pool.type,
9f95a23c 263 crushRule: rules.find((rule: CrushRule) => rule.rule_name === pool.crush_rule),
11fdf7f2
TL
264 size: pool.size,
265 erasureProfile: this.ecProfiles.find((ecp) => ecp.name === pool.erasure_code_profile),
9f95a23c 266 pgAutoscaleMode: pool.pg_autoscale_mode,
11fdf7f2
TL
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),
9f95a23c
TL
273 ratio: pool.options.compression_required_ratio,
274 max_bytes: this.dimlessBinaryPipe.transform(pool.quota_max_bytes),
275 max_objects: pool.quota_max_objects
11fdf7f2 276 };
11fdf7f2
TL
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 });
eafe8130 283 this.data.pgs = this.form.getValue('pgNum');
9f95a23c 284 this.setAvailableApps(this.data.applications.default.concat(pool.application_metadata));
11fdf7f2
TL
285 this.data.applications.selected = pool.application_metadata;
286 }
287
9f95a23c
TL
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
11fdf7f2
TL
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) => {
9f95a23c 329 this.poolTypeChange(poolType);
11fdf7f2 330 });
9f95a23c
TL
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;
11fdf7f2 338 }
9f95a23c
TL
339 this.crushRuleIsUsedBy(rule.rule_name);
340 this.replicatedRuleChange();
11fdf7f2
TL
341 this.pgCalc();
342 });
343 this.form.get('size').valueChanges.subscribe(() => {
9f95a23c 344 // The size can only be changed if type 'replicated' is set.
11fdf7f2
TL
345 this.pgCalc();
346 });
e306af50 347 this.form.get('erasureProfile').valueChanges.subscribe((profile) => {
9f95a23c 348 // The ec profile can only be changed if type 'erasure' is set.
e306af50
TL
349 if (this.ecpDeletionBtn && this.ecpDeletionBtn.isOpen) {
350 this.ecpDeletionBtn.hide();
351 }
352 if (!profile) {
353 return;
354 }
355 this.ecpIsUsedBy(profile.name);
11fdf7f2
TL
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
9f95a23c
TL
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 }
11fdf7f2
TL
379 if (!poolType || !this.info) {
380 this.current.rules = [];
381 return;
382 }
383 const rules = this.info['crush_rules_' + poolType] || [];
11fdf7f2 384 this.current.rules = rules;
9f95a23c
TL
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;
11fdf7f2
TL
398 }
399
400 private replicatedRuleChange() {
9f95a23c 401 if (!this.isReplicated) {
11fdf7f2
TL
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) {
9f95a23c 420 return 0;
11fdf7f2
TL
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) {
9f95a23c 431 return 0;
11fdf7f2
TL
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;
9f95a23c 449 const pgs = this.isReplicated ? this.replicatedPgCalc(pgMax) : this.erasurePgCalc(pgMax);
11fdf7f2
TL
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
9f95a23c 461 private replicatedPgCalc(pgs: number): number {
11fdf7f2
TL
462 const sizeControl = this.form.get('size');
463 const size = sizeControl.value;
9f95a23c 464 return sizeControl.valid && size > 0 ? pgs / size : 0;
11fdf7f2
TL
465 }
466
9f95a23c 467 private erasurePgCalc(pgs: number): number {
11fdf7f2
TL
468 const ecpControl = this.form.get('erasureProfile');
469 const ecp = ecpControl.value;
9f95a23c 470 return (ecpControl.valid || ecpControl.disabled) && ecp ? pgs / (ecp.k + ecp.m) : 0;
11fdf7f2
TL
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) {
11fdf7f2
TL
479 this.form
480 .get('name')
481 .setValidators([
482 this.form.get('name').validator,
483 CdValidators.custom(
484 'uniqueName',
9f95a23c 485 (name: string) =>
11fdf7f2
TL
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 {
9f95a23c
TL
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 ]);
11fdf7f2
TL
504 this.form
505 .get('name')
506 .setValidators([
507 this.form.get('name').validator,
508 CdValidators.custom(
509 'uniqueName',
9f95a23c 510 (name: string) => this.info && this.info.pool_names.indexOf(name) !== -1
11fdf7f2
TL
511 )
512 ]);
513 }
514 this.setCompressionValidators();
515 }
516
517 private setCompressionValidators() {
518 CdValidators.validateIf(this.form.get('minBlobSize'), () => this.hasCompressionEnabled(), [
519 Validators.min(0),
9f95a23c 520 CdValidators.custom('maximum', (size: string) =>
11fdf7f2
TL
521 this.oddBlobSize(size, this.form.getValue('maxBlobSize'))
522 )
523 ]);
524 CdValidators.validateIf(this.form.get('maxBlobSize'), () => this.hasCompressionEnabled(), [
525 Validators.min(0),
9f95a23c 526 CdValidators.custom('minimum', (size: string) =>
11fdf7f2
TL
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
9f95a23c
TL
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);
11fdf7f2
TL
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() {
e306af50 555 this.addModal(ErasureCodeProfileFormModalComponent, (name) => this.reloadECPs(name));
11fdf7f2
TL
556 }
557
e306af50
TL
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 });
11fdf7f2
TL
564 }
565
e306af50
TL
566 private hideOpenTooltips() {
567 const hideTooltip = (btn: TooltipDirective) => btn && btn.isOpen && btn.hide();
568 hideTooltip(this.ecpDeletionBtn);
569 hideTooltip(this.crushDeletionBtn);
11fdf7f2
TL
570 }
571
e306af50
TL
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'
9f95a23c
TL
579 });
580 }
581
e306af50
TL
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 }) {
9f95a23c
TL
595 if (this.modalSubscription) {
596 this.modalSubscription.unsubscribe();
597 }
e306af50
TL
598 getInfo().subscribe((items: any) => {
599 initInfo(items);
600 if (!newItemName) {
9f95a23c
TL
601 return;
602 }
e306af50
TL
603 const item = findNewItem();
604 if (item) {
605 this.form.get(controlName).setValue(item);
9f95a23c
TL
606 }
607 });
608 }
609
e306af50
TL
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) {
9f95a23c
TL
652 return;
653 }
e306af50
TL
654 if (usage) {
655 deletionBtn.toggle();
656 this.data[dataName] = true;
9f95a23c 657 setTimeout(() => {
e306af50
TL
658 const tabs = getTabs();
659 if (tabs) {
660 tabs.tabs[tabPosition].active = true;
9f95a23c
TL
661 }
662 }, 50);
663 return;
664 }
e306af50 665 const name = value[nameAttribute];
9f95a23c
TL
666 this.modalService.show(CriticalConfirmationModalComponent, {
667 initialState: {
e306af50 668 itemDescription,
9f95a23c 669 itemNames: [name],
e306af50
TL
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 }
9f95a23c
TL
678 }
679 });
680 }
681
e306af50
TL
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
9f95a23c
TL
716 crushRuleIsUsedBy(ruleName: string) {
717 this.crushUsage = ruleName ? this.info.used_rules[ruleName] : undefined;
718 }
719
e306af50
TL
720 ecpIsUsedBy(profileName: string) {
721 this.ecpUsage = profileName ? this.info.used_profiles[profileName] : undefined;
722 }
723
11fdf7f2
TL
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' },
9f95a23c
TL
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
11fdf7f2
TL
746 ? { externalFieldName: 'size', formControlName: 'size' }
747 : {
748 externalFieldName: 'erasure_code_profile',
749 formControlName: 'erasureProfile',
750 attr: 'name'
751 },
9f95a23c
TL
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 }
11fdf7f2
TL
770 ]);
771
772 if (this.info.is_all_bluestore) {
773 this.assignFormField(pool, {
774 externalFieldName: 'flags',
775 formControlName: 'ecOverwrites',
9f95a23c 776 replaceFn: () => (this.isErasure ? ['ec_overwrites'] : undefined)
11fdf7f2
TL
777 });
778
779 if (this.form.getValue('mode') !== 'none') {
780 this.assignFormFields(pool, [
781 {
782 externalFieldName: 'compression_mode',
783 formControlName: 'mode',
784 editable: true,
9f95a23c 785 replaceFn: (value: boolean) => this.hasCompressionEnabled() && value
11fdf7f2
TL
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,
92f5a8d4 819 replaceFn: () => 'unset' // Is used if no compression is set
11fdf7f2
TL
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.
9f95a23c 838 if (this.isReplicated && !_.isEmpty(this.currentConfigurationValues)) {
11fdf7f2
TL
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
9f95a23c 885 private triggerApiTask(pool: Record<string, any>) {
11fdf7f2
TL
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() {
9f95a23c 906 this.form.get('name').updateValueAndValidity({ emitEvent: false, onlySelf: true });
11fdf7f2
TL
907 }
908}