]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
ba22359761fa6791cb393818cc26f00f8d261a82
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-form / rbd-form.component.ts
1 import { Component, EventEmitter, OnInit } from '@angular/core';
2 import { FormControl, ValidatorFn, 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 { Observable } from 'rxjs';
8
9 import { PoolService } from '../../../shared/api/pool.service';
10 import { RbdService } from '../../../shared/api/rbd.service';
11 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
12 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
13 import {
14 RbdConfigurationEntry,
15 RbdConfigurationSourceField
16 } from '../../../shared/models/configuration';
17 import { FinishedTask } from '../../../shared/models/finished-task';
18 import { Permission } from '../../../shared/models/permissions';
19 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
20 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
21 import { FormatterService } from '../../../shared/services/formatter.service';
22 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
23 import { RbdImageFeature } from './rbd-feature.interface';
24 import { RbdFormCloneRequestModel } from './rbd-form-clone-request.model';
25 import { RbdFormCopyRequestModel } from './rbd-form-copy-request.model';
26 import { RbdFormCreateRequestModel } from './rbd-form-create-request.model';
27 import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
28 import { RbdFormMode } from './rbd-form-mode.enum';
29 import { RbdFormResponseModel } from './rbd-form-response.model';
30
31 @Component({
32 selector: 'cd-rbd-form',
33 templateUrl: './rbd-form.component.html',
34 styleUrls: ['./rbd-form.component.scss']
35 })
36 export class RbdFormComponent implements OnInit {
37 poolPermission: Permission;
38 rbdForm: CdFormGroup;
39 getDirtyConfigurationValues: (
40 includeLocalField?: boolean,
41 localField?: RbdConfigurationSourceField
42 ) => RbdConfigurationEntry[];
43
44 pools: Array<string> = null;
45 allPools: Array<string> = null;
46 dataPools: Array<string> = null;
47 allDataPools: Array<string> = null;
48 features: { [key: string]: RbdImageFeature };
49 featuresList: RbdImageFeature[] = [];
50 initializeConfigData = new EventEmitter<{
51 initialData: RbdConfigurationEntry[];
52 sourceType: RbdConfigurationSourceField;
53 }>();
54
55 pool: string;
56
57 advancedEnabled = false;
58
59 public rbdFormMode = RbdFormMode;
60 mode: RbdFormMode;
61
62 response: RbdFormResponseModel;
63 snapName: string;
64
65 defaultObjectSize = '4 MiB';
66
67 objectSizes: Array<string> = [
68 '4 KiB',
69 '8 KiB',
70 '16 KiB',
71 '32 KiB',
72 '64 KiB',
73 '128 KiB',
74 '256 KiB',
75 '512 KiB',
76 '1 MiB',
77 '2 MiB',
78 '4 MiB',
79 '8 MiB',
80 '16 MiB',
81 '32 MiB'
82 ];
83 action: string;
84 resource: string;
85
86 constructor(
87 private authStorageService: AuthStorageService,
88 private route: ActivatedRoute,
89 private poolService: PoolService,
90 private rbdService: RbdService,
91 private formatter: FormatterService,
92 private taskWrapper: TaskWrapperService,
93 private dimlessBinaryPipe: DimlessBinaryPipe,
94 private i18n: I18n,
95 public actionLabels: ActionLabelsI18n,
96 public router: Router
97 ) {
98 this.poolPermission = this.authStorageService.getPermissions().pool;
99 this.resource = this.i18n('RBD');
100 this.features = {
101 'deep-flatten': {
102 desc: this.i18n('Deep flatten'),
103 requires: null,
104 allowEnable: false,
105 allowDisable: true
106 },
107 layering: {
108 desc: this.i18n('Layering'),
109 requires: null,
110 allowEnable: false,
111 allowDisable: false
112 },
113 'exclusive-lock': {
114 desc: this.i18n('Exclusive lock'),
115 requires: null,
116 allowEnable: true,
117 allowDisable: true
118 },
119 'object-map': {
120 desc: this.i18n('Object map (requires exclusive-lock)'),
121 requires: 'exclusive-lock',
122 allowEnable: true,
123 allowDisable: true,
124 initDisabled: true
125 },
126 journaling: {
127 desc: this.i18n('Journaling (requires exclusive-lock)'),
128 requires: 'exclusive-lock',
129 allowEnable: true,
130 allowDisable: true,
131 initDisabled: true
132 },
133 'fast-diff': {
134 desc: this.i18n('Fast diff (interlocked with object-map)'),
135 requires: 'object-map',
136 allowEnable: true,
137 allowDisable: true,
138 interlockedWith: 'object-map',
139 initDisabled: true
140 }
141 };
142 this.featuresList = this.objToArray(this.features);
143 this.createForm();
144 }
145
146 objToArray(obj: { [key: string]: any }) {
147 return _.map(obj, (o, key) => Object.assign(o, { key: key }));
148 }
149
150 createForm() {
151 this.rbdForm = new CdFormGroup(
152 {
153 parent: new FormControl(''),
154 name: new FormControl('', {
155 validators: [Validators.required, Validators.pattern(/^[^@/]+?$/)]
156 }),
157 pool: new FormControl(null, {
158 validators: [Validators.required]
159 }),
160 useDataPool: new FormControl(false),
161 dataPool: new FormControl(null),
162 size: new FormControl(null, {
163 updateOn: 'blur'
164 }),
165 obj_size: new FormControl(this.defaultObjectSize),
166 features: new CdFormGroup(
167 this.featuresList.reduce((acc, e) => {
168 acc[e.key] = new FormControl({ value: false, disabled: !!e.initDisabled });
169 return acc;
170 }, {})
171 ),
172 stripingUnit: new FormControl(null),
173 stripingCount: new FormControl(null, {
174 updateOn: 'blur'
175 })
176 },
177 this.validateRbdForm(this.formatter)
178 );
179 }
180
181 disableForEdit() {
182 this.rbdForm.get('parent').disable();
183 this.rbdForm.get('pool').disable();
184 this.rbdForm.get('useDataPool').disable();
185 this.rbdForm.get('dataPool').disable();
186 this.rbdForm.get('obj_size').disable();
187 this.rbdForm.get('stripingUnit').disable();
188 this.rbdForm.get('stripingCount').disable();
189 }
190
191 disableForClone() {
192 this.rbdForm.get('parent').disable();
193 this.rbdForm.get('size').disable();
194 }
195
196 disableForCopy() {
197 this.rbdForm.get('parent').disable();
198 this.rbdForm.get('size').disable();
199 }
200
201 ngOnInit() {
202 if (this.router.url.startsWith('/block/rbd/edit')) {
203 this.mode = this.rbdFormMode.editing;
204 this.action = this.actionLabels.EDIT;
205 this.disableForEdit();
206 } else if (this.router.url.startsWith('/block/rbd/clone')) {
207 this.mode = this.rbdFormMode.cloning;
208 this.disableForClone();
209 this.action = this.actionLabels.CLONE;
210 } else if (this.router.url.startsWith('/block/rbd/copy')) {
211 this.mode = this.rbdFormMode.copying;
212 this.action = this.actionLabels.COPY;
213 this.disableForCopy();
214 } else {
215 this.action = this.actionLabels.CREATE;
216 }
217 if (
218 this.mode === this.rbdFormMode.editing ||
219 this.mode === this.rbdFormMode.cloning ||
220 this.mode === this.rbdFormMode.copying
221 ) {
222 this.route.params.subscribe((params: { pool: string; name: string; snap: string }) => {
223 const poolName = decodeURIComponent(params.pool);
224 const rbdName = decodeURIComponent(params.name);
225 if (params.snap) {
226 this.snapName = decodeURIComponent(params.snap);
227 }
228 this.rbdService.get(poolName, rbdName).subscribe((resp: RbdFormResponseModel) => {
229 this.setResponse(resp, this.snapName);
230 });
231 });
232 } else {
233 // New image
234 this.rbdService.defaultFeatures().subscribe((defaultFeatures: Array<string>) => {
235 this.setFeatures(defaultFeatures);
236 });
237 }
238 if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) {
239 this.poolService
240 .list(['pool_name', 'type', 'flags_names', 'application_metadata'])
241 .then((resp) => {
242 const pools = [];
243 const dataPools = [];
244 for (const pool of resp) {
245 if (_.indexOf(pool.application_metadata, 'rbd') !== -1) {
246 if (!pool.pool_name.includes('/')) {
247 if (pool.type === 'replicated') {
248 pools.push(pool);
249 dataPools.push(pool);
250 } else if (
251 pool.type === 'erasure' &&
252 pool.flags_names.indexOf('ec_overwrites') !== -1
253 ) {
254 dataPools.push(pool);
255 }
256 }
257 }
258 }
259 this.pools = pools;
260 this.allPools = pools;
261 this.dataPools = dataPools;
262 this.allDataPools = dataPools;
263 if (this.pools.length === 1) {
264 const poolName = this.pools[0]['pool_name'];
265 this.rbdForm.get('pool').setValue(poolName);
266 this.onPoolChange(poolName);
267 }
268 });
269 }
270 _.each(this.features, (feature) => {
271 this.rbdForm
272 .get('features')
273 .get(feature.key)
274 .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
275 });
276 }
277
278 onPoolChange(selectedPoolName) {
279 const newDataPools = this.allDataPools.filter((dataPool: any) => {
280 return dataPool.pool_name !== selectedPoolName;
281 });
282 if (this.rbdForm.getValue('dataPool') === selectedPoolName) {
283 this.rbdForm.get('dataPool').setValue(null);
284 }
285 this.dataPools = newDataPools;
286 }
287
288 onUseDataPoolChange() {
289 if (!this.rbdForm.getValue('useDataPool')) {
290 this.rbdForm.get('dataPool').setValue(null);
291 this.onDataPoolChange(null);
292 }
293 }
294
295 onDataPoolChange(selectedDataPoolName) {
296 const newPools = this.allPools.filter((pool: any) => {
297 return pool.pool_name !== selectedDataPoolName;
298 });
299 if (this.rbdForm.getValue('pool') === selectedDataPoolName) {
300 this.rbdForm.get('pool').setValue(null);
301 }
302 this.pools = newPools;
303 }
304
305 validateRbdForm(formatter: FormatterService): ValidatorFn {
306 return (formGroup: CdFormGroup) => {
307 // Data Pool
308 const useDataPoolControl = formGroup.get('useDataPool');
309 const dataPoolControl = formGroup.get('dataPool');
310 let dataPoolControlErrors = null;
311 if (useDataPoolControl.value && dataPoolControl.value == null) {
312 dataPoolControlErrors = { required: true };
313 }
314 dataPoolControl.setErrors(dataPoolControlErrors);
315 // Size
316 const sizeControl = formGroup.get('size');
317 const objectSizeControl = formGroup.get('obj_size');
318 const objectSizeInBytes = formatter.toBytes(
319 objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize
320 );
321 const stripingCountControl = formGroup.get('stripingCount');
322 const stripingCount = stripingCountControl.value != null ? stripingCountControl.value : 1;
323 let sizeControlErrors = null;
324 if (sizeControl.value === null) {
325 sizeControlErrors = { required: true };
326 } else {
327 const sizeInBytes = formatter.toBytes(sizeControl.value);
328 if (stripingCount * objectSizeInBytes > sizeInBytes) {
329 sizeControlErrors = { invalidSizeObject: true };
330 }
331 }
332 sizeControl.setErrors(sizeControlErrors);
333 // Striping Unit
334 const stripingUnitControl = formGroup.get('stripingUnit');
335 let stripingUnitControlErrors = null;
336 if (stripingUnitControl.value === null && stripingCountControl.value !== null) {
337 stripingUnitControlErrors = { required: true };
338 } else if (stripingUnitControl.value !== null) {
339 const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value);
340 if (stripingUnitInBytes > objectSizeInBytes) {
341 stripingUnitControlErrors = { invalidStripingUnit: true };
342 }
343 }
344 stripingUnitControl.setErrors(stripingUnitControlErrors);
345 // Striping Count
346 let stripingCountControlErrors = null;
347 if (stripingCountControl.value === null && stripingUnitControl.value !== null) {
348 stripingCountControlErrors = { required: true };
349 } else if (stripingCount < 1) {
350 stripingCountControlErrors = { min: true };
351 }
352 stripingCountControl.setErrors(stripingCountControlErrors);
353 return null;
354 };
355 }
356
357 protected getDependendChildFeatures(featureKey: string) {
358 return _.filter(this.features, (f) => f.requires === featureKey) || [];
359 }
360
361 deepBoxCheck(key, checked) {
362 const childFeatures = this.getDependendChildFeatures(key);
363 childFeatures.forEach((feature) => {
364 const featureControl = this.rbdForm.get(feature.key);
365 if (checked) {
366 featureControl.enable({ emitEvent: false });
367 } else {
368 featureControl.disable({ emitEvent: false });
369 featureControl.setValue(false, { emitEvent: false });
370 this.deepBoxCheck(feature.key, checked);
371 }
372
373 const featureFormGroup = this.rbdForm.get('features');
374 if (this.mode === this.rbdFormMode.editing && featureFormGroup.get(feature.key).enabled) {
375 if (this.response.features_name.indexOf(feature.key) !== -1 && !feature.allowDisable) {
376 featureFormGroup.get(feature.key).disable();
377 } else if (
378 this.response.features_name.indexOf(feature.key) === -1 &&
379 !feature.allowEnable
380 ) {
381 featureFormGroup.get(feature.key).disable();
382 }
383 }
384 });
385 }
386
387 interlockCheck(key, checked) {
388 // Adds a compatibility layer for Ceph cluster where the feature interlock of features hasn't
389 // been implemented yet. It disables the feature interlock for images which only have one of
390 // both interlocked features (at the time of this writing: object-map and fast-diff) enabled.
391 const feature = this.featuresList.find((f) => f.key === key);
392 if (this.response) {
393 // Ignore `create` page
394 const hasInterlockedFeature = feature.interlockedWith != null;
395 const dependentInterlockedFeature = this.featuresList.find(
396 (f) => f.interlockedWith === feature.key
397 );
398 const isOriginFeatureEnabled = !!this.response.features_name.find((e) => e === feature.key); // in this case: fast-diff
399 if (hasInterlockedFeature) {
400 const isLinkedEnabled = !!this.response.features_name.find(
401 (e) => e === feature.interlockedWith
402 ); // depends: object-map
403 if (isOriginFeatureEnabled !== isLinkedEnabled) {
404 return; // Ignore incompatible setting because it's from a previous cluster version
405 }
406 } else if (dependentInterlockedFeature) {
407 const isOtherInterlockedFeatureEnabled = !!this.response.features_name.find(
408 (e) => e === dependentInterlockedFeature.key
409 );
410 if (isOtherInterlockedFeatureEnabled !== isOriginFeatureEnabled) {
411 return; // Ignore incompatible setting because it's from a previous cluster version
412 }
413 }
414 }
415
416 if (checked) {
417 _.filter(this.features, (f) => f.interlockedWith === key).forEach((f) =>
418 this.rbdForm.get(f.key).setValue(true, { emitEvent: false })
419 );
420 } else {
421 if (feature.interlockedWith) {
422 // Don't skip emitting the event here, as it prevents `fast-diff` from
423 // becoming disabled when manually unchecked. This is because it
424 // triggers an update on `object-map` and if `object-map` doesn't emit,
425 // `fast-diff` will not be automatically disabled.
426 this.rbdForm
427 .get('features')
428 .get(feature.interlockedWith)
429 .setValue(false);
430 }
431 }
432 }
433
434 featureFormUpdate(key, checked) {
435 if (checked) {
436 const required = this.features[key].requires;
437 if (required && !this.rbdForm.getValue(required)) {
438 this.rbdForm.get(`features.${key}`).setValue(false);
439 return;
440 }
441 }
442 this.deepBoxCheck(key, checked);
443 this.interlockCheck(key, checked);
444 }
445
446 setFeatures(features: Array<string>) {
447 const featuresControl = this.rbdForm.get('features');
448 _.forIn(this.features, (feature) => {
449 if (features.indexOf(feature.key) !== -1) {
450 featuresControl.get(feature.key).setValue(true);
451 }
452 this.featureFormUpdate(feature.key, featuresControl.get(feature.key).value);
453 });
454 }
455
456 setResponse(response: RbdFormResponseModel, snapName: string) {
457 this.response = response;
458 if (this.mode === this.rbdFormMode.cloning) {
459 this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}@${snapName}`);
460 } else if (this.mode === this.rbdFormMode.copying) {
461 if (snapName) {
462 this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}@${snapName}`);
463 } else {
464 this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}`);
465 }
466 } else if (response.parent) {
467 const parent = response.parent;
468 this.rbdForm
469 .get('parent')
470 .setValue(`${parent.pool_name}/${parent.image_name}@${parent.snap_name}`);
471 }
472 if (this.mode === this.rbdFormMode.editing) {
473 this.rbdForm.get('name').setValue(response.name);
474 }
475 this.rbdForm.get('pool').setValue(response.pool_name);
476 if (response.data_pool) {
477 this.rbdForm.get('useDataPool').setValue(true);
478 this.rbdForm.get('dataPool').setValue(response.data_pool);
479 }
480 this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size));
481 this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size));
482 this.setFeatures(response.features_name);
483 this.rbdForm
484 .get('stripingUnit')
485 .setValue(this.dimlessBinaryPipe.transform(response.stripe_unit));
486 this.rbdForm.get('stripingCount').setValue(response.stripe_count);
487
488 /* Configuration */
489 this.initializeConfigData.emit({
490 initialData: this.response.configuration,
491 sourceType: RbdConfigurationSourceField.image
492 });
493 }
494
495 createRequest() {
496 const request = new RbdFormCreateRequestModel();
497 request.pool_name = this.rbdForm.getValue('pool');
498 request.name = this.rbdForm.getValue('name');
499 request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
500 request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size'));
501 _.forIn(this.features, (feature) => {
502 if (this.rbdForm.getValue(feature.key)) {
503 request.features.push(feature.key);
504 }
505 });
506
507 /* Striping */
508 request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
509 request.stripe_count = this.rbdForm.getValue('stripingCount');
510 request.data_pool = this.rbdForm.getValue('dataPool');
511
512 /* Configuration */
513 request.configuration = this.getDirtyConfigurationValues();
514
515 return request;
516 }
517
518 createAction(): Observable<any> {
519 const request = this.createRequest();
520 return this.taskWrapper.wrapTaskAroundCall({
521 task: new FinishedTask('rbd/create', {
522 pool_name: request.pool_name,
523 image_name: request.name
524 }),
525 call: this.rbdService.create(request)
526 });
527 }
528
529 editRequest() {
530 const request = new RbdFormEditRequestModel();
531 request.name = this.rbdForm.getValue('name');
532 request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
533 _.forIn(this.features, (feature) => {
534 if (this.rbdForm.getValue(feature.key)) {
535 request.features.push(feature.key);
536 }
537 });
538
539 request.configuration = this.getDirtyConfigurationValues();
540
541 return request;
542 }
543
544 cloneRequest(): RbdFormCloneRequestModel {
545 const request = new RbdFormCloneRequestModel();
546 request.child_pool_name = this.rbdForm.getValue('pool');
547 request.child_image_name = this.rbdForm.getValue('name');
548 request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size'));
549 _.forIn(this.features, (feature) => {
550 if (this.rbdForm.getValue(feature.key)) {
551 request.features.push(feature.key);
552 }
553 });
554
555 /* Striping */
556 request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
557 request.stripe_count = this.rbdForm.getValue('stripingCount');
558 request.data_pool = this.rbdForm.getValue('dataPool');
559
560 /* Configuration */
561 request.configuration = this.getDirtyConfigurationValues(
562 true,
563 RbdConfigurationSourceField.image
564 );
565
566 return request;
567 }
568
569 editAction(): Observable<any> {
570 return this.taskWrapper.wrapTaskAroundCall({
571 task: new FinishedTask('rbd/edit', {
572 pool_name: this.response.pool_name,
573 image_name: this.response.name
574 }),
575 call: this.rbdService.update(this.response.pool_name, this.response.name, this.editRequest())
576 });
577 }
578
579 cloneAction(): Observable<any> {
580 const request = this.cloneRequest();
581 return this.taskWrapper.wrapTaskAroundCall({
582 task: new FinishedTask('rbd/clone', {
583 parent_pool_name: this.response.pool_name,
584 parent_image_name: this.response.name,
585 parent_snap_name: this.snapName,
586 child_pool_name: request.child_pool_name,
587 child_image_name: request.child_image_name
588 }),
589 call: this.rbdService.cloneSnapshot(
590 this.response.pool_name,
591 this.response.name,
592 this.snapName,
593 request
594 )
595 });
596 }
597
598 copyRequest(): RbdFormCopyRequestModel {
599 const request = new RbdFormCopyRequestModel();
600 if (this.snapName) {
601 request.snapshot_name = this.snapName;
602 }
603 request.dest_pool_name = this.rbdForm.getValue('pool');
604 request.dest_image_name = this.rbdForm.getValue('name');
605 request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size'));
606 _.forIn(this.features, (feature) => {
607 if (this.rbdForm.getValue(feature.key)) {
608 request.features.push(feature.key);
609 }
610 });
611
612 /* Striping */
613 request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
614 request.stripe_count = this.rbdForm.getValue('stripingCount');
615 request.data_pool = this.rbdForm.getValue('dataPool');
616
617 /* Configuration */
618 request.configuration = this.getDirtyConfigurationValues(
619 true,
620 RbdConfigurationSourceField.image
621 );
622
623 return request;
624 }
625
626 copyAction(): Observable<any> {
627 const request = this.copyRequest();
628
629 return this.taskWrapper.wrapTaskAroundCall({
630 task: new FinishedTask('rbd/copy', {
631 src_pool_name: this.response.pool_name,
632 src_image_name: this.response.name,
633 dest_pool_name: request.dest_pool_name,
634 dest_image_name: request.dest_image_name
635 }),
636 call: this.rbdService.copy(this.response.pool_name, this.response.name, request)
637 });
638 }
639
640 submit() {
641 let action: Observable<any>;
642
643 if (this.mode === this.rbdFormMode.editing) {
644 action = this.editAction();
645 } else if (this.mode === this.rbdFormMode.cloning) {
646 action = this.cloneAction();
647 } else if (this.mode === this.rbdFormMode.copying) {
648 action = this.copyAction();
649 } else {
650 action = this.createAction();
651 }
652
653 action.subscribe(
654 undefined,
655 () => this.rbdForm.setErrors({ cdSubmitButton: true }),
656 () => this.router.navigate(['/block/rbd'])
657 );
658 }
659 }