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