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