]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
import ceph pacific 16.2.5
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-list / rbd-list.component.ts
CommitLineData
11fdf7f2
TL
1import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
2
f67539c2
TL
3import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
4import _ from 'lodash';
5
6import { RbdService } from '~/app/shared/api/rbd.service';
7import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
8import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
9import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
10import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
11import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
12import { TableComponent } from '~/app/shared/datatable/table/table.component';
f67539c2
TL
13import { Icons } from '~/app/shared/enum/icons.enum';
14import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
15import { CdTableAction } from '~/app/shared/models/cd-table-action';
16import { CdTableColumn } from '~/app/shared/models/cd-table-column';
17import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
18import { FinishedTask } from '~/app/shared/models/finished-task';
19import { ImageSpec } from '~/app/shared/models/image-spec';
20import { Permission } from '~/app/shared/models/permissions';
21import { Task } from '~/app/shared/models/task';
22import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
23import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
24import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
25import { ModalService } from '~/app/shared/services/modal.service';
26import { TaskListService } from '~/app/shared/services/task-list.service';
27import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
28import { URLBuilderService } from '~/app/shared/services/url-builder.service';
11fdf7f2
TL
29import { RbdParentModel } from '../rbd-form/rbd-parent.model';
30import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
f6b5b4d7 31import { RBDImageFormat, RbdModel } from './rbd-model';
11fdf7f2
TL
32
33const BASE_URL = 'block/rbd';
34
35@Component({
36 selector: 'cd-rbd-list',
37 templateUrl: './rbd-list.component.html',
38 styleUrls: ['./rbd-list.component.scss'],
39 providers: [
40 TaskListService,
41 { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
42 ]
43})
e306af50 44export class RbdListComponent extends ListWithDetails implements OnInit {
9f95a23c 45 @ViewChild(TableComponent, { static: true })
11fdf7f2 46 table: TableComponent;
f67539c2 47 @ViewChild('usageTpl')
11fdf7f2 48 usageTpl: TemplateRef<any>;
9f95a23c 49 @ViewChild('parentTpl', { static: true })
11fdf7f2 50 parentTpl: TemplateRef<any>;
f67539c2 51 @ViewChild('nameTpl')
11fdf7f2 52 nameTpl: TemplateRef<any>;
9f95a23c 53 @ViewChild('flattenTpl', { static: true })
11fdf7f2 54 flattenTpl: TemplateRef<any>;
9f95a23c
TL
55 @ViewChild('deleteTpl', { static: true })
56 deleteTpl: TemplateRef<any>;
b3b6e05e
TL
57 @ViewChild('removingStatTpl', { static: true })
58 removingStatTpl: TemplateRef<any>;
11fdf7f2
TL
59
60 permission: Permission;
61 tableActions: CdTableAction[];
62 images: any;
63 columns: CdTableColumn[];
64 retries: number;
f67539c2 65 tableStatus = new TableStatusViewCache();
11fdf7f2 66 selection = new CdTableSelection();
b3b6e05e 67 icons = Icons;
11fdf7f2 68
f67539c2 69 modalRef: NgbModalRef;
11fdf7f2
TL
70
71 builders = {
9f95a23c
TL
72 'rbd/create': (metadata: object) =>
73 this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
74 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
75 'rbd/clone': (metadata: object) =>
76 this.createRbdFromTask(
77 metadata['child_pool_name'],
78 metadata['child_namespace'],
79 metadata['child_image_name']
80 ),
81 'rbd/copy': (metadata: object) =>
82 this.createRbdFromTask(
83 metadata['dest_pool_name'],
84 metadata['dest_namespace'],
85 metadata['dest_image_name']
86 )
11fdf7f2
TL
87 };
88
9f95a23c
TL
89 private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
90 const imageSpec = ImageSpec.fromString(imageSpecStr);
91 return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
92 }
93
94 private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
11fdf7f2
TL
95 const model = new RbdModel();
96 model.id = '-1';
f6b5b4d7 97 model.unique_id = '-1';
11fdf7f2 98 model.name = name;
9f95a23c 99 model.namespace = namespace;
11fdf7f2 100 model.pool_name = pool;
f6b5b4d7 101 model.image_format = RBDImageFormat.V2;
11fdf7f2
TL
102 return model;
103 }
104
105 constructor(
106 private authStorageService: AuthStorageService,
107 private rbdService: RbdService,
108 private dimlessBinaryPipe: DimlessBinaryPipe,
109 private dimlessPipe: DimlessPipe,
f67539c2 110 private modalService: ModalService,
11fdf7f2 111 private taskWrapper: TaskWrapperService,
f67539c2 112 public taskListService: TaskListService,
11fdf7f2
TL
113 private urlBuilder: URLBuilderService,
114 public actionLabels: ActionLabelsI18n
115 ) {
e306af50 116 super();
11fdf7f2
TL
117 this.permission = this.authStorageService.getPermissions().rbdImage;
118 const getImageUri = () =>
119 this.selection.first() &&
9f95a23c
TL
120 new ImageSpec(
121 this.selection.first().pool_name,
122 this.selection.first().namespace,
11fdf7f2 123 this.selection.first().name
9f95a23c 124 ).toStringEncoded();
11fdf7f2
TL
125 const addAction: CdTableAction = {
126 permission: 'create',
9f95a23c 127 icon: Icons.add,
11fdf7f2
TL
128 routerLink: () => this.urlBuilder.getCreate(),
129 canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
130 name: this.actionLabels.CREATE
131 };
132 const editAction: CdTableAction = {
133 permission: 'update',
9f95a23c 134 icon: Icons.edit,
11fdf7f2 135 routerLink: () => this.urlBuilder.getEdit(getImageUri()),
f67539c2 136 name: this.actionLabels.EDIT,
b3b6e05e
TL
137 disable: (selection: CdTableSelection) =>
138 this.getRemovingStatusDesc(selection) || this.getInvalidNameDisable(selection)
11fdf7f2
TL
139 };
140 const deleteAction: CdTableAction = {
141 permission: 'delete',
9f95a23c 142 icon: Icons.destroy,
11fdf7f2 143 click: () => this.deleteRbdModal(),
9f95a23c 144 name: this.actionLabels.DELETE,
f91f0fd5 145 disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
11fdf7f2
TL
146 };
147 const copyAction: CdTableAction = {
148 permission: 'create',
149 canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
150 disable: (selection: CdTableSelection) =>
b3b6e05e
TL
151 this.getRemovingStatusDesc(selection) ||
152 this.getInvalidNameDisable(selection) ||
153 !!selection.first().cdExecuting,
9f95a23c 154 icon: Icons.copy,
11fdf7f2 155 routerLink: () => `/block/rbd/copy/${getImageUri()}`,
eafe8130 156 name: this.actionLabels.COPY
11fdf7f2
TL
157 };
158 const flattenAction: CdTableAction = {
159 permission: 'update',
160 disable: (selection: CdTableSelection) =>
b3b6e05e 161 this.getRemovingStatusDesc(selection) ||
f67539c2
TL
162 this.getInvalidNameDisable(selection) ||
163 selection.first().cdExecuting ||
164 !selection.first().parent,
9f95a23c 165 icon: Icons.flatten,
11fdf7f2 166 click: () => this.flattenRbdModal(),
eafe8130 167 name: this.actionLabels.FLATTEN
11fdf7f2
TL
168 };
169 const moveAction: CdTableAction = {
170 permission: 'delete',
9f95a23c 171 icon: Icons.trash,
11fdf7f2 172 click: () => this.trashRbdModal(),
f6b5b4d7
TL
173 name: this.actionLabels.TRASH,
174 disable: (selection: CdTableSelection) =>
b3b6e05e 175 this.getRemovingStatusDesc(selection) ||
f67539c2 176 this.getInvalidNameDisable(selection) ||
f6b5b4d7 177 selection.first().image_format === RBDImageFormat.V1
11fdf7f2
TL
178 };
179 this.tableActions = [
180 addAction,
181 editAction,
182 copyAction,
183 flattenAction,
184 deleteAction,
185 moveAction
186 ];
187 }
188
189 ngOnInit() {
190 this.columns = [
191 {
f67539c2 192 name: $localize`Name`,
11fdf7f2
TL
193 prop: 'name',
194 flexGrow: 2,
b3b6e05e 195 cellTemplate: this.removingStatTpl
11fdf7f2
TL
196 },
197 {
f67539c2 198 name: $localize`Pool`,
11fdf7f2
TL
199 prop: 'pool_name',
200 flexGrow: 2
201 },
9f95a23c 202 {
f67539c2 203 name: $localize`Namespace`,
9f95a23c
TL
204 prop: 'namespace',
205 flexGrow: 2
206 },
11fdf7f2 207 {
f67539c2 208 name: $localize`Size`,
11fdf7f2
TL
209 prop: 'size',
210 flexGrow: 1,
211 cellClass: 'text-right',
212 pipe: this.dimlessBinaryPipe
213 },
214 {
f67539c2 215 name: $localize`Objects`,
11fdf7f2
TL
216 prop: 'num_objs',
217 flexGrow: 1,
218 cellClass: 'text-right',
219 pipe: this.dimlessPipe
220 },
221 {
f67539c2 222 name: $localize`Object size`,
11fdf7f2
TL
223 prop: 'obj_size',
224 flexGrow: 1,
225 cellClass: 'text-right',
226 pipe: this.dimlessBinaryPipe
227 },
228 {
f67539c2 229 name: $localize`Provisioned`,
11fdf7f2
TL
230 prop: 'disk_usage',
231 cellClass: 'text-center',
232 flexGrow: 1,
233 pipe: this.dimlessBinaryPipe
234 },
235 {
f67539c2 236 name: $localize`Total provisioned`,
11fdf7f2
TL
237 prop: 'total_disk_usage',
238 cellClass: 'text-center',
239 flexGrow: 1,
240 pipe: this.dimlessBinaryPipe
241 },
242 {
f67539c2 243 name: $localize`Parent`,
11fdf7f2
TL
244 prop: 'parent',
245 flexGrow: 2,
246 cellTemplate: this.parentTpl
247 }
248 ];
249
9f95a23c
TL
250 const itemFilter = (entry: Record<string, any>, task: Task) => {
251 let taskImageSpec: string;
252 switch (task.name) {
253 case 'rbd/copy':
254 taskImageSpec = new ImageSpec(
255 task.metadata['dest_pool_name'],
256 task.metadata['dest_namespace'],
257 task.metadata['dest_image_name']
258 ).toString();
259 break;
260 case 'rbd/clone':
261 taskImageSpec = new ImageSpec(
262 task.metadata['child_pool_name'],
263 task.metadata['child_namespace'],
264 task.metadata['child_image_name']
265 ).toString();
266 break;
267 case 'rbd/create':
268 taskImageSpec = new ImageSpec(
269 task.metadata['pool_name'],
270 task.metadata['namespace'],
271 task.metadata['image_name']
272 ).toString();
273 break;
274 default:
275 taskImageSpec = task.metadata['image_spec'];
276 break;
277 }
278 return (
279 taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
280 );
281 };
282
283 const taskFilter = (task: Task) => {
284 return [
285 'rbd/clone',
286 'rbd/copy',
287 'rbd/create',
288 'rbd/delete',
289 'rbd/edit',
290 'rbd/flatten',
291 'rbd/trash/move'
292 ].includes(task.name);
293 };
294
11fdf7f2
TL
295 this.taskListService.init(
296 () => this.rbdService.list(),
297 (resp) => this.prepareResponse(resp),
298 (images) => (this.images = images),
299 () => this.onFetchError(),
9f95a23c
TL
300 taskFilter,
301 itemFilter,
11fdf7f2
TL
302 this.builders
303 );
304 }
305
306 onFetchError() {
307 this.table.reset(); // Disable loading indicator.
f67539c2 308 this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException);
11fdf7f2
TL
309 }
310
311 prepareResponse(resp: any[]): any[] {
9f95a23c 312 let images: any[] = [];
11fdf7f2 313 const viewCacheStatusMap = {};
f67539c2 314
11fdf7f2
TL
315 resp.forEach((pool) => {
316 if (_.isUndefined(viewCacheStatusMap[pool.status])) {
317 viewCacheStatusMap[pool.status] = [];
318 }
319 viewCacheStatusMap[pool.status].push(pool.pool_name);
320 images = images.concat(pool.value);
321 });
f67539c2
TL
322
323 let status: number;
324 if (viewCacheStatusMap[ViewCacheStatus.ValueException]) {
325 status = ViewCacheStatus.ValueException;
326 } else if (viewCacheStatusMap[ViewCacheStatus.ValueStale]) {
327 status = ViewCacheStatus.ValueStale;
328 } else if (viewCacheStatusMap[ViewCacheStatus.ValueNone]) {
329 status = ViewCacheStatus.ValueNone;
330 }
331
332 if (status) {
333 const statusFor =
334 (viewCacheStatusMap[status].length > 1 ? 'pools ' : 'pool ') +
335 viewCacheStatusMap[status].join();
336
337 this.tableStatus = new TableStatusViewCache(status, statusFor);
338 } else {
339 this.tableStatus = new TableStatusViewCache();
340 }
341
11fdf7f2
TL
342 return images;
343 }
344
11fdf7f2
TL
345 updateSelection(selection: CdTableSelection) {
346 this.selection = selection;
347 }
348
349 deleteRbdModal() {
350 const poolName = this.selection.first().pool_name;
9f95a23c 351 const namespace = this.selection.first().namespace;
11fdf7f2 352 const imageName = this.selection.first().name;
9f95a23c 353 const imageSpec = new ImageSpec(poolName, namespace, imageName);
11fdf7f2
TL
354
355 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
f67539c2
TL
356 itemDescription: 'RBD',
357 itemNames: [imageSpec],
358 bodyTemplate: this.deleteTpl,
359 bodyContext: {
360 hasSnapshots: this.hasSnapshots(),
361 snapshots: this.listProtectedSnapshots()
362 },
363 submitActionObservable: () =>
364 this.taskWrapper.wrapTaskAroundCall({
365 task: new FinishedTask('rbd/delete', {
366 image_spec: imageSpec.toString()
367 }),
368 call: this.rbdService.delete(imageSpec)
369 })
11fdf7f2
TL
370 });
371 }
372
373 trashRbdModal() {
374 const initialState = {
11fdf7f2 375 poolName: this.selection.first().pool_name,
9f95a23c
TL
376 namespace: this.selection.first().namespace,
377 imageName: this.selection.first().name,
378 hasSnapshots: this.hasSnapshots()
11fdf7f2 379 };
f67539c2 380 this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState);
11fdf7f2
TL
381 }
382
9f95a23c 383 flattenRbd(imageSpec: ImageSpec) {
11fdf7f2
TL
384 this.taskWrapper
385 .wrapTaskAroundCall({
386 task: new FinishedTask('rbd/flatten', {
9f95a23c 387 image_spec: imageSpec.toString()
11fdf7f2 388 }),
9f95a23c 389 call: this.rbdService.flatten(imageSpec)
11fdf7f2 390 })
f67539c2
TL
391 .subscribe({
392 complete: () => {
393 this.modalRef.close();
394 }
11fdf7f2
TL
395 });
396 }
397
398 flattenRbdModal() {
399 const poolName = this.selection.first().pool_name;
9f95a23c 400 const namespace = this.selection.first().namespace;
11fdf7f2
TL
401 const imageName = this.selection.first().name;
402 const parent: RbdParentModel = this.selection.first().parent;
9f95a23c
TL
403 const parentImageSpec = new ImageSpec(
404 parent.pool_name,
405 parent.pool_namespace,
406 parent.image_name
407 );
408 const childImageSpec = new ImageSpec(poolName, namespace, imageName);
11fdf7f2
TL
409
410 const initialState = {
411 titleText: 'RBD flatten',
412 buttonText: 'Flatten',
413 bodyTpl: this.flattenTpl,
414 bodyData: {
9f95a23c
TL
415 parent: `${parentImageSpec}@${parent.snap_name}`,
416 child: childImageSpec.toString()
11fdf7f2
TL
417 },
418 onSubmit: () => {
9f95a23c 419 this.flattenRbd(childImageSpec);
11fdf7f2
TL
420 }
421 };
422
f67539c2 423 this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
11fdf7f2 424 }
9f95a23c
TL
425
426 hasSnapshots() {
427 const snapshots = this.selection.first()['snapshots'] || [];
428 return snapshots.length > 0;
429 }
430
431 hasClonedSnapshots(image: object) {
432 const snapshots = image['snapshots'] || [];
433 return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
434 }
435
436 listProtectedSnapshots() {
437 const first = this.selection.first();
438 const snapshots = first['snapshots'];
439 return snapshots.reduce((accumulator: string[], snap: object) => {
440 if (snap['is_protected']) {
441 accumulator.push(snap['name']);
442 }
443 return accumulator;
444 }, []);
445 }
446
f91f0fd5
TL
447 getDeleteDisableDesc(selection: CdTableSelection): string | boolean {
448 const first = selection.first();
449
9f95a23c 450 if (first && this.hasClonedSnapshots(first)) {
f67539c2 451 return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`;
9f95a23c
TL
452 }
453
f67539c2
TL
454 return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
455 }
456
457 getInvalidNameDisable(selection: CdTableSelection): string | boolean {
458 const first = selection.first();
459
460 if (first?.name?.match(/[@/]/)) {
461 return $localize`This RBD image has an invalid name and can't be managed by ceph.`;
462 }
463
464 return !selection.first() || !selection.hasSingleSelection;
9f95a23c 465 }
b3b6e05e
TL
466
467 getRemovingStatusDesc(selection: CdTableSelection): string | boolean {
468 const first = selection.first();
469 if (first?.source === 'REMOVING') {
470 return $localize`Action not possible for an RBD in status 'Removing'`;
471 }
472 return false;
473 }
11fdf7f2 474}