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