]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
import ceph quincy 17.2.6
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-list / rbd-list.component.ts
1 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
2
3 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
4 import _ from 'lodash';
5 import { Observable, Subscriber } from 'rxjs';
6
7 import { RbdService } from '~/app/shared/api/rbd.service';
8 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
9 import { TableStatus } from '~/app/shared/classes/table-status';
10 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
11 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
12 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
13 import { TableComponent } from '~/app/shared/datatable/table/table.component';
14 import { Icons } from '~/app/shared/enum/icons.enum';
15 import { CdTableAction } from '~/app/shared/models/cd-table-action';
16 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
17 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
18 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
19 import { FinishedTask } from '~/app/shared/models/finished-task';
20 import { ImageSpec } from '~/app/shared/models/image-spec';
21 import { Permission } from '~/app/shared/models/permissions';
22 import { Task } from '~/app/shared/models/task';
23 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
24 import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
25 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
26 import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service';
27 import { ModalService } from '~/app/shared/services/modal.service';
28 import { TaskListService } from '~/app/shared/services/task-list.service';
29 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
30 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
31 import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model';
32 import { RbdParentModel } from '../rbd-form/rbd-parent.model';
33 import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
34 import { RBDImageFormat, RbdModel } from './rbd-model';
35
36 const BASE_URL = 'block/rbd';
37
38 @Component({
39 selector: 'cd-rbd-list',
40 templateUrl: './rbd-list.component.html',
41 styleUrls: ['./rbd-list.component.scss'],
42 providers: [
43 TaskListService,
44 { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
45 ]
46 })
47 export class RbdListComponent extends ListWithDetails implements OnInit {
48 @ViewChild(TableComponent, { static: true })
49 table: TableComponent;
50 @ViewChild('usageTpl')
51 usageTpl: TemplateRef<any>;
52 @ViewChild('parentTpl', { static: true })
53 parentTpl: TemplateRef<any>;
54 @ViewChild('nameTpl')
55 nameTpl: TemplateRef<any>;
56 @ViewChild('scheduleTpl', { static: true })
57 scheduleTpl: TemplateRef<any>;
58 @ViewChild('mirroringTpl', { static: true })
59 mirroringTpl: TemplateRef<any>;
60 @ViewChild('flattenTpl', { static: true })
61 flattenTpl: TemplateRef<any>;
62 @ViewChild('deleteTpl', { static: true })
63 deleteTpl: TemplateRef<any>;
64 @ViewChild('removingStatTpl', { static: true })
65 removingStatTpl: TemplateRef<any>;
66 @ViewChild('provisionedNotAvailableTooltipTpl', { static: true })
67 provisionedNotAvailableTooltipTpl: TemplateRef<any>;
68 @ViewChild('totalProvisionedNotAvailableTooltipTpl', { static: true })
69 totalProvisionedNotAvailableTooltipTpl: TemplateRef<any>;
70
71 permission: Permission;
72 tableActions: CdTableAction[];
73 images: any;
74 columns: CdTableColumn[];
75 retries: number;
76 tableStatus = new TableStatus('light');
77 selection = new CdTableSelection();
78 icons = Icons;
79 count = 0;
80 private tableContext: CdTableFetchDataContext = null;
81 modalRef: NgbModalRef;
82
83 builders = {
84 'rbd/create': (metadata: object) =>
85 this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
86 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
87 'rbd/clone': (metadata: object) =>
88 this.createRbdFromTask(
89 metadata['child_pool_name'],
90 metadata['child_namespace'],
91 metadata['child_image_name']
92 ),
93 'rbd/copy': (metadata: object) =>
94 this.createRbdFromTask(
95 metadata['dest_pool_name'],
96 metadata['dest_namespace'],
97 metadata['dest_image_name']
98 )
99 };
100 remove_scheduling: boolean;
101
102 private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
103 const imageSpec = ImageSpec.fromString(imageSpecStr);
104 return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
105 }
106
107 private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
108 const model = new RbdModel();
109 model.id = '-1';
110 model.unique_id = '-1';
111 model.name = name;
112 model.namespace = namespace;
113 model.pool_name = pool;
114 model.image_format = RBDImageFormat.V2;
115 return model;
116 }
117
118 constructor(
119 private authStorageService: AuthStorageService,
120 private rbdService: RbdService,
121 private dimlessBinaryPipe: DimlessBinaryPipe,
122 private dimlessPipe: DimlessPipe,
123 private modalService: ModalService,
124 private taskWrapper: TaskWrapperService,
125 public taskListService: TaskListService,
126 private urlBuilder: URLBuilderService,
127 public actionLabels: ActionLabelsI18n
128 ) {
129 super();
130 this.permission = this.authStorageService.getPermissions().rbdImage;
131 const getImageUri = () =>
132 this.selection.first() &&
133 new ImageSpec(
134 this.selection.first().pool_name,
135 this.selection.first().namespace,
136 this.selection.first().name
137 ).toStringEncoded();
138 const addAction: CdTableAction = {
139 permission: 'create',
140 icon: Icons.add,
141 routerLink: () => this.urlBuilder.getCreate(),
142 canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
143 name: this.actionLabels.CREATE
144 };
145 const editAction: CdTableAction = {
146 permission: 'update',
147 icon: Icons.edit,
148 routerLink: () => this.urlBuilder.getEdit(getImageUri()),
149 name: this.actionLabels.EDIT,
150 disable: (selection: CdTableSelection) =>
151 this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection)
152 };
153 const deleteAction: CdTableAction = {
154 permission: 'delete',
155 icon: Icons.destroy,
156 click: () => this.deleteRbdModal(),
157 name: this.actionLabels.DELETE,
158 disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
159 };
160 const resyncAction: CdTableAction = {
161 permission: 'update',
162 icon: Icons.refresh,
163 click: () => this.resyncRbdModal(),
164 name: this.actionLabels.RESYNC,
165 disable: (selection: CdTableSelection) => this.getResyncDisableDesc(selection)
166 };
167 const copyAction: CdTableAction = {
168 permission: 'create',
169 canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
170 disable: (selection: CdTableSelection) =>
171 this.getRemovingStatusDesc(selection) ||
172 this.getInvalidNameDisable(selection) ||
173 !!selection.first().cdExecuting,
174 icon: Icons.copy,
175 routerLink: () => `/block/rbd/copy/${getImageUri()}`,
176 name: this.actionLabels.COPY
177 };
178 const flattenAction: CdTableAction = {
179 permission: 'update',
180 disable: (selection: CdTableSelection) =>
181 this.getRemovingStatusDesc(selection) ||
182 this.getInvalidNameDisable(selection) ||
183 selection.first().cdExecuting ||
184 !selection.first().parent,
185 icon: Icons.flatten,
186 click: () => this.flattenRbdModal(),
187 name: this.actionLabels.FLATTEN
188 };
189 const moveAction: CdTableAction = {
190 permission: 'delete',
191 icon: Icons.trash,
192 click: () => this.trashRbdModal(),
193 name: this.actionLabels.TRASH,
194 disable: (selection: CdTableSelection) =>
195 this.getRemovingStatusDesc(selection) ||
196 this.getInvalidNameDisable(selection) ||
197 selection.first().image_format === RBDImageFormat.V1
198 };
199 const removeSchedulingAction: CdTableAction = {
200 permission: 'update',
201 icon: Icons.edit,
202 click: () => this.removeSchedulingModal(),
203 name: this.actionLabels.REMOVE_SCHEDULING,
204 disable: (selection: CdTableSelection) =>
205 this.getRemovingStatusDesc(selection) ||
206 this.getInvalidNameDisable(selection) ||
207 selection.first().schedule_info === undefined
208 };
209 const promoteAction: CdTableAction = {
210 permission: 'update',
211 icon: Icons.edit,
212 click: () => this.actionPrimary(true),
213 name: this.actionLabels.PROMOTE,
214 visible: () => this.selection.first() != null && !this.selection.first().primary
215 };
216 const demoteAction: CdTableAction = {
217 permission: 'update',
218 icon: Icons.edit,
219 click: () => this.actionPrimary(false),
220 name: this.actionLabels.DEMOTE,
221 visible: () => this.selection.first() != null && this.selection.first().primary
222 };
223 this.tableActions = [
224 addAction,
225 editAction,
226 copyAction,
227 flattenAction,
228 resyncAction,
229 deleteAction,
230 moveAction,
231 removeSchedulingAction,
232 promoteAction,
233 demoteAction
234 ];
235 }
236
237 ngOnInit() {
238 this.columns = [
239 {
240 name: $localize`Name`,
241 prop: 'name',
242 flexGrow: 2,
243 cellTemplate: this.removingStatTpl
244 },
245 {
246 name: $localize`Pool`,
247 prop: 'pool_name',
248 flexGrow: 2
249 },
250 {
251 name: $localize`Namespace`,
252 prop: 'namespace',
253 flexGrow: 2
254 },
255 {
256 name: $localize`Size`,
257 prop: 'size',
258 flexGrow: 1,
259 cellClass: 'text-right',
260 sortable: false,
261 pipe: this.dimlessBinaryPipe
262 },
263 {
264 name: $localize`Objects`,
265 prop: 'num_objs',
266 flexGrow: 1,
267 cellClass: 'text-right',
268 sortable: false,
269 pipe: this.dimlessPipe
270 },
271 {
272 name: $localize`Object size`,
273 prop: 'obj_size',
274 flexGrow: 1,
275 cellClass: 'text-right',
276 sortable: false,
277 pipe: this.dimlessBinaryPipe
278 },
279 {
280 name: $localize`Provisioned`,
281 prop: 'disk_usage',
282 cellClass: 'text-center',
283 flexGrow: 1,
284 pipe: this.dimlessBinaryPipe,
285 sortable: false,
286 cellTemplate: this.provisionedNotAvailableTooltipTpl
287 },
288 {
289 name: $localize`Total provisioned`,
290 prop: 'total_disk_usage',
291 cellClass: 'text-center',
292 flexGrow: 1,
293 pipe: this.dimlessBinaryPipe,
294 sortable: false,
295 cellTemplate: this.totalProvisionedNotAvailableTooltipTpl
296 },
297 {
298 name: $localize`Parent`,
299 prop: 'parent',
300 flexGrow: 2,
301 sortable: false,
302 cellTemplate: this.parentTpl
303 },
304 {
305 name: $localize`Mirroring`,
306 prop: 'mirror_mode',
307 flexGrow: 3,
308 sortable: false,
309 cellTemplate: this.mirroringTpl
310 },
311 {
312 name: $localize`Next Scheduled Snapshot`,
313 prop: 'mirror_mode',
314 flexGrow: 3,
315 sortable: false,
316 cellTemplate: this.scheduleTpl
317 }
318 ];
319
320 const itemFilter = (entry: Record<string, any>, task: Task) => {
321 let taskImageSpec: string;
322 switch (task.name) {
323 case 'rbd/copy':
324 taskImageSpec = new ImageSpec(
325 task.metadata['dest_pool_name'],
326 task.metadata['dest_namespace'],
327 task.metadata['dest_image_name']
328 ).toString();
329 break;
330 case 'rbd/clone':
331 taskImageSpec = new ImageSpec(
332 task.metadata['child_pool_name'],
333 task.metadata['child_namespace'],
334 task.metadata['child_image_name']
335 ).toString();
336 break;
337 case 'rbd/create':
338 taskImageSpec = new ImageSpec(
339 task.metadata['pool_name'],
340 task.metadata['namespace'],
341 task.metadata['image_name']
342 ).toString();
343 break;
344 default:
345 taskImageSpec = task.metadata['image_spec'];
346 break;
347 }
348 return (
349 taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
350 );
351 };
352
353 const taskFilter = (task: Task) => {
354 return [
355 'rbd/clone',
356 'rbd/copy',
357 'rbd/create',
358 'rbd/delete',
359 'rbd/edit',
360 'rbd/flatten',
361 'rbd/trash/move'
362 ].includes(task.name);
363 };
364
365 this.taskListService.init(
366 (context) => this.getRbdImages(context),
367 (resp) => this.prepareResponse(resp),
368 (images) => (this.images = images),
369 () => this.onFetchError(),
370 taskFilter,
371 itemFilter,
372 this.builders
373 );
374 }
375
376 onFetchError() {
377 this.table.reset(); // Disable loading indicator.
378 this.tableStatus = new TableStatus('danger');
379 }
380
381 getRbdImages(context: CdTableFetchDataContext) {
382 if (context !== null) {
383 this.tableContext = context;
384 }
385 if (this.tableContext == null) {
386 this.tableContext = new CdTableFetchDataContext(() => undefined);
387 }
388 return this.rbdService.list(this.tableContext?.toParams());
389 }
390
391 prepareResponse(resp: any[]): any[] {
392 let images: any[] = [];
393
394 resp.forEach((pool) => {
395 images = images.concat(pool.value);
396 });
397
398 images.forEach((image) => {
399 if (image.schedule_info !== undefined) {
400 let scheduling: any[] = [];
401 const scheduleStatus = 'scheduled';
402 let nextSnapshotDate = +new Date(image.schedule_info.schedule_time);
403 const offset = new Date().getTimezoneOffset();
404 nextSnapshotDate = nextSnapshotDate + Math.abs(offset) * 60000;
405 scheduling.push(image.mirror_mode, scheduleStatus, nextSnapshotDate);
406 image.mirror_mode = scheduling;
407 scheduling = [];
408 }
409 });
410
411 if (images.length > 0) {
412 this.count = CdTableServerSideService.getCount(resp[0]);
413 } else {
414 this.count = 0;
415 }
416 return images;
417 }
418
419 updateSelection(selection: CdTableSelection) {
420 this.selection = selection;
421 }
422
423 deleteRbdModal() {
424 const poolName = this.selection.first().pool_name;
425 const namespace = this.selection.first().namespace;
426 const imageName = this.selection.first().name;
427 const imageSpec = new ImageSpec(poolName, namespace, imageName);
428
429 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
430 itemDescription: 'RBD',
431 itemNames: [imageSpec],
432 bodyTemplate: this.deleteTpl,
433 bodyContext: {
434 hasSnapshots: this.hasSnapshots(),
435 snapshots: this.listProtectedSnapshots()
436 },
437 submitActionObservable: () =>
438 this.taskWrapper.wrapTaskAroundCall({
439 task: new FinishedTask('rbd/delete', {
440 image_spec: imageSpec.toString()
441 }),
442 call: this.rbdService.delete(imageSpec)
443 })
444 });
445 }
446
447 resyncRbdModal() {
448 const poolName = this.selection.first().pool_name;
449 const namespace = this.selection.first().namespace;
450 const imageName = this.selection.first().name;
451 const imageSpec = new ImageSpec(poolName, namespace, imageName);
452
453 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
454 itemDescription: 'RBD',
455 itemNames: [imageSpec],
456 actionDescription: 'resync',
457 submitActionObservable: () =>
458 this.taskWrapper.wrapTaskAroundCall({
459 task: new FinishedTask('rbd/edit', {
460 image_spec: imageSpec.toString()
461 }),
462 call: this.rbdService.update(imageSpec, { resync: true })
463 })
464 });
465 }
466
467 trashRbdModal() {
468 const initialState = {
469 poolName: this.selection.first().pool_name,
470 namespace: this.selection.first().namespace,
471 imageName: this.selection.first().name,
472 hasSnapshots: this.hasSnapshots()
473 };
474 this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState);
475 }
476
477 flattenRbd(imageSpec: ImageSpec) {
478 this.taskWrapper
479 .wrapTaskAroundCall({
480 task: new FinishedTask('rbd/flatten', {
481 image_spec: imageSpec.toString()
482 }),
483 call: this.rbdService.flatten(imageSpec)
484 })
485 .subscribe({
486 complete: () => {
487 this.modalRef.close();
488 }
489 });
490 }
491
492 flattenRbdModal() {
493 const poolName = this.selection.first().pool_name;
494 const namespace = this.selection.first().namespace;
495 const imageName = this.selection.first().name;
496 const parent: RbdParentModel = this.selection.first().parent;
497 const parentImageSpec = new ImageSpec(
498 parent.pool_name,
499 parent.pool_namespace,
500 parent.image_name
501 );
502 const childImageSpec = new ImageSpec(poolName, namespace, imageName);
503
504 const initialState = {
505 titleText: 'RBD flatten',
506 buttonText: 'Flatten',
507 bodyTpl: this.flattenTpl,
508 bodyData: {
509 parent: `${parentImageSpec}@${parent.snap_name}`,
510 child: childImageSpec.toString()
511 },
512 onSubmit: () => {
513 this.flattenRbd(childImageSpec);
514 }
515 };
516
517 this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
518 }
519
520 editRequest() {
521 const request = new RbdFormEditRequestModel();
522 request.remove_scheduling = !request.remove_scheduling;
523 return request;
524 }
525
526 removeSchedulingModal() {
527 const imageName = this.selection.first().name;
528
529 const imageSpec = new ImageSpec(
530 this.selection.first().pool_name,
531 this.selection.first().namespace,
532 this.selection.first().name
533 );
534
535 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
536 actionDescription: 'remove scheduling on',
537 itemDescription: $localize`image`,
538 itemNames: [`${imageName}`],
539 submitActionObservable: () =>
540 new Observable((observer: Subscriber<any>) => {
541 this.taskWrapper
542 .wrapTaskAroundCall({
543 task: new FinishedTask('rbd/edit', {
544 image_spec: imageSpec.toString()
545 }),
546 call: this.rbdService.update(imageSpec, this.editRequest())
547 })
548 .subscribe({
549 error: (resp) => observer.error(resp),
550 complete: () => {
551 this.modalRef.close();
552 }
553 });
554 })
555 });
556 }
557
558 actionPrimary(primary: boolean) {
559 const request = new RbdFormEditRequestModel();
560 request.primary = primary;
561 request.features = null;
562 const imageSpec = new ImageSpec(
563 this.selection.first().pool_name,
564 this.selection.first().namespace,
565 this.selection.first().name
566 );
567 this.taskWrapper
568 .wrapTaskAroundCall({
569 task: new FinishedTask('rbd/edit', {
570 image_spec: imageSpec.toString()
571 }),
572 call: this.rbdService.update(imageSpec, request)
573 })
574 .subscribe();
575 }
576
577 hasSnapshots() {
578 const snapshots = this.selection.first()['snapshots'] || [];
579 return snapshots.length > 0;
580 }
581
582 hasClonedSnapshots(image: object) {
583 const snapshots = image['snapshots'] || [];
584 return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
585 }
586
587 listProtectedSnapshots() {
588 const first = this.selection.first();
589 const snapshots = first['snapshots'];
590 return snapshots.reduce((accumulator: string[], snap: object) => {
591 if (snap['is_protected']) {
592 accumulator.push(snap['name']);
593 }
594 return accumulator;
595 }, []);
596 }
597
598 getDeleteDisableDesc(selection: CdTableSelection): string | boolean {
599 const first = selection.first();
600
601 if (first && this.hasClonedSnapshots(first)) {
602 return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`;
603 }
604
605 return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
606 }
607
608 getResyncDisableDesc(selection: CdTableSelection): string | boolean {
609 const first = selection.first();
610
611 if (first && this.imageIsPrimary(first)) {
612 return $localize`Primary RBD images cannot be resynced`;
613 }
614
615 return this.getInvalidNameDisable(selection);
616 }
617
618 imageIsPrimary(image: object) {
619 return image['primary'];
620 }
621 getInvalidNameDisable(selection: CdTableSelection): string | boolean {
622 const first = selection.first();
623
624 if (first?.name?.match(/[@/]/)) {
625 return $localize`This RBD image has an invalid name and can't be managed by ceph.`;
626 }
627
628 return !selection.first() || !selection.hasSingleSelection;
629 }
630
631 getRemovingStatusDesc(selection: CdTableSelection): string | boolean {
632 const first = selection.first();
633 if (first?.source === 'REMOVING') {
634 return $localize`Action not possible for an RBD in status 'Removing'`;
635 }
636 return false;
637 }
638 }