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