1 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
3 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
4 import _ from 'lodash';
5 import { Observable, Subscriber } from 'rxjs';
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';
36 const BASE_URL = 'block/rbd';
39 selector: 'cd-rbd-list',
40 templateUrl: './rbd-list.component.html',
41 styleUrls: ['./rbd-list.component.scss'],
44 { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
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>;
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>;
71 permission: Permission;
72 tableActions: CdTableAction[];
74 columns: CdTableColumn[];
76 tableStatus = new TableStatus('light');
77 selection = new CdTableSelection();
80 private tableContext: CdTableFetchDataContext = null;
81 modalRef: NgbModalRef;
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']
93 'rbd/copy': (metadata: object) =>
94 this.createRbdFromTask(
95 metadata['dest_pool_name'],
96 metadata['dest_namespace'],
97 metadata['dest_image_name']
100 remove_scheduling: boolean;
102 private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
103 const imageSpec = ImageSpec.fromString(imageSpecStr);
104 return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
107 private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
108 const model = new RbdModel();
110 model.unique_id = '-1';
112 model.namespace = namespace;
113 model.pool_name = pool;
114 model.image_format = RBDImageFormat.V2;
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
130 this.permission = this.authStorageService.getPermissions().rbdImage;
131 const getImageUri = () =>
132 this.selection.first() &&
134 this.selection.first().pool_name,
135 this.selection.first().namespace,
136 this.selection.first().name
138 const addAction: CdTableAction = {
139 permission: 'create',
141 routerLink: () => this.urlBuilder.getCreate(),
142 canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
143 name: this.actionLabels.CREATE
145 const editAction: CdTableAction = {
146 permission: 'update',
148 routerLink: () => this.urlBuilder.getEdit(getImageUri()),
149 name: this.actionLabels.EDIT,
150 disable: (selection: CdTableSelection) =>
151 this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection)
153 const deleteAction: CdTableAction = {
154 permission: 'delete',
156 click: () => this.deleteRbdModal(),
157 name: this.actionLabels.DELETE,
158 disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
160 const resyncAction: CdTableAction = {
161 permission: 'update',
163 click: () => this.resyncRbdModal(),
164 name: this.actionLabels.RESYNC,
165 disable: (selection: CdTableSelection) => this.getResyncDisableDesc(selection)
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,
175 routerLink: () => `/block/rbd/copy/${getImageUri()}`,
176 name: this.actionLabels.COPY
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,
186 click: () => this.flattenRbdModal(),
187 name: this.actionLabels.FLATTEN
189 const moveAction: CdTableAction = {
190 permission: 'delete',
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
199 const removeSchedulingAction: CdTableAction = {
200 permission: 'update',
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
209 const promoteAction: CdTableAction = {
210 permission: 'update',
212 click: () => this.actionPrimary(true),
213 name: this.actionLabels.PROMOTE,
214 visible: () => this.selection.first() != null && !this.selection.first().primary
216 const demoteAction: CdTableAction = {
217 permission: 'update',
219 click: () => this.actionPrimary(false),
220 name: this.actionLabels.DEMOTE,
221 visible: () => this.selection.first() != null && this.selection.first().primary
223 this.tableActions = [
231 removeSchedulingAction,
240 name: $localize`Name`,
243 cellTemplate: this.removingStatTpl
246 name: $localize`Pool`,
251 name: $localize`Namespace`,
256 name: $localize`Size`,
259 cellClass: 'text-right',
261 pipe: this.dimlessBinaryPipe
264 name: $localize`Objects`,
267 cellClass: 'text-right',
269 pipe: this.dimlessPipe
272 name: $localize`Object size`,
275 cellClass: 'text-right',
277 pipe: this.dimlessBinaryPipe
280 name: $localize`Provisioned`,
282 cellClass: 'text-center',
284 pipe: this.dimlessBinaryPipe,
286 cellTemplate: this.provisionedNotAvailableTooltipTpl
289 name: $localize`Total provisioned`,
290 prop: 'total_disk_usage',
291 cellClass: 'text-center',
293 pipe: this.dimlessBinaryPipe,
295 cellTemplate: this.totalProvisionedNotAvailableTooltipTpl
298 name: $localize`Parent`,
302 cellTemplate: this.parentTpl
305 name: $localize`Mirroring`,
309 cellTemplate: this.mirroringTpl
312 name: $localize`Next Scheduled Snapshot`,
316 cellTemplate: this.scheduleTpl
320 const itemFilter = (entry: Record<string, any>, task: Task) => {
321 let taskImageSpec: string;
324 taskImageSpec = new ImageSpec(
325 task.metadata['dest_pool_name'],
326 task.metadata['dest_namespace'],
327 task.metadata['dest_image_name']
331 taskImageSpec = new ImageSpec(
332 task.metadata['child_pool_name'],
333 task.metadata['child_namespace'],
334 task.metadata['child_image_name']
338 taskImageSpec = new ImageSpec(
339 task.metadata['pool_name'],
340 task.metadata['namespace'],
341 task.metadata['image_name']
345 taskImageSpec = task.metadata['image_spec'];
349 taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
353 const taskFilter = (task: Task) => {
362 ].includes(task.name);
365 this.taskListService.init(
366 (context) => this.getRbdImages(context),
367 (resp) => this.prepareResponse(resp),
368 (images) => (this.images = images),
369 () => this.onFetchError(),
377 this.table.reset(); // Disable loading indicator.
378 this.tableStatus = new TableStatus('danger');
381 getRbdImages(context: CdTableFetchDataContext) {
382 if (context !== null) {
383 this.tableContext = context;
385 if (this.tableContext == null) {
386 this.tableContext = new CdTableFetchDataContext(() => undefined);
388 return this.rbdService.list(this.tableContext?.toParams());
391 prepareResponse(resp: any[]): any[] {
392 let images: any[] = [];
394 resp.forEach((pool) => {
395 images = images.concat(pool.value);
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;
411 if (images.length > 0) {
412 this.count = CdTableServerSideService.getCount(resp[0]);
419 updateSelection(selection: CdTableSelection) {
420 this.selection = selection;
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);
429 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
430 itemDescription: 'RBD',
431 itemNames: [imageSpec],
432 bodyTemplate: this.deleteTpl,
434 hasSnapshots: this.hasSnapshots(),
435 snapshots: this.listProtectedSnapshots()
437 submitActionObservable: () =>
438 this.taskWrapper.wrapTaskAroundCall({
439 task: new FinishedTask('rbd/delete', {
440 image_spec: imageSpec.toString()
442 call: this.rbdService.delete(imageSpec)
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);
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()
462 call: this.rbdService.update(imageSpec, { resync: true })
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()
474 this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState);
477 flattenRbd(imageSpec: ImageSpec) {
479 .wrapTaskAroundCall({
480 task: new FinishedTask('rbd/flatten', {
481 image_spec: imageSpec.toString()
483 call: this.rbdService.flatten(imageSpec)
487 this.modalRef.close();
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(
499 parent.pool_namespace,
502 const childImageSpec = new ImageSpec(poolName, namespace, imageName);
504 const initialState = {
505 titleText: 'RBD flatten',
506 buttonText: 'Flatten',
507 bodyTpl: this.flattenTpl,
509 parent: `${parentImageSpec}@${parent.snap_name}`,
510 child: childImageSpec.toString()
513 this.flattenRbd(childImageSpec);
517 this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
521 const request = new RbdFormEditRequestModel();
522 request.remove_scheduling = !request.remove_scheduling;
526 removeSchedulingModal() {
527 const imageName = this.selection.first().name;
529 const imageSpec = new ImageSpec(
530 this.selection.first().pool_name,
531 this.selection.first().namespace,
532 this.selection.first().name
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>) => {
542 .wrapTaskAroundCall({
543 task: new FinishedTask('rbd/edit', {
544 image_spec: imageSpec.toString()
546 call: this.rbdService.update(imageSpec, this.editRequest())
549 error: (resp) => observer.error(resp),
551 this.modalRef.close();
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
568 .wrapTaskAroundCall({
569 task: new FinishedTask('rbd/edit', {
570 image_spec: imageSpec.toString()
572 call: this.rbdService.update(imageSpec, request)
578 const snapshots = this.selection.first()['snapshots'] || [];
579 return snapshots.length > 0;
582 hasClonedSnapshots(image: object) {
583 const snapshots = image['snapshots'] || [];
584 return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
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']);
598 getDeleteDisableDesc(selection: CdTableSelection): string | boolean {
599 const first = selection.first();
601 if (first && this.hasClonedSnapshots(first)) {
602 return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`;
605 return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
608 getResyncDisableDesc(selection: CdTableSelection): string | boolean {
609 const first = selection.first();
611 if (first && this.imageIsPrimary(first)) {
612 return $localize`Primary RBD images cannot be resynced`;
615 return this.getInvalidNameDisable(selection);
618 imageIsPrimary(image: object) {
619 return image['primary'];
621 getInvalidNameDisable(selection: CdTableSelection): string | boolean {
622 const first = selection.first();
624 if (first?.name?.match(/[@/]/)) {
625 return $localize`This RBD image has an invalid name and can't be managed by ceph.`;
628 return !selection.first() || !selection.hasSingleSelection;
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'`;