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