]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
4fa71646215fafe9f6603309d04818892c97651b
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-form / rbd-form.component.ts
1 import { Component, OnInit } from '@angular/core';
2 import { FormControl, ValidatorFn, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import _ from 'lodash';
6 import { forkJoin, Observable, ReplaySubject } from 'rxjs';
7 import { first, switchMap } from 'rxjs/operators';
8
9 import { Pool } from '~/app/ceph/pool/pool';
10 import { PoolService } from '~/app/shared/api/pool.service';
11 import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service';
12 import { RbdService } from '~/app/shared/api/rbd.service';
13 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
14 import { Icons } from '~/app/shared/enum/icons.enum';
15 import { CdForm } from '~/app/shared/forms/cd-form';
16 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
17 import {
18 RbdConfigurationEntry,
19 RbdConfigurationSourceField
20 } from '~/app/shared/models/configuration';
21 import { FinishedTask } from '~/app/shared/models/finished-task';
22 import { ImageSpec } from '~/app/shared/models/image-spec';
23 import { Permission } from '~/app/shared/models/permissions';
24 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
25 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
26 import { FormatterService } from '~/app/shared/services/formatter.service';
27 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
28 import { RBDImageFormat, RbdModel } from '../rbd-list/rbd-model';
29 import { RbdImageFeature } from './rbd-feature.interface';
30 import { RbdFormCloneRequestModel } from './rbd-form-clone-request.model';
31 import { RbdFormCopyRequestModel } from './rbd-form-copy-request.model';
32 import { RbdFormCreateRequestModel } from './rbd-form-create-request.model';
33 import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
34 import { RbdFormMode } from './rbd-form-mode.enum';
35 import { RbdFormResponseModel } from './rbd-form-response.model';
36
37 class ExternalData {
38 rbd: RbdFormResponseModel;
39 defaultFeatures: string[];
40 pools: Pool[];
41 }
42
43 @Component({
44 selector: 'cd-rbd-form',
45 templateUrl: './rbd-form.component.html',
46 styleUrls: ['./rbd-form.component.scss']
47 })
48 export class RbdFormComponent extends CdForm implements OnInit {
49 poolPermission: Permission;
50 rbdForm: CdFormGroup;
51 getDirtyConfigurationValues: (
52 includeLocalField?: boolean,
53 localField?: RbdConfigurationSourceField
54 ) => RbdConfigurationEntry[];
55
56 namespaces: Array<string> = [];
57 namespacesByPoolCache = {};
58 pools: Array<Pool> = null;
59 allPools: Array<Pool> = null;
60 dataPools: Array<Pool> = null;
61 allDataPools: Array<Pool> = [];
62 features: { [key: string]: RbdImageFeature };
63 featuresList: RbdImageFeature[] = [];
64 initializeConfigData = new ReplaySubject<{
65 initialData: RbdConfigurationEntry[];
66 sourceType: RbdConfigurationSourceField;
67 }>(1);
68
69 pool: string;
70
71 advancedEnabled = false;
72
73 public rbdFormMode = RbdFormMode;
74 mode: RbdFormMode;
75
76 response: RbdFormResponseModel;
77 snapName: string;
78
79 defaultObjectSize = '4 MiB';
80
81 mirroringOptions = ['journal', 'snapshot'];
82 poolMirrorMode: string;
83 mirroring = false;
84 currentPoolName = '';
85
86 objectSizes: Array<string> = [
87 '4 KiB',
88 '8 KiB',
89 '16 KiB',
90 '32 KiB',
91 '64 KiB',
92 '128 KiB',
93 '256 KiB',
94 '512 KiB',
95 '1 MiB',
96 '2 MiB',
97 '4 MiB',
98 '8 MiB',
99 '16 MiB',
100 '32 MiB'
101 ];
102
103 defaultStripingUnit = '4 MiB';
104
105 defaultStripingCount = 1;
106
107 action: string;
108 resource: string;
109 private rbdImage = new ReplaySubject(1);
110 private routerUrl: string;
111
112 icons = Icons;
113
114 constructor(
115 private authStorageService: AuthStorageService,
116 private route: ActivatedRoute,
117 private poolService: PoolService,
118 private rbdService: RbdService,
119 private formatter: FormatterService,
120 private taskWrapper: TaskWrapperService,
121 private dimlessBinaryPipe: DimlessBinaryPipe,
122 public actionLabels: ActionLabelsI18n,
123 private router: Router,
124 private rbdMirroringService: RbdMirroringService
125 ) {
126 super();
127 this.routerUrl = this.router.url;
128 this.poolPermission = this.authStorageService.getPermissions().pool;
129 this.resource = $localize`RBD`;
130 this.features = {
131 'deep-flatten': {
132 desc: $localize`Deep flatten`,
133 requires: null,
134 allowEnable: false,
135 allowDisable: true
136 },
137 layering: {
138 desc: $localize`Layering`,
139 requires: null,
140 allowEnable: false,
141 allowDisable: false
142 },
143 'exclusive-lock': {
144 desc: $localize`Exclusive lock`,
145 requires: null,
146 allowEnable: true,
147 allowDisable: true
148 },
149 'object-map': {
150 desc: $localize`Object map (requires exclusive-lock)`,
151 requires: 'exclusive-lock',
152 allowEnable: true,
153 allowDisable: true,
154 initDisabled: true
155 },
156 'fast-diff': {
157 desc: $localize`Fast diff (interlocked with object-map)`,
158 requires: 'object-map',
159 allowEnable: true,
160 allowDisable: true,
161 interlockedWith: 'object-map',
162 initDisabled: true
163 }
164 };
165 this.featuresList = this.objToArray(this.features);
166 this.createForm();
167 }
168
169 objToArray(obj: { [key: string]: any }) {
170 return _.map(obj, (o, key) => Object.assign(o, { key: key }));
171 }
172
173 createForm() {
174 this.rbdForm = new CdFormGroup(
175 {
176 parent: new FormControl(''),
177 name: new FormControl('', {
178 validators: [Validators.required, Validators.pattern(/^[^@/]+?$/)]
179 }),
180 pool: new FormControl(null, {
181 validators: [Validators.required]
182 }),
183 namespace: new FormControl(null),
184 useDataPool: new FormControl(false),
185 dataPool: new FormControl(null),
186 size: new FormControl(null, {
187 updateOn: 'blur'
188 }),
189 obj_size: new FormControl(this.defaultObjectSize),
190 features: new CdFormGroup(
191 this.featuresList.reduce((acc: object, e) => {
192 acc[e.key] = new FormControl({ value: false, disabled: !!e.initDisabled });
193 return acc;
194 }, {})
195 ),
196 mirroring: new FormControl(false),
197 schedule: new FormControl('', {
198 validators: [Validators.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/)] // check schedule interval to be in format - 1d or 1h or 1m
199 }),
200 mirroringMode: new FormControl(this.mirroringOptions[0]),
201 stripingUnit: new FormControl(this.defaultStripingUnit),
202 stripingCount: new FormControl(this.defaultStripingCount, {
203 updateOn: 'blur'
204 })
205 },
206 this.validateRbdForm(this.formatter)
207 );
208 }
209
210 disableForEdit() {
211 this.rbdForm.get('parent').disable();
212 this.rbdForm.get('pool').disable();
213 this.rbdForm.get('namespace').disable();
214 this.rbdForm.get('useDataPool').disable();
215 this.rbdForm.get('dataPool').disable();
216 this.rbdForm.get('obj_size').disable();
217 this.rbdForm.get('stripingUnit').disable();
218 this.rbdForm.get('stripingCount').disable();
219
220 /* RBD Image Format v1 */
221 this.rbdImage.subscribe((image: RbdModel) => {
222 if (image.image_format === RBDImageFormat.V1) {
223 this.rbdForm.get('deep-flatten').disable();
224 this.rbdForm.get('layering').disable();
225 this.rbdForm.get('exclusive-lock').disable();
226 }
227 });
228 }
229
230 disableForClone() {
231 this.rbdForm.get('parent').disable();
232 this.rbdForm.get('size').disable();
233 }
234
235 disableForCopy() {
236 this.rbdForm.get('parent').disable();
237 this.rbdForm.get('size').disable();
238 }
239
240 ngOnInit() {
241 this.prepareFormForAction();
242 this.gatherNeededData().subscribe(this.handleExternalData.bind(this));
243 }
244
245 setExclusiveLock() {
246 if (this.mirroring && this.rbdForm.get('mirroringMode').value === 'journal') {
247 this.rbdForm.get('exclusive-lock').setValue(true);
248 this.rbdForm.get('exclusive-lock').disable();
249 } else {
250 this.rbdForm.get('exclusive-lock').enable();
251 if (this.poolMirrorMode === 'pool') {
252 this.rbdForm.get('mirroringMode').setValue(this.mirroringOptions[0]);
253 }
254 }
255 }
256
257 setMirrorMode() {
258 this.mirroring = !this.mirroring;
259 this.setExclusiveLock();
260 }
261
262 setPoolMirrorMode() {
263 this.currentPoolName =
264 this.mode === this.rbdFormMode.editing
265 ? this.response?.pool_name
266 : this.rbdForm.getValue('pool');
267 if (this.currentPoolName) {
268 this.rbdMirroringService.refresh();
269 this.rbdMirroringService.subscribeSummary((data) => {
270 const pool = data.content_data.pools.find((o: any) => o.name === this.currentPoolName);
271 this.poolMirrorMode = pool.mirror_mode;
272
273 if (pool.mirror_mode === 'disabled') {
274 this.mirroring = false;
275 this.rbdForm.get('mirroring').setValue(this.mirroring);
276 this.rbdForm.get('mirroring').disable();
277 } else if (this.mode !== this.rbdFormMode.editing) {
278 this.rbdForm.get('mirroring').enable();
279 this.mirroring = true;
280 this.rbdForm.get('mirroring').setValue(this.mirroring);
281 }
282 });
283 }
284 this.setExclusiveLock();
285 }
286
287 private prepareFormForAction() {
288 const url = this.routerUrl;
289 if (url.startsWith('/block/rbd/edit')) {
290 this.mode = this.rbdFormMode.editing;
291 this.action = this.actionLabels.EDIT;
292 this.disableForEdit();
293 } else if (url.startsWith('/block/rbd/clone')) {
294 this.mode = this.rbdFormMode.cloning;
295 this.disableForClone();
296 this.action = this.actionLabels.CLONE;
297 } else if (url.startsWith('/block/rbd/copy')) {
298 this.mode = this.rbdFormMode.copying;
299 this.action = this.actionLabels.COPY;
300 this.disableForCopy();
301 } else {
302 this.action = this.actionLabels.CREATE;
303 }
304 _.each(this.features, (feature) => {
305 this.rbdForm
306 .get('features')
307 .get(feature.key)
308 .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
309 });
310 }
311
312 private gatherNeededData(): Observable<object> {
313 const promises = {};
314 if (this.mode) {
315 // Mode is not set for creation
316 this.route.params.subscribe((params: { image_spec: string; snap: string }) => {
317 const imageSpec = ImageSpec.fromString(decodeURIComponent(params.image_spec));
318 if (params.snap) {
319 this.snapName = decodeURIComponent(params.snap);
320 }
321 promises['rbd'] = this.rbdService.get(imageSpec);
322 });
323 } else {
324 // New image
325 promises['defaultFeatures'] = this.rbdService.defaultFeatures();
326 }
327 if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) {
328 promises['pools'] = this.poolService.list([
329 'pool_name',
330 'type',
331 'flags_names',
332 'application_metadata'
333 ]);
334 }
335 return forkJoin(promises);
336 }
337
338 private handleExternalData(data: ExternalData) {
339 this.handlePoolData(data.pools);
340 this.setPoolMirrorMode();
341
342 if (data.defaultFeatures) {
343 // Fetched only during creation
344 this.setFeatures(data.defaultFeatures);
345 }
346
347 if (data.rbd) {
348 // Not fetched for creation
349 const resp = data.rbd;
350 this.setResponse(resp, this.snapName);
351 this.rbdImage.next(resp);
352 }
353
354 this.loadingReady();
355 }
356
357 private handlePoolData(data: Pool[]) {
358 if (!data) {
359 // Not fetched while editing
360 return;
361 }
362 const pools: Pool[] = [];
363 const dataPools = [];
364 for (const pool of data) {
365 if (this.rbdService.isRBDPool(pool)) {
366 if (pool.type === 'replicated') {
367 pools.push(pool);
368 dataPools.push(pool);
369 } else if (pool.type === 'erasure' && pool.flags_names.indexOf('ec_overwrites') !== -1) {
370 dataPools.push(pool);
371 }
372 }
373 }
374 this.pools = pools;
375 this.allPools = pools;
376 this.dataPools = dataPools;
377 this.allDataPools = dataPools;
378 if (this.pools.length === 1) {
379 const poolName = this.pools[0].pool_name;
380 this.rbdForm.get('pool').setValue(poolName);
381 this.onPoolChange(poolName);
382 }
383 if (this.allDataPools.length <= 1) {
384 this.rbdForm.get('useDataPool').disable();
385 }
386 }
387
388 onPoolChange(selectedPoolName: string) {
389 const dataPoolControl = this.rbdForm.get('dataPool');
390 if (dataPoolControl.value === selectedPoolName) {
391 dataPoolControl.setValue(null);
392 }
393 this.dataPools = this.allDataPools
394 ? this.allDataPools.filter((dataPool: any) => {
395 return dataPool.pool_name !== selectedPoolName;
396 })
397 : [];
398 this.namespaces = null;
399 if (selectedPoolName in this.namespacesByPoolCache) {
400 this.namespaces = this.namespacesByPoolCache[selectedPoolName];
401 } else {
402 this.rbdService.listNamespaces(selectedPoolName).subscribe((namespaces: any[]) => {
403 namespaces = namespaces.map((namespace) => namespace.namespace);
404 this.namespacesByPoolCache[selectedPoolName] = namespaces;
405 this.namespaces = namespaces;
406 });
407 }
408 this.rbdForm.get('namespace').setValue(null);
409 }
410
411 onUseDataPoolChange() {
412 if (!this.rbdForm.getValue('useDataPool')) {
413 this.rbdForm.get('dataPool').setValue(null);
414 this.onDataPoolChange(null);
415 }
416 }
417
418 onDataPoolChange(selectedDataPoolName: string) {
419 const newPools = this.allPools.filter((pool: Pool) => {
420 return pool.pool_name !== selectedDataPoolName;
421 });
422 if (this.rbdForm.getValue('pool') === selectedDataPoolName) {
423 this.rbdForm.get('pool').setValue(null);
424 }
425 this.pools = newPools;
426 }
427
428 validateRbdForm(formatter: FormatterService): ValidatorFn {
429 return (formGroup: CdFormGroup) => {
430 // Data Pool
431 const useDataPoolControl = formGroup.get('useDataPool');
432 const dataPoolControl = formGroup.get('dataPool');
433 let dataPoolControlErrors = null;
434 if (useDataPoolControl.value && dataPoolControl.value == null) {
435 dataPoolControlErrors = { required: true };
436 }
437 dataPoolControl.setErrors(dataPoolControlErrors);
438 // Size
439 const sizeControl = formGroup.get('size');
440 const objectSizeControl = formGroup.get('obj_size');
441 const objectSizeInBytes = formatter.toBytes(
442 objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize
443 );
444 const stripingCountControl = formGroup.get('stripingCount');
445 const stripingCount =
446 stripingCountControl.value != null ? stripingCountControl.value : this.defaultStripingCount;
447 let sizeControlErrors = null;
448 if (sizeControl.value === null) {
449 sizeControlErrors = { required: true };
450 } else {
451 const sizeInBytes = formatter.toBytes(sizeControl.value);
452 if (stripingCount * objectSizeInBytes > sizeInBytes) {
453 sizeControlErrors = { invalidSizeObject: true };
454 }
455 }
456 sizeControl.setErrors(sizeControlErrors);
457 // Striping Unit
458 const stripingUnitControl = formGroup.get('stripingUnit');
459 let stripingUnitControlErrors = null;
460 if (stripingUnitControl.value === null && stripingCountControl.value !== null) {
461 stripingUnitControlErrors = { required: true };
462 } else if (stripingUnitControl.value !== null) {
463 const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value);
464 if (stripingUnitInBytes > objectSizeInBytes) {
465 stripingUnitControlErrors = { invalidStripingUnit: true };
466 }
467 }
468 stripingUnitControl.setErrors(stripingUnitControlErrors);
469 // Striping Count
470 let stripingCountControlErrors = null;
471 if (stripingCountControl.value === null && stripingUnitControl.value !== null) {
472 stripingCountControlErrors = { required: true };
473 } else if (stripingCount < 1) {
474 stripingCountControlErrors = { min: true };
475 }
476 stripingCountControl.setErrors(stripingCountControlErrors);
477 return null;
478 };
479 }
480
481 deepBoxCheck(key: string, checked: boolean) {
482 const childFeatures = this.getDependentChildFeatures(key);
483 childFeatures.forEach((feature) => {
484 const featureControl = this.rbdForm.get(feature.key);
485 if (checked) {
486 featureControl.enable({ emitEvent: false });
487 } else {
488 featureControl.disable({ emitEvent: false });
489 featureControl.setValue(false, { emitEvent: false });
490 this.deepBoxCheck(feature.key, checked);
491 }
492
493 const featureFormGroup = this.rbdForm.get('features');
494 if (this.mode === this.rbdFormMode.editing && featureFormGroup.get(feature.key).enabled) {
495 if (this.response.features_name.indexOf(feature.key) !== -1 && !feature.allowDisable) {
496 featureFormGroup.get(feature.key).disable();
497 } else if (
498 this.response.features_name.indexOf(feature.key) === -1 &&
499 !feature.allowEnable
500 ) {
501 featureFormGroup.get(feature.key).disable();
502 }
503 }
504 });
505 }
506
507 protected getDependentChildFeatures(featureKey: string) {
508 return _.filter(this.features, (f) => f.requires === featureKey) || [];
509 }
510
511 interlockCheck(key: string, checked: boolean) {
512 // Adds a compatibility layer for Ceph cluster where the feature interlock of features hasn't
513 // been implemented yet. It disables the feature interlock for images which only have one of
514 // both interlocked features (at the time of this writing: object-map and fast-diff) enabled.
515 const feature = this.featuresList.find((f) => f.key === key);
516 if (this.response) {
517 // Ignore `create` page
518 const hasInterlockedFeature = feature.interlockedWith != null;
519 const dependentInterlockedFeature = this.featuresList.find(
520 (f) => f.interlockedWith === feature.key
521 );
522 const isOriginFeatureEnabled = !!this.response.features_name.find((e) => e === feature.key); // in this case: fast-diff
523 if (hasInterlockedFeature) {
524 const isLinkedEnabled = !!this.response.features_name.find(
525 (e) => e === feature.interlockedWith
526 ); // depends: object-map
527 if (isOriginFeatureEnabled !== isLinkedEnabled) {
528 return; // Ignore incompatible setting because it's from a previous cluster version
529 }
530 } else if (dependentInterlockedFeature) {
531 const isOtherInterlockedFeatureEnabled = !!this.response.features_name.find(
532 (e) => e === dependentInterlockedFeature.key
533 );
534 if (isOtherInterlockedFeatureEnabled !== isOriginFeatureEnabled) {
535 return; // Ignore incompatible setting because it's from a previous cluster version
536 }
537 }
538 }
539
540 if (checked) {
541 _.filter(this.features, (f) => f.interlockedWith === key).forEach((f) =>
542 this.rbdForm.get(f.key).setValue(true, { emitEvent: false })
543 );
544 } else {
545 if (feature.interlockedWith) {
546 // Don't skip emitting the event here, as it prevents `fast-diff` from
547 // becoming disabled when manually unchecked. This is because it
548 // triggers an update on `object-map` and if `object-map` doesn't emit,
549 // `fast-diff` will not be automatically disabled.
550 this.rbdForm.get('features').get(feature.interlockedWith).setValue(false);
551 }
552 }
553 }
554
555 featureFormUpdate(key: string, checked: boolean) {
556 if (checked) {
557 const required = this.features[key].requires;
558 if (required && !this.rbdForm.getValue(required)) {
559 this.rbdForm.get(`features.${key}`).setValue(false);
560 return;
561 }
562 }
563 this.deepBoxCheck(key, checked);
564 this.interlockCheck(key, checked);
565 }
566
567 setFeatures(features: Array<string>) {
568 const featuresControl = this.rbdForm.get('features');
569 _.forIn(this.features, (feature) => {
570 if (features.indexOf(feature.key) !== -1) {
571 featuresControl.get(feature.key).setValue(true);
572 }
573 this.featureFormUpdate(feature.key, featuresControl.get(feature.key).value);
574 });
575 }
576
577 setResponse(response: RbdFormResponseModel, snapName: string) {
578 this.response = response;
579 const imageSpec = new ImageSpec(
580 response.pool_name,
581 response.namespace,
582 response.name
583 ).toString();
584 if (this.mode === this.rbdFormMode.cloning) {
585 this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`);
586 } else if (this.mode === this.rbdFormMode.copying) {
587 if (snapName) {
588 this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`);
589 } else {
590 this.rbdForm.get('parent').setValue(`${imageSpec}`);
591 }
592 } else if (response.parent) {
593 const parent = response.parent;
594 this.rbdForm
595 .get('parent')
596 .setValue(`${parent.pool_name}/${parent.image_name}@${parent.snap_name}`);
597 }
598 if (this.mode === this.rbdFormMode.editing) {
599 this.rbdForm.get('name').setValue(response.name);
600 if (response?.mirror_mode === 'snapshot' || response.features_name.includes('journaling')) {
601 this.mirroring = true;
602 this.rbdForm.get('mirroring').setValue(this.mirroring);
603 this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode);
604 this.rbdForm.get('schedule').setValue(response?.schedule_interval);
605 } else {
606 this.mirroring = false;
607 this.rbdForm.get('mirroring').setValue(this.mirroring);
608 }
609 this.setPoolMirrorMode();
610 }
611 this.rbdForm.get('pool').setValue(response.pool_name);
612 this.onPoolChange(response.pool_name);
613 this.rbdForm.get('namespace').setValue(response.namespace);
614 if (response.data_pool) {
615 this.rbdForm.get('useDataPool').setValue(true);
616 this.rbdForm.get('dataPool').setValue(response.data_pool);
617 }
618 this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size));
619 this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size));
620 this.setFeatures(response.features_name);
621 this.rbdForm
622 .get('stripingUnit')
623 .setValue(this.dimlessBinaryPipe.transform(response.stripe_unit));
624 this.rbdForm.get('stripingCount').setValue(response.stripe_count);
625 /* Configuration */
626 this.initializeConfigData.next({
627 initialData: this.response.configuration,
628 sourceType: RbdConfigurationSourceField.image
629 });
630 }
631
632 createRequest() {
633 const request = new RbdFormCreateRequestModel();
634 request.pool_name = this.rbdForm.getValue('pool');
635 request.namespace = this.rbdForm.getValue('namespace');
636 request.name = this.rbdForm.getValue('name');
637 request.schedule_interval = this.rbdForm.getValue('schedule');
638 request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
639 if (this.poolMirrorMode === 'image') {
640 request.mirror_mode = this.rbdForm.getValue('mirroringMode');
641 }
642 this.addObjectSizeAndStripingToRequest(request);
643 request.configuration = this.getDirtyConfigurationValues();
644 return request;
645 }
646
647 private addObjectSizeAndStripingToRequest(
648 request: RbdFormCreateRequestModel | RbdFormCloneRequestModel | RbdFormCopyRequestModel
649 ) {
650 request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size'));
651 _.forIn(this.features, (feature) => {
652 if (this.rbdForm.getValue(feature.key)) {
653 request.features.push(feature.key);
654 }
655 });
656
657 if (this.mirroring && this.rbdForm.getValue('mirroringMode') === 'journal') {
658 request.features.push('journaling');
659 }
660
661 /* Striping */
662 request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit'));
663 request.stripe_count = this.rbdForm.getValue('stripingCount');
664 request.data_pool = this.rbdForm.getValue('dataPool');
665 }
666
667 createAction(): Observable<any> {
668 const request = this.createRequest();
669 return this.taskWrapper.wrapTaskAroundCall({
670 task: new FinishedTask('rbd/create', {
671 pool_name: request.pool_name,
672 namespace: request.namespace,
673 image_name: request.name,
674 schedule_interval: request.schedule_interval,
675 start_time: request.start_time
676 }),
677 call: this.rbdService.create(request)
678 });
679 }
680
681 editRequest() {
682 const request = new RbdFormEditRequestModel();
683 request.name = this.rbdForm.getValue('name');
684 request.schedule_interval = this.rbdForm.getValue('schedule');
685 request.name = this.rbdForm.getValue('name');
686 request.size = this.formatter.toBytes(this.rbdForm.getValue('size'));
687 _.forIn(this.features, (feature) => {
688 if (this.rbdForm.getValue(feature.key)) {
689 request.features.push(feature.key);
690 }
691 });
692 request.enable_mirror = this.rbdForm.getValue('mirroring');
693 if (this.poolMirrorMode === 'image') {
694 if (request.enable_mirror) {
695 request.mirror_mode = this.rbdForm.getValue('mirroringMode');
696 }
697 } else {
698 if (request.enable_mirror) {
699 request.features.push('journaling');
700 } else {
701 const index = request.features.indexOf('journaling', 0);
702 if (index > -1) {
703 request.features.splice(index, 1);
704 }
705 }
706 }
707 request.configuration = this.getDirtyConfigurationValues();
708 return request;
709 }
710
711 cloneRequest(): RbdFormCloneRequestModel {
712 const request = new RbdFormCloneRequestModel();
713 request.child_pool_name = this.rbdForm.getValue('pool');
714 request.child_namespace = this.rbdForm.getValue('namespace');
715 request.child_image_name = this.rbdForm.getValue('name');
716 this.addObjectSizeAndStripingToRequest(request);
717 request.configuration = this.getDirtyConfigurationValues(
718 true,
719 RbdConfigurationSourceField.image
720 );
721 return request;
722 }
723
724 editAction(): Observable<any> {
725 const imageSpec = new ImageSpec(
726 this.response.pool_name,
727 this.response.namespace,
728 this.response.name
729 );
730 return this.taskWrapper.wrapTaskAroundCall({
731 task: new FinishedTask('rbd/edit', {
732 image_spec: imageSpec.toString()
733 }),
734 call: this.rbdService.update(imageSpec, this.editRequest())
735 });
736 }
737
738 cloneAction(): Observable<any> {
739 const request = this.cloneRequest();
740 const imageSpec = new ImageSpec(
741 this.response.pool_name,
742 this.response.namespace,
743 this.response.name
744 );
745 return this.taskWrapper.wrapTaskAroundCall({
746 task: new FinishedTask('rbd/clone', {
747 parent_image_spec: imageSpec.toString(),
748 parent_snap_name: this.snapName,
749 child_pool_name: request.child_pool_name,
750 child_namespace: request.child_namespace,
751 child_image_name: request.child_image_name
752 }),
753 call: this.rbdService.cloneSnapshot(imageSpec, this.snapName, request)
754 });
755 }
756
757 copyRequest(): RbdFormCopyRequestModel {
758 const request = new RbdFormCopyRequestModel();
759 if (this.snapName) {
760 request.snapshot_name = this.snapName;
761 }
762 request.dest_pool_name = this.rbdForm.getValue('pool');
763 request.dest_namespace = this.rbdForm.getValue('namespace');
764 request.dest_image_name = this.rbdForm.getValue('name');
765 this.addObjectSizeAndStripingToRequest(request);
766 request.configuration = this.getDirtyConfigurationValues(
767 true,
768 RbdConfigurationSourceField.image
769 );
770 return request;
771 }
772
773 copyAction(): Observable<any> {
774 const request = this.copyRequest();
775 const imageSpec = new ImageSpec(
776 this.response.pool_name,
777 this.response.namespace,
778 this.response.name
779 );
780 return this.taskWrapper.wrapTaskAroundCall({
781 task: new FinishedTask('rbd/copy', {
782 src_image_spec: imageSpec.toString(),
783 dest_pool_name: request.dest_pool_name,
784 dest_namespace: request.dest_namespace,
785 dest_image_name: request.dest_image_name
786 }),
787 call: this.rbdService.copy(imageSpec, request)
788 });
789 }
790
791 submit() {
792 if (!this.mode) {
793 this.rbdImage.next('create');
794 }
795 this.rbdImage
796 .pipe(
797 first(),
798 switchMap(() => {
799 if (this.mode === this.rbdFormMode.editing) {
800 return this.editAction();
801 } else if (this.mode === this.rbdFormMode.cloning) {
802 return this.cloneAction();
803 } else if (this.mode === this.rbdFormMode.copying) {
804 return this.copyAction();
805 } else {
806 return this.createAction();
807 }
808 })
809 )
810 .subscribe(
811 () => undefined,
812 () => this.rbdForm.setErrors({ cdSubmitButton: true }),
813 () => this.router.navigate(['/block/rbd'])
814 );
815 }
816 }