summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts629
1 files changed, 629 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
new file mode 100644
index 000000000..a24e59f82
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
@@ -0,0 +1,629 @@
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Observable, Subscriber } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { TableStatus } from '~/app/shared/classes/table-status';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { Permission } from '~/app/shared/models/permissions';
+import { Task } from '~/app/shared/models/task';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model';
+import { RbdParentModel } from '../rbd-form/rbd-parent.model';
+import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
+import { RBDImageFormat, RbdModel } from './rbd-model';
+
+const BASE_URL = 'block/rbd';
+
+@Component({
+ selector: 'cd-rbd-list',
+ templateUrl: './rbd-list.component.html',
+ styleUrls: ['./rbd-list.component.scss'],
+ providers: [
+ TaskListService,
+ { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
+ ]
+})
+export class RbdListComponent extends ListWithDetails implements OnInit {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('usageTpl')
+ usageTpl: TemplateRef<any>;
+ @ViewChild('parentTpl', { static: true })
+ parentTpl: TemplateRef<any>;
+ @ViewChild('nameTpl')
+ nameTpl: TemplateRef<any>;
+ @ViewChild('mirroringTpl', { static: true })
+ mirroringTpl: TemplateRef<any>;
+ @ViewChild('flattenTpl', { static: true })
+ flattenTpl: TemplateRef<any>;
+ @ViewChild('deleteTpl', { static: true })
+ deleteTpl: TemplateRef<any>;
+ @ViewChild('removingStatTpl', { static: true })
+ removingStatTpl: TemplateRef<any>;
+ @ViewChild('provisionedNotAvailableTooltipTpl', { static: true })
+ provisionedNotAvailableTooltipTpl: TemplateRef<any>;
+ @ViewChild('totalProvisionedNotAvailableTooltipTpl', { static: true })
+ totalProvisionedNotAvailableTooltipTpl: TemplateRef<any>;
+
+ permission: Permission;
+ tableActions: CdTableAction[];
+ images: any;
+ columns: CdTableColumn[];
+ retries: number;
+ tableStatus = new TableStatus('light');
+ selection = new CdTableSelection();
+ icons = Icons;
+ count = 0;
+ private tableContext: CdTableFetchDataContext = null;
+ modalRef: NgbModalRef;
+
+ builders = {
+ 'rbd/create': (metadata: object) =>
+ this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
+ 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
+ 'rbd/clone': (metadata: object) =>
+ this.createRbdFromTask(
+ metadata['child_pool_name'],
+ metadata['child_namespace'],
+ metadata['child_image_name']
+ ),
+ 'rbd/copy': (metadata: object) =>
+ this.createRbdFromTask(
+ metadata['dest_pool_name'],
+ metadata['dest_namespace'],
+ metadata['dest_image_name']
+ )
+ };
+ remove_scheduling: boolean;
+
+ private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
+ const imageSpec = ImageSpec.fromString(imageSpecStr);
+ return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
+ }
+
+ private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
+ const model = new RbdModel();
+ model.id = '-1';
+ model.unique_id = '-1';
+ model.name = name;
+ model.namespace = namespace;
+ model.pool_name = pool;
+ model.image_format = RBDImageFormat.V2;
+ return model;
+ }
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ private rbdService: RbdService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private dimlessPipe: DimlessPipe,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService,
+ public taskListService: TaskListService,
+ private urlBuilder: URLBuilderService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().rbdImage;
+ const getImageUri = () =>
+ this.selection.first() &&
+ new ImageSpec(
+ this.selection.first().pool_name,
+ this.selection.first().namespace,
+ this.selection.first().name
+ ).toStringEncoded();
+ const addAction: CdTableAction = {
+ permission: 'create',
+ icon: Icons.add,
+ routerLink: () => this.urlBuilder.getCreate(),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
+ name: this.actionLabels.CREATE
+ };
+ const editAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ routerLink: () => this.urlBuilder.getEdit(getImageUri()),
+ name: this.actionLabels.EDIT,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection)
+ };
+ const deleteAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteRbdModal(),
+ name: this.actionLabels.DELETE,
+ disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
+ };
+ const resyncAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.refresh,
+ click: () => this.resyncRbdModal(),
+ name: this.actionLabels.RESYNC,
+ disable: (selection: CdTableSelection) => this.getResyncDisableDesc(selection)
+ };
+ const copyAction: CdTableAction = {
+ permission: 'create',
+ canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ !!selection.first().cdExecuting,
+ icon: Icons.copy,
+ routerLink: () => `/block/rbd/copy/${getImageUri()}`,
+ name: this.actionLabels.COPY
+ };
+ const flattenAction: CdTableAction = {
+ permission: 'update',
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ selection.first().cdExecuting ||
+ !selection.first().parent,
+ icon: Icons.flatten,
+ click: () => this.flattenRbdModal(),
+ name: this.actionLabels.FLATTEN
+ };
+ const moveAction: CdTableAction = {
+ permission: 'delete',
+ icon: Icons.trash,
+ click: () => this.trashRbdModal(),
+ name: this.actionLabels.TRASH,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ selection.first().image_format === RBDImageFormat.V1
+ };
+ const removeSchedulingAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.removeSchedulingModal(),
+ name: this.actionLabels.REMOVE_SCHEDULING,
+ disable: (selection: CdTableSelection) =>
+ this.getRemovingStatusDesc(selection) ||
+ this.getInvalidNameDisable(selection) ||
+ selection.first().schedule_info === undefined
+ };
+ const promoteAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.actionPrimary(true),
+ name: this.actionLabels.PROMOTE,
+ visible: () => this.selection.first() != null && !this.selection.first().primary
+ };
+ const demoteAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.actionPrimary(false),
+ name: this.actionLabels.DEMOTE,
+ visible: () => this.selection.first() != null && this.selection.first().primary
+ };
+ this.tableActions = [
+ addAction,
+ editAction,
+ copyAction,
+ flattenAction,
+ resyncAction,
+ deleteAction,
+ moveAction,
+ removeSchedulingAction,
+ promoteAction,
+ demoteAction
+ ];
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 2,
+ cellTemplate: this.removingStatTpl
+ },
+ {
+ name: $localize`Pool`,
+ prop: 'pool_name',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Namespace`,
+ prop: 'namespace',
+ flexGrow: 2
+ },
+ {
+ name: $localize`Size`,
+ prop: 'size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ sortable: false,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`Objects`,
+ prop: 'num_objs',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ sortable: false,
+ pipe: this.dimlessPipe
+ },
+ {
+ name: $localize`Object size`,
+ prop: 'obj_size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ sortable: false,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: $localize`Provisioned`,
+ prop: 'disk_usage',
+ cellClass: 'text-center',
+ flexGrow: 1,
+ pipe: this.dimlessBinaryPipe,
+ sortable: false,
+ cellTemplate: this.provisionedNotAvailableTooltipTpl
+ },
+ {
+ name: $localize`Total provisioned`,
+ prop: 'total_disk_usage',
+ cellClass: 'text-center',
+ flexGrow: 1,
+ pipe: this.dimlessBinaryPipe,
+ sortable: false,
+ cellTemplate: this.totalProvisionedNotAvailableTooltipTpl
+ },
+ {
+ name: $localize`Parent`,
+ prop: 'parent',
+ flexGrow: 2,
+ sortable: false,
+ cellTemplate: this.parentTpl
+ },
+ {
+ name: $localize`Mirroring`,
+ prop: 'mirror_mode',
+ flexGrow: 3,
+ sortable: false,
+ cellTemplate: this.mirroringTpl
+ }
+ ];
+
+ const itemFilter = (entry: Record<string, any>, task: Task) => {
+ let taskImageSpec: string;
+ switch (task.name) {
+ case 'rbd/copy':
+ taskImageSpec = new ImageSpec(
+ task.metadata['dest_pool_name'],
+ task.metadata['dest_namespace'],
+ task.metadata['dest_image_name']
+ ).toString();
+ break;
+ case 'rbd/clone':
+ taskImageSpec = new ImageSpec(
+ task.metadata['child_pool_name'],
+ task.metadata['child_namespace'],
+ task.metadata['child_image_name']
+ ).toString();
+ break;
+ case 'rbd/create':
+ taskImageSpec = new ImageSpec(
+ task.metadata['pool_name'],
+ task.metadata['namespace'],
+ task.metadata['image_name']
+ ).toString();
+ break;
+ default:
+ taskImageSpec = task.metadata['image_spec'];
+ break;
+ }
+ return (
+ taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
+ );
+ };
+
+ const taskFilter = (task: Task) => {
+ return [
+ 'rbd/clone',
+ 'rbd/copy',
+ 'rbd/create',
+ 'rbd/delete',
+ 'rbd/edit',
+ 'rbd/flatten',
+ 'rbd/trash/move'
+ ].includes(task.name);
+ };
+
+ this.taskListService.init(
+ (context) => this.getRbdImages(context),
+ (resp) => this.prepareResponse(resp),
+ (images) => (this.images = images),
+ () => this.onFetchError(),
+ taskFilter,
+ itemFilter,
+ this.builders
+ );
+ }
+
+ onFetchError() {
+ this.table.reset(); // Disable loading indicator.
+ this.tableStatus = new TableStatus('danger');
+ }
+
+ getRbdImages(context: CdTableFetchDataContext) {
+ if (context !== null) {
+ this.tableContext = context;
+ }
+ if (this.tableContext == null) {
+ this.tableContext = new CdTableFetchDataContext(() => undefined);
+ }
+ return this.rbdService.list(this.tableContext?.toParams());
+ }
+
+ prepareResponse(resp: any[]): any[] {
+ let images: any[] = [];
+
+ resp.forEach((pool) => {
+ images = images.concat(pool.value);
+ });
+
+ images.forEach((image) => {
+ if (image.schedule_info !== undefined) {
+ let scheduling: any[] = [];
+ const scheduleStatus = 'scheduled';
+ let nextSnapshotDate = +new Date(image.schedule_info.schedule_time);
+ const offset = new Date().getTimezoneOffset();
+ nextSnapshotDate = nextSnapshotDate + Math.abs(offset) * 60000;
+ scheduling.push(image.mirror_mode, scheduleStatus, nextSnapshotDate);
+ image.mirror_mode = scheduling;
+ scheduling = [];
+ }
+ });
+
+ if (images.length > 0) {
+ this.count = CdTableServerSideService.getCount(resp[0]);
+ } else {
+ this.count = 0;
+ }
+ return images;
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageName = this.selection.first().name;
+ const imageSpec = new ImageSpec(poolName, namespace, imageName);
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'RBD',
+ itemNames: [imageSpec],
+ bodyTemplate: this.deleteTpl,
+ bodyContext: {
+ hasSnapshots: this.hasSnapshots(),
+ snapshots: this.listProtectedSnapshots()
+ },
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/delete', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.delete(imageSpec)
+ })
+ });
+ }
+
+ resyncRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageName = this.selection.first().name;
+ const imageSpec = new ImageSpec(poolName, namespace, imageName);
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'RBD',
+ itemNames: [imageSpec],
+ actionDescription: 'resync',
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, { resync: true })
+ })
+ });
+ }
+
+ trashRbdModal() {
+ const initialState = {
+ poolName: this.selection.first().pool_name,
+ namespace: this.selection.first().namespace,
+ imageName: this.selection.first().name,
+ hasSnapshots: this.hasSnapshots()
+ };
+ this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState);
+ }
+
+ flattenRbd(imageSpec: ImageSpec) {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/flatten', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.flatten(imageSpec)
+ })
+ .subscribe({
+ complete: () => {
+ this.modalRef.close();
+ }
+ });
+ }
+
+ flattenRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const namespace = this.selection.first().namespace;
+ const imageName = this.selection.first().name;
+ const parent: RbdParentModel = this.selection.first().parent;
+ const parentImageSpec = new ImageSpec(
+ parent.pool_name,
+ parent.pool_namespace,
+ parent.image_name
+ );
+ const childImageSpec = new ImageSpec(poolName, namespace, imageName);
+
+ const initialState = {
+ titleText: 'RBD flatten',
+ buttonText: 'Flatten',
+ bodyTpl: this.flattenTpl,
+ bodyData: {
+ parent: `${parentImageSpec}@${parent.snap_name}`,
+ child: childImageSpec.toString()
+ },
+ onSubmit: () => {
+ this.flattenRbd(childImageSpec);
+ }
+ };
+
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
+ }
+
+ editRequest() {
+ const request = new RbdFormEditRequestModel();
+ request.remove_scheduling = !request.remove_scheduling;
+ return request;
+ }
+
+ removeSchedulingModal() {
+ const imageName = this.selection.first().name;
+
+ const imageSpec = new ImageSpec(
+ this.selection.first().pool_name,
+ this.selection.first().namespace,
+ this.selection.first().name
+ );
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ actionDescription: 'remove scheduling on',
+ itemDescription: $localize`image`,
+ itemNames: [`${imageName}`],
+ submitActionObservable: () =>
+ new Observable((observer: Subscriber<any>) => {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, this.editRequest())
+ })
+ .subscribe({
+ error: (resp) => observer.error(resp),
+ complete: () => {
+ this.modalRef.close();
+ }
+ });
+ })
+ });
+ }
+
+ actionPrimary(primary: boolean) {
+ const request = new RbdFormEditRequestModel();
+ request.primary = primary;
+ request.features = null;
+ const imageSpec = new ImageSpec(
+ this.selection.first().pool_name,
+ this.selection.first().namespace,
+ this.selection.first().name
+ );
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('rbd/edit', {
+ image_spec: imageSpec.toString()
+ }),
+ call: this.rbdService.update(imageSpec, request)
+ })
+ .subscribe();
+ }
+
+ hasSnapshots() {
+ const snapshots = this.selection.first()['snapshots'] || [];
+ return snapshots.length > 0;
+ }
+
+ hasClonedSnapshots(image: object) {
+ const snapshots = image['snapshots'] || [];
+ return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
+ }
+
+ listProtectedSnapshots() {
+ const first = this.selection.first();
+ const snapshots = first['snapshots'];
+ return snapshots.reduce((accumulator: string[], snap: object) => {
+ if (snap['is_protected']) {
+ accumulator.push(snap['name']);
+ }
+ return accumulator;
+ }, []);
+ }
+
+ getDeleteDisableDesc(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+
+ if (first && this.hasClonedSnapshots(first)) {
+ return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`;
+ }
+
+ return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
+ }
+
+ getResyncDisableDesc(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+
+ if (first && this.imageIsPrimary(first)) {
+ return $localize`Primary RBD images cannot be resynced`;
+ }
+
+ return this.getInvalidNameDisable(selection);
+ }
+
+ imageIsPrimary(image: object) {
+ return image['primary'];
+ }
+ getInvalidNameDisable(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+
+ if (first?.name?.match(/[@/]/)) {
+ return $localize`This RBD image has an invalid name and can't be managed by ceph.`;
+ }
+
+ return !selection.first() || !selection.hasSingleSelection;
+ }
+
+ getRemovingStatusDesc(selection: CdTableSelection): string | boolean {
+ const first = selection.first();
+ if (first?.source === 'REMOVING') {
+ return $localize`Action not possible for an RBD in status 'Removing'`;
+ }
+ return false;
+ }
+}