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