]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
bump version to 15.2.8-pve2
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / osd / osd-list / osd-list.component.ts
CommitLineData
11fdf7f2 1import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
f6b5b4d7 2import { FormControl } from '@angular/forms';
9f95a23c 3import { Router } from '@angular/router';
11fdf7f2
TL
4
5import { I18n } from '@ngx-translate/i18n-polyfill';
9f95a23c 6import * as _ from 'lodash';
11fdf7f2 7import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
9f95a23c 8import { forkJoin as observableForkJoin, Observable } from 'rxjs';
11fdf7f2
TL
9
10import { OsdService } from '../../../../shared/api/osd.service';
e306af50 11import { ListWithDetails } from '../../../../shared/classes/list-with-details.class';
11fdf7f2
TL
12import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
13import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
9f95a23c
TL
14import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
15import { ActionLabelsI18n, URLVerbs } from '../../../../shared/constants/app.constants';
11fdf7f2
TL
16import { TableComponent } from '../../../../shared/datatable/table/table.component';
17import { CellTemplate } from '../../../../shared/enum/cell-template.enum';
9f95a23c
TL
18import { Icons } from '../../../../shared/enum/icons.enum';
19import { NotificationType } from '../../../../shared/enum/notification-type.enum';
f6b5b4d7 20import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
11fdf7f2
TL
21import { CdTableAction } from '../../../../shared/models/cd-table-action';
22import { CdTableColumn } from '../../../../shared/models/cd-table-column';
23import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
9f95a23c 24import { FinishedTask } from '../../../../shared/models/finished-task';
11fdf7f2
TL
25import { Permissions } from '../../../../shared/models/permissions';
26import { DimlessBinaryPipe } from '../../../../shared/pipes/dimless-binary.pipe';
27import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
9f95a23c
TL
28import { DepCheckerService } from '../../../../shared/services/dep-checker.service';
29import { NotificationService } from '../../../../shared/services/notification.service';
30import { TaskWrapperService } from '../../../../shared/services/task-wrapper.service';
31import { URLBuilderService } from '../../../../shared/services/url-builder.service';
11fdf7f2 32import { OsdFlagsModalComponent } from '../osd-flags-modal/osd-flags-modal.component';
81eedcae 33import { OsdPgScrubModalComponent } from '../osd-pg-scrub-modal/osd-pg-scrub-modal.component';
11fdf7f2
TL
34import { OsdRecvSpeedModalComponent } from '../osd-recv-speed-modal/osd-recv-speed-modal.component';
35import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
36import { OsdScrubModalComponent } from '../osd-scrub-modal/osd-scrub-modal.component';
37
9f95a23c
TL
38const BASE_URL = 'osd';
39
11fdf7f2
TL
40@Component({
41 selector: 'cd-osd-list',
42 templateUrl: './osd-list.component.html',
9f95a23c
TL
43 styleUrls: ['./osd-list.component.scss'],
44 providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
11fdf7f2 45})
e306af50 46export class OsdListComponent extends ListWithDetails implements OnInit {
9f95a23c 47 @ViewChild('osdUsageTpl', { static: true })
11fdf7f2 48 osdUsageTpl: TemplateRef<any>;
9f95a23c 49 @ViewChild('markOsdConfirmationTpl', { static: true })
11fdf7f2 50 markOsdConfirmationTpl: TemplateRef<any>;
9f95a23c 51 @ViewChild('criticalConfirmationTpl', { static: true })
11fdf7f2 52 criticalConfirmationTpl: TemplateRef<any>;
9f95a23c 53 @ViewChild(TableComponent, { static: true })
11fdf7f2 54 tableComponent: TableComponent;
9f95a23c 55 @ViewChild('reweightBodyTpl', { static: false })
11fdf7f2 56 reweightBodyTpl: TemplateRef<any>;
9f95a23c 57 @ViewChild('safeToDestroyBodyTpl', { static: false })
11fdf7f2 58 safeToDestroyBodyTpl: TemplateRef<any>;
f6b5b4d7
TL
59 @ViewChild('deleteOsdExtraTpl', { static: false })
60 deleteOsdExtraTpl: TemplateRef<any>;
11fdf7f2
TL
61
62 permissions: Permissions;
63 tableActions: CdTableAction[];
64 bsModalRef: BsModalRef;
65 columns: CdTableColumn[];
9f95a23c
TL
66 clusterWideActions: CdTableAction[];
67 icons = Icons;
11fdf7f2 68
11fdf7f2 69 selection = new CdTableSelection();
9f95a23c 70 osds: any[] = [];
11fdf7f2 71
9f95a23c
TL
72 protected static collectStates(osd: any) {
73 const states = [osd['in'] ? 'in' : 'out'];
74 if (osd['up']) {
75 states.push('up');
76 } else if (osd.state.includes('destroyed')) {
77 states.push('destroyed');
78 } else {
79 states.push('down');
80 }
81 return states;
11fdf7f2
TL
82 }
83
84 constructor(
85 private authStorageService: AuthStorageService,
86 private osdService: OsdService,
87 private dimlessBinaryPipe: DimlessBinaryPipe,
88 private modalService: BsModalService,
eafe8130 89 private i18n: I18n,
9f95a23c
TL
90 private urlBuilder: URLBuilderService,
91 private router: Router,
92 private depCheckerService: DepCheckerService,
93 private taskWrapper: TaskWrapperService,
94 public actionLabels: ActionLabelsI18n,
95 public notificationService: NotificationService
11fdf7f2 96 ) {
e306af50 97 super();
11fdf7f2
TL
98 this.permissions = this.authStorageService.getPermissions();
99 this.tableActions = [
9f95a23c
TL
100 {
101 name: this.actionLabels.CREATE,
102 permission: 'create',
103 icon: Icons.add,
104 click: () => {
105 this.depCheckerService.checkOrchestratorOrModal(
106 this.actionLabels.CREATE,
107 this.i18n('OSD'),
108 () => {
109 this.router.navigate([this.urlBuilder.getCreate()]);
110 }
111 );
112 },
113 canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
114 },
115 {
116 name: this.actionLabels.EDIT,
117 permission: 'update',
118 icon: Icons.edit,
119 click: () => this.editAction()
120 },
11fdf7f2 121 {
eafe8130 122 name: this.actionLabels.SCRUB,
11fdf7f2 123 permission: 'update',
9f95a23c 124 icon: Icons.analyse,
11fdf7f2 125 click: () => this.scrubAction(false),
9f95a23c
TL
126 disable: () => !this.hasOsdSelected,
127 canBePrimary: (selection: CdTableSelection) => selection.hasSelection
11fdf7f2
TL
128 },
129 {
eafe8130 130 name: this.actionLabels.DEEP_SCRUB,
11fdf7f2 131 permission: 'update',
9f95a23c 132 icon: Icons.deepCheck,
11fdf7f2
TL
133 click: () => this.scrubAction(true),
134 disable: () => !this.hasOsdSelected
135 },
136 {
eafe8130 137 name: this.actionLabels.REWEIGHT,
11fdf7f2
TL
138 permission: 'update',
139 click: () => this.reweight(),
9f95a23c
TL
140 disable: () => !this.hasOsdSelected || !this.selection.hasSingleSelection,
141 icon: Icons.reweight
11fdf7f2
TL
142 },
143 {
eafe8130 144 name: this.actionLabels.MARK_OUT,
11fdf7f2
TL
145 permission: 'update',
146 click: () => this.showConfirmationModal(this.i18n('out'), this.osdService.markOut),
147 disable: () => this.isNotSelectedOrInState('out'),
9f95a23c 148 icon: Icons.left
11fdf7f2
TL
149 },
150 {
eafe8130 151 name: this.actionLabels.MARK_IN,
11fdf7f2
TL
152 permission: 'update',
153 click: () => this.showConfirmationModal(this.i18n('in'), this.osdService.markIn),
154 disable: () => this.isNotSelectedOrInState('in'),
9f95a23c 155 icon: Icons.right
11fdf7f2
TL
156 },
157 {
eafe8130 158 name: this.actionLabels.MARK_DOWN,
11fdf7f2
TL
159 permission: 'update',
160 click: () => this.showConfirmationModal(this.i18n('down'), this.osdService.markDown),
161 disable: () => this.isNotSelectedOrInState('down'),
9f95a23c 162 icon: Icons.down
11fdf7f2
TL
163 },
164 {
eafe8130 165 name: this.actionLabels.MARK_LOST,
11fdf7f2
TL
166 permission: 'delete',
167 click: () =>
168 this.showCriticalConfirmationModal(
169 this.i18n('Mark'),
170 this.i18n('OSD lost'),
171 this.i18n('marked lost'),
9f95a23c
TL
172 (ids: number[]) => {
173 return this.osdService.safeToDestroy(JSON.stringify(ids));
174 },
175 'is_safe_to_destroy',
11fdf7f2
TL
176 this.osdService.markLost
177 ),
178 disable: () => this.isNotSelectedOrInState('up'),
9f95a23c 179 icon: Icons.flatten
11fdf7f2
TL
180 },
181 {
eafe8130 182 name: this.actionLabels.PURGE,
11fdf7f2
TL
183 permission: 'delete',
184 click: () =>
185 this.showCriticalConfirmationModal(
186 this.i18n('Purge'),
187 this.i18n('OSD'),
188 this.i18n('purged'),
9f95a23c
TL
189 (ids: number[]) => {
190 return this.osdService.safeToDestroy(JSON.stringify(ids));
191 },
192 'is_safe_to_destroy',
193 (id: number) => {
194 this.selection = new CdTableSelection();
195 return this.osdService.purge(id);
196 }
11fdf7f2
TL
197 ),
198 disable: () => this.isNotSelectedOrInState('up'),
9f95a23c 199 icon: Icons.erase
11fdf7f2
TL
200 },
201 {
eafe8130 202 name: this.actionLabels.DESTROY,
11fdf7f2
TL
203 permission: 'delete',
204 click: () =>
205 this.showCriticalConfirmationModal(
206 this.i18n('destroy'),
207 this.i18n('OSD'),
208 this.i18n('destroyed'),
9f95a23c
TL
209 (ids: number[]) => {
210 return this.osdService.safeToDestroy(JSON.stringify(ids));
211 },
212 'is_safe_to_destroy',
213 (id: number) => {
214 this.selection = new CdTableSelection();
215 return this.osdService.destroy(id);
216 }
11fdf7f2
TL
217 ),
218 disable: () => this.isNotSelectedOrInState('up'),
9f95a23c
TL
219 icon: Icons.destroyCircle
220 },
221 {
222 name: this.actionLabels.DELETE,
223 permission: 'delete',
f6b5b4d7 224 click: () => this.delete(),
9f95a23c
TL
225 disable: () => !this.hasOsdSelected,
226 icon: Icons.destroy
11fdf7f2
TL
227 }
228 ];
9f95a23c
TL
229 }
230
231 ngOnInit() {
232 this.clusterWideActions = [
81eedcae 233 {
9f95a23c
TL
234 name: this.i18n('Flags'),
235 icon: Icons.flag,
81eedcae 236 click: () => this.configureFlagsAction(),
9f95a23c
TL
237 permission: 'read',
238 visible: () => this.permissions.osd.read
81eedcae
TL
239 },
240 {
9f95a23c
TL
241 name: this.i18n('Recovery Priority'),
242 icon: Icons.deepCheck,
81eedcae 243 click: () => this.configureQosParamsAction(),
9f95a23c
TL
244 permission: 'read',
245 visible: () => this.permissions.configOpt.read
81eedcae
TL
246 },
247 {
248 name: this.i18n('PG scrub'),
9f95a23c 249 icon: Icons.analyse,
81eedcae 250 click: () => this.configurePgScrubAction(),
9f95a23c
TL
251 permission: 'read',
252 visible: () => this.permissions.configOpt.read
81eedcae
TL
253 }
254 ];
11fdf7f2
TL
255 this.columns = [
256 { prop: 'host.name', name: this.i18n('Host') },
9f95a23c
TL
257 { prop: 'id', name: this.i18n('ID'), flexGrow: 1, cellTransformation: CellTemplate.bold },
258 {
259 prop: 'collectedStates',
260 name: this.i18n('Status'),
261 flexGrow: 1,
262 cellTransformation: CellTemplate.badge,
263 customTemplateConfig: {
264 map: {
265 in: { class: 'badge-success' },
266 up: { class: 'badge-success' },
267 down: { class: 'badge-danger' },
268 out: { class: 'badge-danger' },
269 destroyed: { class: 'badge-danger' }
270 }
271 }
272 },
273 {
274 prop: 'tree.device_class',
275 name: this.i18n('Device class'),
276 flexGrow: 1,
277 cellTransformation: CellTemplate.badge,
278 customTemplateConfig: {
279 map: {
280 hdd: { class: 'badge-hdd' },
281 ssd: { class: 'badge-ssd' }
282 }
283 }
284 },
285 {
286 prop: 'stats.numpg',
287 name: this.i18n('PGs'),
288 flexGrow: 1
289 },
290 {
291 prop: 'stats.stat_bytes',
292 name: this.i18n('Size'),
293 flexGrow: 1,
294 pipe: this.dimlessBinaryPipe
295 },
81eedcae 296 { prop: 'stats.usage', name: this.i18n('Usage'), cellTemplate: this.osdUsageTpl },
11fdf7f2
TL
297 {
298 prop: 'stats_history.out_bytes',
299 name: this.i18n('Read bytes'),
300 cellTransformation: CellTemplate.sparkline
301 },
302 {
303 prop: 'stats_history.in_bytes',
9f95a23c 304 name: this.i18n('Write bytes'),
11fdf7f2
TL
305 cellTransformation: CellTemplate.sparkline
306 },
307 {
308 prop: 'stats.op_r',
309 name: this.i18n('Read ops'),
310 cellTransformation: CellTemplate.perSecond
311 },
312 {
313 prop: 'stats.op_w',
314 name: this.i18n('Write ops'),
315 cellTransformation: CellTemplate.perSecond
316 }
317 ];
9f95a23c 318 }
81eedcae 319
9f95a23c
TL
320 /**
321 * Only returns valid IDs, e.g. if an OSD is falsely still selected after being deleted, it won't
322 * get returned.
323 */
324 getSelectedOsdIds(): number[] {
325 const osdIds = this.osds.map((osd) => osd.id);
326 return this.selection.selected.map((row) => row.id).filter((id) => osdIds.includes(id));
11fdf7f2
TL
327 }
328
9f95a23c
TL
329 getSelectedOsds(): any[] {
330 return this.osds.filter(
331 (osd) => !_.isUndefined(osd) && this.getSelectedOsdIds().includes(osd.id)
332 );
333 }
334
335 get hasOsdSelected(): boolean {
336 return this.getSelectedOsdIds().length > 0;
11fdf7f2
TL
337 }
338
339 updateSelection(selection: CdTableSelection) {
340 this.selection = selection;
341 }
342
343 /**
9f95a23c 344 * Returns true if no rows are selected or if *any* of the selected rows are in the given
11fdf7f2
TL
345 * state. Useful for deactivating the corresponding menu entry.
346 */
347 isNotSelectedOrInState(state: 'in' | 'up' | 'down' | 'out'): boolean {
9f95a23c
TL
348 const selectedOsds = this.getSelectedOsds();
349 if (selectedOsds.length === 0) {
11fdf7f2
TL
350 return true;
351 }
11fdf7f2
TL
352 switch (state) {
353 case 'in':
9f95a23c 354 return selectedOsds.some((osd) => osd.in === 1);
11fdf7f2 355 case 'out':
9f95a23c 356 return selectedOsds.some((osd) => osd.in !== 1);
11fdf7f2 357 case 'down':
9f95a23c 358 return selectedOsds.some((osd) => osd.up !== 1);
11fdf7f2 359 case 'up':
9f95a23c 360 return selectedOsds.some((osd) => osd.up === 1);
11fdf7f2
TL
361 }
362 }
363
364 getOsdList() {
365 this.osdService.getList().subscribe((data: any[]) => {
81eedcae 366 this.osds = data.map((osd) => {
11fdf7f2 367 osd.collectedStates = OsdListComponent.collectStates(osd);
9f95a23c
TL
368 osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map((i: string) => i[1]);
369 osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map((i: string) => i[1]);
81eedcae 370 osd.stats.usage = osd.stats.stat_bytes_used / osd.stats.stat_bytes;
11fdf7f2
TL
371 osd.cdIsBinary = true;
372 return osd;
373 });
374 });
375 }
376
9f95a23c
TL
377 editAction() {
378 const selectedOsd = _.filter(this.osds, ['id', this.selection.first().id]).pop();
379
380 this.modalService.show(FormModalComponent, {
381 initialState: {
382 titleText: this.i18n('Edit OSD: {{id}}', {
383 id: selectedOsd.id
384 }),
385 fields: [
386 {
387 type: 'text',
388 name: 'deviceClass',
389 value: selectedOsd.tree.device_class,
390 label: this.i18n('Device class'),
391 required: true
392 }
393 ],
394 submitButtonText: this.i18n('Edit OSD'),
395 onSubmit: (values: any) => {
396 this.osdService.update(selectedOsd.id, values.deviceClass).subscribe(() => {
397 this.notificationService.show(
398 NotificationType.success,
399 this.i18n('Updated OSD "{{id}}"', {
400 id: selectedOsd.id
401 })
402 );
403 this.getOsdList();
404 });
405 }
406 }
407 });
408 }
409
410 scrubAction(deep: boolean) {
11fdf7f2
TL
411 if (!this.hasOsdSelected) {
412 return;
413 }
414
415 const initialState = {
9f95a23c 416 selected: this.getSelectedOsdIds(),
11fdf7f2
TL
417 deep: deep
418 };
419
420 this.bsModalRef = this.modalService.show(OsdScrubModalComponent, { initialState });
421 }
422
81eedcae 423 configureFlagsAction() {
11fdf7f2
TL
424 this.bsModalRef = this.modalService.show(OsdFlagsModalComponent, {});
425 }
426
427 showConfirmationModal(markAction: string, onSubmit: (id: number) => Observable<any>) {
428 this.bsModalRef = this.modalService.show(ConfirmationModalComponent, {
429 initialState: {
430 titleText: this.i18n('Mark OSD {{markAction}}', { markAction: markAction }),
431 buttonText: this.i18n('Mark {{markAction}}', { markAction: markAction }),
432 bodyTpl: this.markOsdConfirmationTpl,
433 bodyContext: {
434 markActionDescription: markAction
435 },
436 onSubmit: () => {
9f95a23c
TL
437 observableForkJoin(
438 this.getSelectedOsdIds().map((osd: any) => onSubmit.call(this.osdService, osd))
439 ).subscribe(() => this.bsModalRef.hide());
11fdf7f2
TL
440 }
441 }
442 });
443 }
444
445 reweight() {
446 const selectedOsd = this.osds.filter((o) => o.id === this.selection.first().id).pop();
447 this.modalService.show(OsdReweightModalComponent, {
448 initialState: {
449 currentWeight: selectedOsd.weight,
450 osdId: selectedOsd.id
451 }
452 });
453 }
454
f6b5b4d7
TL
455 delete() {
456 const deleteFormGroup = new CdFormGroup({
457 preserve: new FormControl(false)
458 });
459
460 this.depCheckerService.checkOrchestratorOrModal(
461 this.actionLabels.DELETE,
462 this.i18n('OSD'),
463 () => {
464 this.showCriticalConfirmationModal(
465 this.i18n('delete'),
466 this.i18n('OSD'),
467 this.i18n('deleted'),
468 (ids: number[]) => {
469 return this.osdService.safeToDelete(JSON.stringify(ids));
470 },
471 'is_safe_to_delete',
472 (id: number) => {
473 this.selection = new CdTableSelection();
474 return this.taskWrapper.wrapTaskAroundCall({
475 task: new FinishedTask('osd/' + URLVerbs.DELETE, {
476 svc_id: id
477 }),
478 call: this.osdService.delete(id, deleteFormGroup.value.preserve, true)
479 });
480 },
481 true,
482 deleteFormGroup,
483 this.deleteOsdExtraTpl
484 );
485 }
486 );
487 }
488
9f95a23c
TL
489 /**
490 * Perform check first and display a critical confirmation modal.
491 * @param {string} actionDescription name of the action.
492 * @param {string} itemDescription the item's name that the action operates on.
493 * @param {string} templateItemDescription the action name to be displayed in modal template.
494 * @param {Function} check the function is called to check if the action is safe.
495 * @param {string} checkKey the safe indicator's key in the check response.
496 * @param {Function} action the action function.
f6b5b4d7
TL
497 * @param {boolean} taskWrapped if true, hide confirmation modal after action
498 * @param {CdFormGroup} childFormGroup additional child form group to be passed to confirmation modal
499 * @param {TemplateRef<any>} childFormGroupTemplate template for additional child form group
9f95a23c 500 */
11fdf7f2
TL
501 showCriticalConfirmationModal(
502 actionDescription: string,
503 itemDescription: string,
504 templateItemDescription: string,
9f95a23c
TL
505 check: (ids: number[]) => Observable<any>,
506 checkKey: string,
507 action: (id: number | number[]) => Observable<any>,
f6b5b4d7
TL
508 taskWrapped: boolean = false,
509 childFormGroup?: CdFormGroup,
510 childFormGroupTemplate?: TemplateRef<any>
11fdf7f2 511 ): void {
9f95a23c 512 check(this.getSelectedOsdIds()).subscribe((result) => {
11fdf7f2
TL
513 const modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
514 initialState: {
515 actionDescription: actionDescription,
516 itemDescription: itemDescription,
517 bodyTemplate: this.criticalConfirmationTpl,
518 bodyContext: {
9f95a23c
TL
519 safeToPerform: result[checkKey],
520 message: result.message,
f6b5b4d7
TL
521 actionDescription: templateItemDescription,
522 osdIds: this.getSelectedOsdIds()
11fdf7f2 523 },
f6b5b4d7
TL
524 childFormGroup: childFormGroup,
525 childFormGroupTemplate: childFormGroupTemplate,
11fdf7f2 526 submitAction: () => {
9f95a23c
TL
527 const observable = observableForkJoin(
528 this.getSelectedOsdIds().map((osd: any) => action.call(this.osdService, osd))
529 );
530 if (taskWrapped) {
531 observable.subscribe(
532 undefined,
533 () => {
534 this.getOsdList();
535 modalRef.hide();
536 },
537 () => modalRef.hide()
538 );
539 } else {
540 observable.subscribe(
541 () => {
542 this.getOsdList();
543 modalRef.hide();
544 },
545 () => modalRef.hide()
546 );
547 }
11fdf7f2
TL
548 }
549 }
550 });
551 });
552 }
553
554 configureQosParamsAction() {
555 this.bsModalRef = this.modalService.show(OsdRecvSpeedModalComponent, {});
556 }
81eedcae
TL
557
558 configurePgScrubAction() {
559 this.bsModalRef = this.modalService.show(OsdPgScrubModalComponent, { class: 'modal-lg' });
560 }
11fdf7f2 561}