1 import { Component, OnInit } from '@angular/core';
2 import { FormControl, ValidatorFn, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import _ from 'lodash';
6 import { forkJoin, Observable, ReplaySubject } from 'rxjs';
7 import { first, switchMap } from 'rxjs/operators';
9 import { Pool } from '~/app/ceph/pool/pool';
10 import { PoolService } from '~/app/shared/api/pool.service';
11 import { RbdService } from '~/app/shared/api/rbd.service';
12 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
13 import { Icons } from '~/app/shared/enum/icons.enum';
14 import { CdForm } from '~/app/shared/forms/cd-form';
15 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
17 RbdConfigurationEntry,
18 RbdConfigurationSourceField
19 } from '~/app/shared/models/configuration';
20 import { FinishedTask } from '~/app/shared/models/finished-task';
21 import { ImageSpec } from '~/app/shared/models/image-spec';
22 import { Permission } from '~/app/shared/models/permissions';
23 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
24 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
25 import { FormatterService } from '~/app/shared/services/formatter.service';
26 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
27 import { RBDImageFormat, RbdModel } from '../rbd-list/rbd-model';
28 import { RbdImageFeature } from './rbd-feature.interface';
29 import { RbdFormCloneRequestModel } from './rbd-form-clone-request.model';
30 import { RbdFormCopyRequestModel } from './rbd-form-copy-request.model';
31 import { RbdFormCreateRequestModel } from './rbd-form-create-request.model';
32 import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
33 import { RbdFormMode } from './rbd-form-mode.enum';
34 import { RbdFormResponseModel } from './rbd-form-response.model';
37 rbd: RbdFormResponseModel;
38 defaultFeatures: string[];
43 selector: 'cd-rbd-form',
44 templateUrl: './rbd-form.component.html',
45 styleUrls: ['./rbd-form.component.scss']
47 export class RbdFormComponent extends CdForm implements OnInit {
48 poolPermission: Permission;
50 getDirtyConfigurationValues: (
51 includeLocalField?: boolean,
52 localField?: RbdConfigurationSourceField
53 ) => RbdConfigurationEntry[];
55 namespaces: Array<string> = [];
56 namespacesByPoolCache = {};
57 pools: Array<Pool> = null;
58 allPools: Array<Pool> = null;
59 dataPools: Array<Pool> = null;
60 allDataPools: Array<Pool> = [];
61 features: { [key: string]: RbdImageFeature };
62 featuresList: RbdImageFeature[] = [];
63 initializeConfigData = new ReplaySubject<{
64 initialData: RbdConfigurationEntry[];
65 sourceType: RbdConfigurationSourceField;
70 advancedEnabled = false;
72 public rbdFormMode = RbdFormMode;
75 response: RbdFormResponseModel;
78 defaultObjectSize = '4 MiB';
80 objectSizes: Array<string> = [
98 private rbdImage = new ReplaySubject(1);
99 private routerUrl: string;
104 private authStorageService: AuthStorageService,
105 private route: ActivatedRoute,
106 private poolService: PoolService,
107 private rbdService: RbdService,
108 private formatter: FormatterService,
109 private taskWrapper: TaskWrapperService,
110 private dimlessBinaryPipe: DimlessBinaryPipe,
111 public actionLabels: ActionLabelsI18n,
112 private router: Router
115 this.routerUrl = this.router.url;
116 this.poolPermission = this.authStorageService.getPermissions().pool;
117 this.resource = $localize`RBD`;
120 desc: $localize`Deep flatten`,
126 desc: $localize`Layering`,
132 desc: $localize`Exclusive lock`,
138 desc: $localize`Object map (requires exclusive-lock)`,
139 requires: 'exclusive-lock',
145 desc: $localize`Journaling (requires exclusive-lock)`,
146 requires: 'exclusive-lock',
152 desc: $localize`Fast diff (interlocked with object-map)`,
153 requires: 'object-map',
156 interlockedWith: 'object-map',
160 this.featuresList = this.objToArray(this.features);
164 objToArray(obj: { [key: string]: any }) {
165 return _.map(obj, (o, key) => Object.assign(o, { key: key }));
169 this.rbdForm = new CdFormGroup(
171 parent: new FormControl(''),
172 name: new FormControl('', {
173 validators: [Validators.required, Validators.pattern(/^[^@/]+?$/)]
175 pool: new FormControl(null, {
176 validators: [Validators.required]
178 namespace: new FormControl(null),
179 useDataPool: new FormControl(false),
180 dataPool: new FormControl(null),
181 size: new FormControl(null, {
184 obj_size: new FormControl(this.defaultObjectSize),
185 features: new CdFormGroup(
186 this.featuresList.reduce((acc: object, e) => {
187 acc[e.key] = new FormControl({ value: false, disabled: !!e.initDisabled });
191 stripingUnit: new FormControl(null),
192 stripingCount: new FormControl(null, {
196 this.validateRbdForm(this.formatter)
201 this.rbdForm.get('parent').disable();
202 this.rbdForm.get('pool').disable();
203 this.rbdForm.get('namespace').disable();
204 this.rbdForm.get('useDataPool').disable();
205 this.rbdForm.get('dataPool').disable();
206 this.rbdForm.get('obj_size').disable();
207 this.rbdForm.get('stripingUnit').disable();
208 this.rbdForm.get('stripingCount').disable();
210 /* RBD Image Format v1 */
211 this.rbdImage.subscribe((image: RbdModel) => {
212 if (image.image_format === RBDImageFormat.V1) {
213 this.rbdForm.get('deep-flatten').disable();
214 this.rbdForm.get('layering').disable();
215 this.rbdForm.get('exclusive-lock').disable();
221 this.rbdForm.get('parent').disable();
222 this.rbdForm.get('size').disable();
226 this.rbdForm.get('parent').disable();
227 this.rbdForm.get('size').disable();
231 this.prepareFormForAction();
232 this.gatherNeededData().subscribe(this.handleExternalData.bind(this));
235 private prepareFormForAction() {
236 const url = this.routerUrl;
237 if (url.startsWith('/block/rbd/edit')) {
238 this.mode = this.rbdFormMode.editing;
239 this.action = this.actionLabels.EDIT;
240 this.disableForEdit();
241 } else if (url.startsWith('/block/rbd/clone')) {
242 this.mode = this.rbdFormMode.cloning;
243 this.disableForClone();
244 this.action = this.actionLabels.CLONE;
245 } else if (url.startsWith('/block/rbd/copy')) {
246 this.mode = this.rbdFormMode.copying;
247 this.action = this.actionLabels.COPY;
248 this.disableForCopy();
250 this.action = this.actionLabels.CREATE;
252 _.each(this.features, (feature) => {
256 .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
260 private gatherNeededData(): Observable<object> {
263 // Mode is not set for creation
264 this.route.params.subscribe((params: { image_spec: string; snap: string }) => {
265 const imageSpec = ImageSpec.fromString(decodeURIComponent(params.image_spec));
267 this.snapName = decodeURIComponent(params.snap);
269 promises['rbd'] = this.rbdService.get(imageSpec);
273 promises['defaultFeatures'] = this.rbdService.defaultFeatures();
275 if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) {
276 promises['pools'] = this.poolService.list([
280 'application_metadata'
283 return forkJoin(promises);
286 private handleExternalData(data: ExternalData) {
287 this.handlePoolData(data.pools);
289 if (data.defaultFeatures) {
290 // Fetched only during creation
291 this.setFeatures(data.defaultFeatures);
295 // Not fetched for creation
296 const resp = data.rbd;
297 this.setResponse(resp, this.snapName);
298 this.rbdImage.next(resp);
304 private handlePoolData(data: Pool[]) {
306 // Not fetched while editing
309 const pools: Pool[] = [];
310 const dataPools = [];
311 for (const pool of data) {
312 if (this.rbdService.isRBDPool(pool)) {
313 if (pool.type === 'replicated') {
315 dataPools.push(pool);
316 } else if (pool.type === 'erasure' && pool.flags_names.indexOf('ec_overwrites') !== -1) {
317 dataPools.push(pool);
322 this.allPools = pools;
323 this.dataPools = dataPools;
324 this.allDataPools = dataPools;
325 if (this.pools.length === 1) {
326 const poolName = this.pools[0].pool_name;
327 this.rbdForm.get('pool').setValue(poolName);
328 this.onPoolChange(poolName);
330 if (this.allDataPools.length <= 1) {
331 this.rbdForm.get('useDataPool').disable();
335 onPoolChange(selectedPoolName: string) {
336 const dataPoolControl = this.rbdForm.get('dataPool');
337 if (dataPoolControl.value === selectedPoolName) {
338 dataPoolControl.setValue(null);
340 this.dataPools = this.allDataPools
341 ? this.allDataPools.filter((dataPool: any) => {
342 return dataPool.pool_name !== selectedPoolName;
345 this.namespaces = null;
346 if (selectedPoolName in this.namespacesByPoolCache) {
347 this.namespaces = this.namespacesByPoolCache[selectedPoolName];
349 this.rbdService.listNamespaces(selectedPoolName).subscribe((namespaces: any[]) => {
350 namespaces = namespaces.map((namespace) => namespace.namespace);
351 this.namespacesByPoolCache[selectedPoolName] = namespaces;
352 this.namespaces = namespaces;
355 this.rbdForm.get('namespace').setValue(null);
358 onUseDataPoolChange() {
359 if (!this.rbdForm.getValue('useDataPool')) {
360 this.rbdForm.get('dataPool').setValue(null);
361 this.onDataPoolChange(null);
365 onDataPoolChange(selectedDataPoolName: string) {
366 const newPools = this.allPools.filter((pool: Pool) => {
367 return pool.pool_name !== selectedDataPoolName;
369 if (this.rbdForm.getValue('pool') === selectedDataPoolName) {
370 this.rbdForm.get('pool').setValue(null);
372 this.pools = newPools;
375 validateRbdForm(formatter: FormatterService): ValidatorFn {
376 return (formGroup: CdFormGroup) => {
378 const useDataPoolControl = formGroup.get('useDataPool');
379 const dataPoolControl = formGroup.get('dataPool');
380 let dataPoolControlErrors = null;
381 if (useDataPoolControl.value && dataPoolControl.value == null) {
382 dataPoolControlErrors = { required: true };
384 dataPoolControl.setErrors(dataPoolControlErrors);
386 const sizeControl = formGroup.get('size');
387 const objectSizeControl = formGroup.get('obj_size');
388 const objectSizeInBytes = formatter.toBytes(
389 objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize
391 const stripingCountControl = formGroup.get('stripingCount');
392 const stripingCount = stripingCountControl.value != null ? stripingCountControl.value : 1;
393 let sizeControlErrors = null;
394 if (sizeControl.value === null) {
395 sizeControlErrors = { required: true };
397 const sizeInBytes = formatter.toBytes(sizeControl.value);
398 if (stripingCount * objectSizeInBytes > sizeInBytes) {
399 sizeControlErrors = { invalidSizeObject: true };
402 sizeControl.setErrors(sizeControlErrors);
404 const stripingUnitControl = formGroup.get('stripingUnit');
405 let stripingUnitControlErrors = null;
406 if (stripingUnitControl.value === null && stripingCountControl.value !== null) {
407 stripingUnitControlErrors = { required: true };
408 } else if (stripingUnitControl.value !== null) {
409 const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value);
410 if (stripingUnitInBytes > objectSizeInBytes) {
411 stripingUnitControlErrors = { invalidStripingUnit: true };
414 stripingUnitControl.setErrors(stripingUnitControlErrors);
416 let stripingCountControlErrors = null;
417 if (stripingCountControl.value === null && stripingUnitControl.value !== null) {
418 stripingCountControlErrors = { required: true };
419 } else if (stripingCount < 1) {
420 stripingCountControlErrors = { min: true };
422 stripingCountControl.setErrors(stripingCountControlErrors);
427 deepBoxCheck(key: string, checked: boolean) {
428 const childFeatures = this.getDependentChildFeatures(key);
429 childFeatures.forEach((feature) => {
430 const featureControl = this.rbdForm.get(feature.key);
432 featureControl.enable({ emitEvent: false });
434 featureControl.disable({ emitEvent: false });
435 featureControl.setValue(false, { emitEvent: false });
436 this.deepBoxCheck(feature.key, checked);
439 const featureFormGroup = this.rbdForm.get('features');
440 if (this.mode === this.rbdFormMode.editing && featureFormGroup.get(feature.key).enabled) {
441 if (this.response.features_name.indexOf(feature.key) !== -1 && !feature.allowDisable) {
442 featureFormGroup.get(feature.key).disable();
444 this.response.features_name.indexOf(feature.key) === -1 &&
447 featureFormGroup.get(feature.key).disable();
453 protected getDependentChildFeatures(featureKey: string) {
454 return _.filter(this.features, (f) => f.requires === featureKey) || [];
457 interlockCheck(key: string, checked: boolean) {
458 // Adds a compatibility layer for Ceph cluster where the feature interlock of features hasn't
459 // been implemented yet. It disables the feature interlock for images which only have one of
460 // both interlocked features (at the time of this writing: object-map and fast-diff) enabled.
461 const feature = this.featuresList.find((f) => f.key === key);
463 // Ignore `create` page
464 const hasInterlockedFeature = feature.interlockedWith != null;
465 const dependentInterlockedFeature = this.featuresList.find(
466 (f) => f.interlockedWith === feature.key
468 const isOriginFeatureEnabled = !!this.response.features_name.find((e) => e === feature.key); // in this case: fast-diff
469 if (hasInterlockedFeature) {
470 const isLinkedEnabled = !!this.response.features_name.find(
471 (e) => e === feature.interlockedWith
472 ); // depends: object-map
473 if (isOriginFeatureEnabled !== isLinkedEnabled) {
474 return; // Ignore incompatible setting because it's from a previous cluster version
476 } else if (dependentInterlockedFeature) {
477 const isOtherInterlockedFeatureEnabled = !!this.response.features_name.find(
478 (e) => e === dependentInterlockedFeature.key
480 if (isOtherInterlockedFeatureEnabled !== isOriginFeatureEnabled) {
481 return; // Ignore incompatible setting because it's from a previous cluster version
487 _.filter(this.features, (f) => f.interlockedWith === key).forEach((f) =>
488 this.rbdForm.get(f.key).setValue(true, { emitEvent: false })
491 if (feature.interlockedWith) {
492 // Don't skip emitting the event here, as it prevents `fast-diff` from
493 // becoming disabled when manually unchecked. This is because it
494 // triggers an update on `object-map` and if `object-map` doesn't emit,
495 // `fast-diff` will not be automatically disabled.
496 this.rbdForm.get('features').get(feature.interlockedWith).setValue(false);
501 featureFormUpdate(key: string, checked: boolean) {
503 const required = this.features[key].requires;
504 if (required && !this.rbdForm.getValue(required)) {
505 this.rbdForm.get(`features.${key}`).setValue(false);
509 this.deepBoxCheck(key, checked);
510 this.interlockCheck(key, checked);
513 setFeatures(features: Array<string>) {
514 const featuresControl = this.rbdForm.get('features');
515 _.forIn(this.features, (feature) => {
516 if (features.indexOf(feature.key) !== -1) {
517 featuresControl.get(feature.key).setValue(true);
519 this.featureFormUpdate(feature.key, featuresControl.get(feature.key).value);
523 setResponse(response: RbdFormResponseModel, snapName: string) {
524 this.response = response;
525 const imageSpec = new ImageSpec(
530 if (this.mode === this.rbdFormMode.cloning) {
531 this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`);
532 } else if (this.mode === this.rbdFormMode.copying) {
534 this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`);
536 this.rbdForm.get('parent').setValue(`${imageSpec}`);
538 } else if (response.parent) {
539 const parent = response.parent;
542 .setValue(`${parent.pool_name}/${parent.image_name}@${parent.snap_name}`);
544 if (this.mode === this.rbdFormMode.editing) {
545 this.rbdForm.get('name').setValue(response.name);
547 this.rbdForm.get('pool').setValue(response.pool_name);
548 this.onPoolChange(response.pool_name);
549 this.rbdForm.get('namespace').setValue(response.namespace);
550 if (response.data_pool) {
551 this.rbdForm.get('useDataPool').setValue(true);
552 this.rbdForm.get('dataPool').setValue(response.data_pool);
554 this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size));
555 this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size));
556 this.setFeatures(response.features_name);
559 .setValue(this.dimlessBinaryPipe.transform(response.stripe_unit));
560 this.rbdForm.get('stripingCount').setValue(response.stripe_count);
562 this.initializeConfigData.next({
563 initialData: this.response.configuration,
564 sourceType: RbdConfigurationSourceField.image
569 const request = new RbdFormCreateRequestModel();
570 request.pool_name = this.rbdForm.getValue('pool');
571 request.namespace = this.rbdForm.getValue('namespace');
572 request.name = this.rbdForm.getValue('name');
573 request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
574 this.addObjectSizeAndStripingToRequest(request);
575 request.configuration = this.getDirtyConfigurationValues();
579 private addObjectSizeAndStripingToRequest(
580 request: RbdFormCreateRequestModel | RbdFormCloneRequestModel | RbdFormCopyRequestModel
582 request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size'));
583 _.forIn(this.features, (feature) => {
584 if (this.rbdForm.getValue(feature.key)) {
585 request.features.push(feature.key);
590 request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
591 request.stripe_count = this.rbdForm.getValue('stripingCount');
592 request.data_pool = this.rbdForm.getValue('dataPool');
595 createAction(): Observable<any> {
596 const request = this.createRequest();
597 return this.taskWrapper.wrapTaskAroundCall({
598 task: new FinishedTask('rbd/create', {
599 pool_name: request.pool_name,
600 namespace: request.namespace,
601 image_name: request.name
603 call: this.rbdService.create(request)
608 const request = new RbdFormEditRequestModel();
609 request.name = this.rbdForm.getValue('name');
610 request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
611 _.forIn(this.features, (feature) => {
612 if (this.rbdForm.getValue(feature.key)) {
613 request.features.push(feature.key);
616 request.configuration = this.getDirtyConfigurationValues();
620 cloneRequest(): RbdFormCloneRequestModel {
621 const request = new RbdFormCloneRequestModel();
622 request.child_pool_name = this.rbdForm.getValue('pool');
623 request.child_namespace = this.rbdForm.getValue('namespace');
624 request.child_image_name = this.rbdForm.getValue('name');
625 this.addObjectSizeAndStripingToRequest(request);
626 request.configuration = this.getDirtyConfigurationValues(
628 RbdConfigurationSourceField.image
633 editAction(): Observable<any> {
634 const imageSpec = new ImageSpec(
635 this.response.pool_name,
636 this.response.namespace,
639 return this.taskWrapper.wrapTaskAroundCall({
640 task: new FinishedTask('rbd/edit', {
641 image_spec: imageSpec.toString()
643 call: this.rbdService.update(imageSpec, this.editRequest())
647 cloneAction(): Observable<any> {
648 const request = this.cloneRequest();
649 const imageSpec = new ImageSpec(
650 this.response.pool_name,
651 this.response.namespace,
654 return this.taskWrapper.wrapTaskAroundCall({
655 task: new FinishedTask('rbd/clone', {
656 parent_image_spec: imageSpec.toString(),
657 parent_snap_name: this.snapName,
658 child_pool_name: request.child_pool_name,
659 child_namespace: request.child_namespace,
660 child_image_name: request.child_image_name
662 call: this.rbdService.cloneSnapshot(imageSpec, this.snapName, request)
666 copyRequest(): RbdFormCopyRequestModel {
667 const request = new RbdFormCopyRequestModel();
669 request.snapshot_name = this.snapName;
671 request.dest_pool_name = this.rbdForm.getValue('pool');
672 request.dest_namespace = this.rbdForm.getValue('namespace');
673 request.dest_image_name = this.rbdForm.getValue('name');
674 this.addObjectSizeAndStripingToRequest(request);
675 request.configuration = this.getDirtyConfigurationValues(
677 RbdConfigurationSourceField.image
682 copyAction(): Observable<any> {
683 const request = this.copyRequest();
684 const imageSpec = new ImageSpec(
685 this.response.pool_name,
686 this.response.namespace,
689 return this.taskWrapper.wrapTaskAroundCall({
690 task: new FinishedTask('rbd/copy', {
691 src_image_spec: imageSpec.toString(),
692 dest_pool_name: request.dest_pool_name,
693 dest_namespace: request.dest_namespace,
694 dest_image_name: request.dest_image_name
696 call: this.rbdService.copy(imageSpec, request)
702 this.rbdImage.next('create');
708 if (this.mode === this.rbdFormMode.editing) {
709 return this.editAction();
710 } else if (this.mode === this.rbdFormMode.cloning) {
711 return this.cloneAction();
712 } else if (this.mode === this.rbdFormMode.copying) {
713 return this.copyAction();
715 return this.createAction();
721 () => this.rbdForm.setErrors({ cdSubmitButton: true }),
722 () => this.router.navigate(['/block/rbd'])