1 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { FormControl } from '@angular/forms';
3 import { Router } from '@angular/router';
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';
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';
42 const BASE_URL = 'osd';
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) }]
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>;
66 permissions: Permissions;
67 tableActions: CdTableAction[];
68 bsModalRef: NgbModalRef;
69 columns: CdTableColumn[];
70 clusterWideActions: CdTableAction[];
72 osdSettings = new OsdSettings();
74 selection = new CdTableSelection();
76 disabledFlags: string[] = [
82 indivFlagNames: string[] = ['noup', 'nodown', 'noin', 'noout'];
84 orchStatus: OrchestratorStatus;
85 actionOrchFeatures = {
86 create: [OrchestratorFeature.OSD_CREATE],
87 delete: [OrchestratorFeature.OSD_DELETE]
90 protected static collectStates(osd: any) {
91 const states = [osd['in'] ? 'in' : 'out'];
94 } else if (osd.state.includes('destroyed')) {
95 states.push('destroyed');
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
115 this.permissions = this.authStorageService.getPermissions();
116 this.tableActions = [
118 name: this.actionLabels.CREATE,
119 permission: 'create',
121 click: () => this.router.navigate([this.urlBuilder.getCreate()]),
122 disable: (selection: CdTableSelection) => this.getDisable('create', selection),
123 canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
126 name: this.actionLabels.EDIT,
127 permission: 'update',
129 click: () => this.editAction()
132 name: this.actionLabels.FLAGS,
133 permission: 'update',
135 click: () => this.configureFlagsIndivAction(),
136 disable: () => !this.hasOsdSelected
139 name: this.actionLabels.SCRUB,
140 permission: 'update',
142 click: () => this.scrubAction(false),
143 disable: () => !this.hasOsdSelected,
144 canBePrimary: (selection: CdTableSelection) => selection.hasSelection
147 name: this.actionLabels.DEEP_SCRUB,
148 permission: 'update',
149 icon: Icons.deepCheck,
150 click: () => this.scrubAction(true),
151 disable: () => !this.hasOsdSelected
154 name: this.actionLabels.REWEIGHT,
155 permission: 'update',
156 click: () => this.reweight(),
157 disable: () => !this.hasOsdSelected || !this.selection.hasSingleSelection,
161 name: this.actionLabels.MARK_OUT,
162 permission: 'update',
163 click: () => this.showConfirmationModal($localize`out`, this.osdService.markOut),
164 disable: () => this.isNotSelectedOrInState('out'),
168 name: this.actionLabels.MARK_IN,
169 permission: 'update',
170 click: () => this.showConfirmationModal($localize`in`, this.osdService.markIn),
171 disable: () => this.isNotSelectedOrInState('in'),
175 name: this.actionLabels.MARK_DOWN,
176 permission: 'update',
177 click: () => this.showConfirmationModal($localize`down`, this.osdService.markDown),
178 disable: () => this.isNotSelectedOrInState('down'),
182 name: this.actionLabels.MARK_LOST,
183 permission: 'delete',
185 this.showCriticalConfirmationModal(
188 $localize`marked lost`,
190 return this.osdService.safeToDestroy(JSON.stringify(ids));
192 'is_safe_to_destroy',
193 this.osdService.markLost
195 disable: () => this.isNotSelectedOrInState('up'),
199 name: this.actionLabels.PURGE,
200 permission: 'delete',
202 this.showCriticalConfirmationModal(
207 return this.osdService.safeToDestroy(JSON.stringify(ids));
209 'is_safe_to_destroy',
211 this.selection = new CdTableSelection();
212 return this.osdService.purge(id);
215 disable: () => this.isNotSelectedOrInState('up'),
219 name: this.actionLabels.DESTROY,
220 permission: 'delete',
222 this.showCriticalConfirmationModal(
225 $localize`destroyed`,
227 return this.osdService.safeToDestroy(JSON.stringify(ids));
229 'is_safe_to_destroy',
231 this.selection = new CdTableSelection();
232 return this.osdService.destroy(id);
235 disable: () => this.isNotSelectedOrInState('up'),
236 icon: Icons.destroyCircle
239 name: this.actionLabels.DELETE,
240 permission: 'delete',
241 click: () => this.delete(),
242 disable: (selection: CdTableSelection) => this.getDisable('delete', selection),
249 this.clusterWideActions = [
251 name: $localize`Flags`,
253 click: () => this.configureFlagsAction(),
255 visible: () => this.permissions.osd.read
258 name: $localize`Recovery Priority`,
259 icon: Icons.deepCheck,
260 click: () => this.configureQosParamsAction(),
262 visible: () => this.permissions.configOpt.read
265 name: $localize`PG scrub`,
267 click: () => this.configurePgScrubAction(),
269 visible: () => this.permissions.configOpt.read
277 cellTransformation: CellTemplate.executing,
278 customTemplateConfig: {
282 { prop: 'host.name', name: $localize`Host` },
284 prop: 'collectedStates',
285 name: $localize`Status`,
287 cellTransformation: CellTemplate.badge,
288 customTemplateConfig: {
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' }
299 prop: 'tree.device_class',
300 name: $localize`Device class`,
302 cellTransformation: CellTemplate.badge,
303 customTemplateConfig: {
305 hdd: { class: 'badge-hdd' },
306 ssd: { class: 'badge-ssd' }
312 name: $localize`PGs`,
316 prop: 'stats.stat_bytes',
317 name: $localize`Size`,
319 pipe: this.dimlessBinaryPipe
323 name: $localize`Flags`,
324 cellTemplate: this.flagsTpl
326 { prop: 'stats.usage', name: $localize`Usage`, cellTemplate: this.osdUsageTpl },
328 prop: 'stats_history.out_bytes',
329 name: $localize`Read bytes`,
330 cellTransformation: CellTemplate.sparkline
333 prop: 'stats_history.in_bytes',
334 name: $localize`Write bytes`,
335 cellTransformation: CellTemplate.sparkline
339 name: $localize`Read ops`,
340 cellTransformation: CellTemplate.perSecond
344 name: $localize`Write ops`,
345 cellTransformation: CellTemplate.perSecond
349 this.orchService.status().subscribe((status: OrchestratorStatus) => (this.orchStatus = status));
354 .subscribe((data: any) => {
355 this.osdSettings = data;
359 getDisable(action: 'create' | 'delete', selection: CdTableSelection): boolean | string {
360 if (action === 'delete') {
361 if (!selection.hasSelection) {
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';
374 return this.orchService.getTableActionDisableDesc(
376 this.actionOrchFeatures[action]
381 * Only returns valid IDs, e.g. if an OSD is falsely still selected after being deleted, it won't
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))
392 getSelectedOsds(): any[] {
393 return this.osds.filter(
394 (osd) => !_.isUndefined(osd) && this.getSelectedOsdIds().includes(osd.id)
398 get hasOsdSelected(): boolean {
399 return this.getSelectedOsdIds().length > 0;
402 updateSelection(selection: CdTableSelection) {
403 this.selection = selection;
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.
410 isNotSelectedOrInState(state: 'in' | 'up' | 'down' | 'out'): boolean {
411 const selectedOsds = this.getSelectedOsds();
412 if (selectedOsds.length === 0) {
417 return selectedOsds.some((osd) => osd.in === 1);
419 return selectedOsds.some((osd) => osd.in !== 1);
421 return selectedOsds.some((osd) => osd.up !== 1);
423 return selectedOsds.some((osd) => osd.up === 1);
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;
448 const selectedOsd = _.filter(this.osds, ['id', this.selection.first().id]).pop();
450 this.modalService.show(FormModalComponent, {
451 titleText: $localize`Edit OSD: ${selectedOsd.id}`,
456 value: selectedOsd.tree.device_class,
457 label: $localize`Device class`,
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}'`
474 scrubAction(deep: boolean) {
475 if (!this.hasOsdSelected) {
479 const initialState = {
480 selected: this.getSelectedOsdIds(),
484 this.bsModalRef = this.modalService.show(OsdScrubModalComponent, initialState);
487 configureFlagsAction() {
488 this.bsModalRef = this.modalService.show(OsdFlagsModalComponent);
491 configureFlagsIndivAction() {
492 const initialState = {
493 selected: this.getSelectedOsds()
495 this.bsModalRef = this.modalService.show(OsdFlagsIndivModalComponent, initialState);
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,
505 markActionDescription: markAction,
510 this.getSelectedOsdIds().map((osd: any) => onSubmit.call(this.osdService, osd))
511 ).subscribe(() => this.bsModalRef.close());
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
525 const deleteFormGroup = new CdFormGroup({
526 preserve: new FormControl(false)
529 this.showCriticalConfirmationModal(
534 return this.osdService.safeToDelete(JSON.stringify(ids));
538 this.selection = new CdTableSelection();
539 return this.taskWrapper.wrapTaskAroundCall({
540 task: new FinishedTask('osd/' + URLVerbs.DELETE, {
543 call: this.osdService.delete(id, deleteFormGroup.value.preserve, true)
548 this.deleteOsdExtraTpl
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
564 showCriticalConfirmationModal(
565 actionDescription: string,
566 itemDescription: string,
567 templateItemDescription: string,
568 check: (ids: number[]) => Observable<any>,
570 action: (id: number | number[]) => Observable<any>,
571 taskWrapped: boolean = false,
572 childFormGroup?: CdFormGroup,
573 childFormGroupTemplate?: TemplateRef<any>
575 check(this.getSelectedOsdIds()).subscribe((result) => {
576 const modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
577 actionDescription: actionDescription,
578 itemDescription: itemDescription,
579 bodyTemplate: this.criticalConfirmationTpl,
581 safeToPerform: result[checkKey],
582 message: result.message,
583 actionDescription: templateItemDescription,
584 osdIds: this.getSelectedOsdIds()
586 childFormGroup: childFormGroup,
587 childFormGroupTemplate: childFormGroupTemplate,
588 submitAction: () => {
589 const observable = observableForkJoin(
590 this.getSelectedOsdIds().map((osd: any) => action.call(this.osdService, osd))
593 observable.subscribe({
598 complete: () => modalRef.close()
601 observable.subscribe(
606 () => modalRef.close()
614 configureQosParamsAction() {
615 this.bsModalRef = this.modalService.show(OsdRecvSpeedModalComponent);
618 configurePgScrubAction() {
619 this.bsModalRef = this.modalService.show(OsdPgScrubModalComponent, undefined, { size: 'lg' });