From b5570e41720adc9e3480b37fe33fa30526564422 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Tue, 10 Dec 2019 14:16:53 +0000 Subject: [PATCH] mgr/dashboard: Explicitly set the device class of an OSD Fixes: https://tracker.ceph.com/issues/43197 Signed-off-by: Ricardo Marques --- src/pybind/mgr/dashboard/controllers/osd.py | 13 ++++++ .../osd/osd-list/osd-list.component.spec.ts | 37 +++++++++++---- .../osd/osd-list/osd-list.component.ts | 45 ++++++++++++++++++- .../src/app/shared/api/osd.service.spec.ts | 7 +++ .../src/app/shared/api/osd.service.ts | 4 ++ 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py index 70fed1d865c6..189da4bb6348 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -103,6 +103,19 @@ class Osd(RESTController): 'histogram': histogram, } + def set(self, svc_id, device_class): + old_device_class = CephService.send_command('mon', 'osd crush get-device-class', + ids=[svc_id]) + old_device_class = old_device_class[0]['device_class'] + if old_device_class != device_class: + CephService.send_command('mon', 'osd crush rm-device-class', + ids=[svc_id]) + if device_class: + CephService.send_command('mon', 'osd crush set-device-class', **{ + 'class': device_class, + 'ids': [svc_id] + }) + @RESTController.Resource('POST', query_params=['deep']) @UpdatePermission def scrub(self, svc_id, deep=False): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts index d3fb7285c67d..2cd2784b9e88 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts @@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import * as _ from 'lodash'; import { BsModalService } from 'ngx-bootstrap/modal'; import { TabsModule } from 'ngx-bootstrap/tabs'; +import { ToastrModule } from 'ngx-toastr'; import { EMPTY, of } from 'rxjs'; import { @@ -18,6 +19,7 @@ import { CoreModule } from '../../../../core/core.module'; import { OsdService } from '../../../../shared/api/osd.service'; import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component'; import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component'; import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component'; import { CdTableAction } from '../../../../shared/models/cd-table-action'; import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; @@ -47,8 +49,8 @@ describe('OsdListComponent', () => { const setFakeSelection = () => { // Default data and selection - const selection = [{ id: 1 }]; - const data = [{ id: 1 }]; + const selection = [{ id: 1, tree: { device_class: 'ssd' } }]; + const data = [{ id: 1, tree: { device_class: 'ssd' } }]; // Table data and selection component.selection = new CdTableSelection(); @@ -78,6 +80,7 @@ describe('OsdListComponent', () => { HttpClientTestingModule, PerformanceCounterModule, TabsModule.forRoot(), + ToastrModule.forRoot(), CephModule, ReactiveFormsModule, RouterTestingModule, @@ -119,6 +122,9 @@ describe('OsdListComponent', () => { const createOsd = (n: number) => ({ in: 'in', up: 'up', + tree: { + device_class: 'ssd' + }, stats_history: { op_out_bytes: [[n, n], [n * 2, n * 2]], op_in_bytes: [[n * 3, n * 3], [n * 4, n * 4]] @@ -239,6 +245,7 @@ describe('OsdListComponent', () => { 'create,update,delete': { actions: [ 'Create', + 'Edit', 'Scrub', 'Deep Scrub', 'Reweight', @@ -249,11 +256,20 @@ describe('OsdListComponent', () => { 'Purge', 'Destroy' ], - primary: { multiple: 'Scrub', executing: 'Scrub', single: 'Scrub', no: 'Create' } + primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,update': { - actions: ['Create', 'Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'], - primary: { multiple: 'Scrub', executing: 'Scrub', single: 'Scrub', no: 'Create' } + actions: [ + 'Create', + 'Edit', + 'Scrub', + 'Deep Scrub', + 'Reweight', + 'Mark Out', + 'Mark In', + 'Mark Down' + ], + primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,delete': { actions: ['Create', 'Mark Lost', 'Purge', 'Destroy'], @@ -270,6 +286,7 @@ describe('OsdListComponent', () => { }, 'update,delete': { actions: [ + 'Edit', 'Scrub', 'Deep Scrub', 'Reweight', @@ -280,11 +297,11 @@ describe('OsdListComponent', () => { 'Purge', 'Destroy' ], - primary: { multiple: 'Scrub', executing: 'Scrub', single: 'Scrub', no: 'Scrub' } + primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' } }, update: { - actions: ['Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'], - primary: { multiple: 'Scrub', executing: 'Scrub', single: 'Scrub', no: 'Scrub' } + actions: ['Edit', 'Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'], + primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' } }, delete: { actions: ['Mark Lost', 'Purge', 'Destroy'], @@ -350,6 +367,10 @@ describe('OsdListComponent', () => { expectOpensModal('Reweight', OsdReweightModalComponent); }); + it('opens the form modal', () => { + expectOpensModal('Edit', FormModalComponent); + }); + it('opens all confirmation modals', () => { const modalClass = ConfirmationModalComponent; expectOpensModal('Mark Out', modalClass); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts index 95bf1279c01d..89842cbf0fe3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts @@ -8,16 +8,19 @@ import { forkJoin as observableForkJoin, Observable } from 'rxjs'; import { OsdService } from '../../../../shared/api/osd.service'; import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component'; import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component'; import { ActionLabelsI18n } from '../../../../shared/constants/app.constants'; import { TableComponent } from '../../../../shared/datatable/table/table.component'; import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; import { Icons } from '../../../../shared/enum/icons.enum'; +import { NotificationType } from '../../../../shared/enum/notification-type.enum'; import { CdTableAction } from '../../../../shared/models/cd-table-action'; import { CdTableColumn } from '../../../../shared/models/cd-table-column'; import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; import { Permissions } from '../../../../shared/models/permissions'; import { DimlessBinaryPipe } from '../../../../shared/pipes/dimless-binary.pipe'; import { AuthStorageService } from '../../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../../shared/services/notification.service'; import { URLBuilderService } from '../../../../shared/services/url-builder.service'; import { OsdFlagsModalComponent } from '../osd-flags-modal/osd-flags-modal.component'; import { OsdPgScrubModalComponent } from '../osd-pg-scrub-modal/osd-pg-scrub-modal.component'; @@ -76,7 +79,8 @@ export class OsdListComponent implements OnInit { private modalService: BsModalService, private i18n: I18n, private urlBuilder: URLBuilderService, - public actionLabels: ActionLabelsI18n + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService ) { this.permissions = this.authStorageService.getPermissions(); this.tableActions = [ @@ -87,6 +91,12 @@ export class OsdListComponent implements OnInit { routerLink: () => this.urlBuilder.getCreate(), canBePrimary: (selection: CdTableSelection) => !selection.hasSelection }, + { + name: this.actionLabels.EDIT, + permission: 'update', + icon: Icons.edit, + click: () => this.editAction() + }, { name: this.actionLabels.SCRUB, permission: 'update', @@ -324,6 +334,39 @@ export class OsdListComponent implements OnInit { }); } + editAction() { + const selectedOsd = _.filter(this.osds, ['id', this.selection.first().id]).pop(); + + this.modalService.show(FormModalComponent, { + initialState: { + titleText: this.i18n('Edit OSD: {{id}}', { + id: selectedOsd.id + }), + fields: [ + { + type: 'inputText', + name: 'deviceClass', + value: selectedOsd.tree.device_class, + label: this.i18n('Device class'), + required: true + } + ], + submitButtonText: this.i18n('Edit OSD'), + onSubmit: (values) => { + this.osdService.update(selectedOsd.id, values.deviceClass).subscribe(() => { + this.notificationService.show( + NotificationType.success, + this.i18n('Updated OSD "{{id}}"', { + id: selectedOsd.id + }) + ); + this.getOsdList(); + }); + } + } + }); + } + scrubAction(deep) { if (!this.hasOsdSelected) { return; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts index 957c646dd3df..cc92721c09d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.spec.ts @@ -88,6 +88,13 @@ describe('OsdService', () => { expect(req.request.body).toEqual({ weight: 0.5 }); }); + it('should update OSD', () => { + service.update(1, 'hdd').subscribe(); + const req = httpTesting.expectOne('api/osd/1'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ device_class: 'hdd' }); + }); + it('should mark an OSD lost', () => { service.markLost(1).subscribe(); const req = httpTesting.expectOne('api/osd/1/mark_lost'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts index e8429342dfd7..d78093e35ae3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts @@ -234,6 +234,10 @@ export class OsdService { return this.http.post(`${this.path}/${id}/reweight`, { weight: weight }); } + update(id: number, deviceClass: string) { + return this.http.put(`${this.path}/${id}`, { device_class: deviceClass }); + } + markLost(id: number) { return this.http.post(`${this.path}/${id}/mark_lost`, null); } -- 2.47.3