From 6b717d5fbab12b946cde53f378e9669b1f5a3772 Mon Sep 17 00:00:00 2001 From: Patrick Seidensal Date: Tue, 24 Sep 2019 22:22:22 +0200 Subject: [PATCH] mgr/dashboard: add smart data to hosts page Fixes: https://tracker.ceph.com/issues/42064 Signed-off-by: Patrick Seidensal --- src/pybind/mgr/dashboard/controllers/host.py | 11 +- src/pybind/mgr/dashboard/controllers/osd.py | 25 +--- .../src/app/ceph/cluster/cluster.module.ts | 5 +- .../host-details/host-details.component.html | 17 ++- .../host-details.component.spec.ts | 30 ++-- .../host-details/host-details.component.ts | 4 + .../cluster/hosts/hosts.component.spec.ts | 2 + .../osd-details/osd-details.component.html | 2 +- .../osd-details/osd-details.component.spec.ts | 25 ++-- .../osd-smart-list.component.ts | 116 --------------- .../src/app/ceph/shared/ceph-shared.module.ts | 8 +- .../smart_data_version_1_0_response.json | 0 .../smart-list/smart-list.component.html} | 0 .../smart-list/smart-list.component.scss} | 0 .../smart-list/smart-list.component.spec.ts} | 16 +-- .../shared/smart-list/smart-list.component.ts | 136 ++++++++++++++++++ .../src/app/shared/api/host.service.ts | 4 + .../src/app/shared/api/osd.service.ts | 2 +- .../mgr/dashboard/services/ceph_service.py | 45 +++++- 19 files changed, 262 insertions(+), 186 deletions(-) delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.ts rename src/pybind/mgr/dashboard/frontend/src/app/ceph/{cluster/osd/osd-smart-list => shared/smart-list}/fixtures/smart_data_version_1_0_response.json (100%) rename src/pybind/mgr/dashboard/frontend/src/app/ceph/{cluster/osd/osd-smart-list/osd-smart-list.component.html => shared/smart-list/smart-list.component.html} (100%) rename src/pybind/mgr/dashboard/frontend/src/app/ceph/{cluster/osd/osd-smart-list/osd-smart-list.component.scss => shared/smart-list/smart-list.component.scss} (100%) rename src/pybind/mgr/dashboard/frontend/src/app/ceph/{cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts => shared/smart-list/smart-list.component.spec.ts} (89%) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index ba72bdc32ef..b04a3d05a9f 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -8,8 +8,8 @@ from .. import mgr from ..exceptions import DashboardException from ..security import Scope from ..services.orchestrator import OrchClient -from ..services.exception import handle_orchestrator_error from ..services.ceph_service import CephService +from ..services.exception import handle_orchestrator_error def host_task(name, metadata, wait_for=10.0): @@ -106,5 +106,10 @@ class Host(RESTController): @RESTController.Resource('GET') def devices(self, hostname): - # (str) -> dict - return CephService.send_command('mon', 'device ls-by-host', host=hostname) + # (str) -> List + return CephService.get_devices_by_host(hostname) + + @RESTController.Resource('GET') + def smart(self, hostname): + # type: (str) -> dict + return CephService.get_smart_data_by_host(hostname) diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py index b36681b9820..70fed1d865c 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -70,30 +70,13 @@ class Osd(RESTController): return resp if svc_id is None else resp[int(svc_id)] @staticmethod - def _get_smart_data(svc_id): + def _get_smart_data(osd_id): # type: (str) -> dict - """ - Returns S.M.A.R.T data for the given OSD ID. - :type svc_id: Numeric ID of the OSD - """ - devices = CephService.send_command( - 'mon', 'device ls-by-daemon', who='osd.{}'.format(svc_id)) - smart_data = {} - for dev_id in [d['devid'] for d in devices]: - if dev_id not in smart_data: - dev_smart_data = mgr.remote('devicehealth', 'do_scrape_daemon', 'osd', svc_id, - dev_id) - if dev_smart_data: - for _, dev_data in dev_smart_data.items(): - if 'error' in dev_data: - logger.warning( - 'Error retrieving smartctl data for device ID %s: %s', dev_id, - dev_smart_data) - smart_data.update(dev_smart_data) - return smart_data + """Returns S.M.A.R.T data for the given OSD ID.""" + return CephService.get_smart_data_by_daemon('osd', osd_id) @RESTController.Resource('GET') - def get_smart_data(self, svc_id): + def smart(self, svc_id): # type: (str) -> dict return self._get_smart_data(svc_id) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index e3bae17c0ca..4a11c8e63bc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -41,7 +41,6 @@ import { OsdPgScrubModalComponent } from './osd/osd-pg-scrub-modal/osd-pg-scrub- import { OsdRecvSpeedModalComponent } from './osd/osd-recv-speed-modal/osd-recv-speed-modal.component'; import { OsdReweightModalComponent } from './osd/osd-reweight-modal/osd-reweight-modal.component'; import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.component'; -import { OsdSmartListComponent } from './osd/osd-smart-list/osd-smart-list.component'; import { ActiveAlertListComponent } from './prometheus/active-alert-list/active-alert-list.component'; import { MonitoringListComponent } from './prometheus/monitoring-list/monitoring-list.component'; import { RulesListComponent } from './prometheus/rules-list/rules-list.component'; @@ -109,7 +108,6 @@ import { ServicesComponent } from './services/services.component'; ServicesComponent, InventoryComponent, HostFormComponent, - OsdSmartListComponent, OsdFormComponent, OsdDevicesSelectionModalComponent, InventoryDevicesComponent, @@ -117,7 +115,8 @@ import { ServicesComponent } from './services/services.component'; OsdCreationPreviewModalComponent, RulesListComponent, ActiveAlertListComponent, - MonitoringListComponent + MonitoringListComponent, + HostFormComponent ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html index c38b669c82b..416aa2de3fe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html @@ -6,23 +6,32 @@ - - + - + + + + + + No hostname found. + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts index ebe9f097e72..224f4c5f3f6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts @@ -4,14 +4,17 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; -import { TabsetComponent, TabsModule } from 'ngx-bootstrap/tabs'; +import { TabsModule } from 'ngx-bootstrap/tabs'; import { of } from 'rxjs'; + import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper'; import { CoreModule } from '../../../../core/core.module'; import { OrchestratorService } from '../../../../shared/api/orchestrator.service'; import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; import { Permissions } from '../../../../shared/models/permissions'; +import { SharedModule } from '../../../../shared/shared.module'; import { CephModule } from '../../../ceph.module'; +import { CephSharedModule } from '../../../shared/ceph-shared.module'; import { HostDetailsComponent } from './host-details.component'; describe('HostDetailsComponent', () => { @@ -26,7 +29,9 @@ describe('HostDetailsComponent', () => { NgBootstrapFormValidationModule.forRoot(), RouterTestingModule, CephModule, - CoreModule + CoreModule, + CephSharedModule, + SharedModule ], declarations: [], providers: [i18nProviders] @@ -44,7 +49,6 @@ describe('HostDetailsComponent', () => { spyOn(orchService, 'status').and.returnValue(of({ available: true })); spyOn(orchService, 'inventoryDeviceList').and.returnValue(of([])); spyOn(orchService, 'serviceList').and.returnValue(of([])); - fixture.detectChanges(); }); it('should create', () => { @@ -53,23 +57,23 @@ describe('HostDetailsComponent', () => { describe('Host details tabset', () => { beforeEach(() => { - component.selection.selected = [ - { - hostname: 'localhost' - } - ]; + component.selection.selected = [{ hostname: 'localhost' }]; + fixture.detectChanges(); }); it('should recognize a tabset child', () => { - fixture.detectChanges(); - const tabsetChild: TabsetComponent = component.tabsetChild; + const tabsetChild = component.tabsetChild; expect(tabsetChild).toBeDefined(); }); it('should show tabs', () => { - fixture.detectChanges(); - const tabs = component.tabsetChild.tabs.map((tab) => tab.heading); - expect(tabs).toEqual(['Devices', 'Inventory', 'Services', 'Performance Details']); + expect(component.tabsetChild.tabs.map((t) => t.heading)).toEqual([ + 'Devices', + 'Device health', + 'Inventory', + 'Services', + 'Performance Details' + ]); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts index d66a7f87fd3..469c4c3ab7e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts @@ -20,5 +20,9 @@ export class HostDetailsComponent { @ViewChild(TabsetComponent, { static: false }) tabsetChild: TabsetComponent; + get selectedHostname(): string { + return this.selection.hasSelection ? this.selection.first()['hostname'] : null; + } + constructor() {} } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts index 9ad1fde6944..b2247f90b9a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts @@ -14,6 +14,7 @@ import { Permissions } from '../../../shared/models/permissions'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { SharedModule } from '../../../shared/shared.module'; import { CephModule } from '../../ceph.module'; +import { CephSharedModule } from '../../shared/ceph-shared.module'; import { HostsComponent } from './hosts.component'; describe('HostsComponent', () => { @@ -29,6 +30,7 @@ describe('HostsComponent', () => { configureTestBed({ imports: [ + CephSharedModule, SharedModule, HttpClientTestingModule, TabsModule.forRoot(), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html index 1f4b137aa6f..67ca9683be9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html @@ -27,7 +27,7 @@ - + { @@ -22,14 +23,14 @@ describe('OsdDetailsComponent', () => { let getDetailsSpy; configureTestBed({ - imports: [ - HttpClientTestingModule, - TabsModule.forRoot(), - PerformanceCounterModule, - CephModule, - CoreModule + imports: [HttpClientTestingModule, TabsModule.forRoot(), SharedModule], + declarations: [ + OsdDetailsComponent, + DeviceListComponent, + SmartListComponent, + TablePerformanceCounterComponent, + OsdPerformanceHistogramComponent ], - declarations: [], providers: i18nProviders }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.ts deleted file mode 100644 index d92186f24c4..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { I18n } from '@ngx-translate/i18n-polyfill'; -import * as _ from 'lodash'; -import { - OsdService, - SmartAttribute, - SmartDataV1, - SmartError -} from '../../../../shared/api/osd.service'; -import { CdTableColumn } from '../../../../shared/models/cd-table-column'; - -@Component({ - selector: 'cd-osd-smart-list', - templateUrl: './osd-smart-list.component.html', - styleUrls: ['./osd-smart-list.component.scss'] -}) -export class OsdSmartListComponent implements OnInit, OnChanges { - @Input() - osdId: number; - loading = false; - incompatible = false; - - data: { - [deviceId: string]: { - info: { [key: string]: number | string | boolean }; - smart: { - revision: number; - table: SmartAttribute[]; - }; - }; - } = {}; - - columns: CdTableColumn[]; - - constructor(private i18n: I18n, private osdService: OsdService) {} - - private isSmartError(data: SmartDataV1 | SmartError): data is SmartError { - return (data as SmartError).error !== undefined; - } - - private updateData(osdId: number) { - this.loading = true; - this.osdService.getSmartData(osdId).subscribe((data) => { - const result = {}; - _.each(data, (smartData, deviceId) => { - if (this.isSmartError(smartData)) { - let userMessage = ''; - if (smartData.smartctl_error_code === -22) { - userMessage = this.i18n( - `Smartctl has received an unknown argument (error code - {{smartData.smartctl_error_code}}). You may be using an - incompatible version of smartmontools. Version >= 7.0 of - smartmontools is required to succesfully retrieve data.`, - { code: smartData.smartctl_error_code } - ); - } else { - userMessage = this.i18n('An error with error code {{code}} occurred.', { - code: smartData.smartctl_error_code - }); - } - result[deviceId] = { - error: smartData.error, - smartctl_error_code: smartData.smartctl_error_code, - smartctl_output: smartData.smartctl_output, - userMessage: userMessage, - device: smartData.dev, - identifier: smartData.nvme_vendor - }; - return; - } - - // Prepare S.M.A.R.T data - if (smartData.json_format_version[0] === 1) { - // Version 1.x - const excludes = [ - 'ata_smart_attributes', - 'ata_smart_selective_self_test_log', - 'ata_smart_data' - ]; - const info = _.pickBy(smartData, (_value, key) => !excludes.includes(key)); - // Build result - result[deviceId] = { - info: info, - smart: { - attributes: smartData.ata_smart_attributes, - data: smartData.ata_smart_data - }, - device: info.device.name, - identifier: info.serial_number - }; - } else { - this.incompatible = true; - } - }); - this.data = result; - this.loading = false; - }); - } - - ngOnInit() { - this.columns = [ - { prop: 'id', name: this.i18n('ID') }, - { prop: 'name', name: this.i18n('Name') }, - { prop: 'raw.value', name: this.i18n('Raw') }, - { prop: 'thresh', name: this.i18n('Threshold') }, - { prop: 'value', name: this.i18n('Value') }, - { prop: 'when_failed', name: this.i18n('When Failed') }, - { prop: 'worst', name: this.i18n('Worst') } - ]; - } - - ngOnChanges(changes: SimpleChanges): void { - this.data = {}; // Clear previous data - this.updateData(changes.osdId.currentValue); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts index 93f4ad77e8b..e595f07f3d9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts @@ -1,12 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { TabsModule } from 'ngx-bootstrap/tabs'; import { DataTableModule } from '../../shared/datatable/datatable.module'; import { SharedModule } from '../../shared/shared.module'; import { DeviceListComponent } from './device-list/device-list.component'; +import { SmartListComponent } from './smart-list/smart-list.component'; @NgModule({ - imports: [CommonModule, DataTableModule, SharedModule], - exports: [DeviceListComponent], - declarations: [DeviceListComponent] + imports: [CommonModule, DataTableModule, SharedModule, TabsModule], + exports: [DeviceListComponent, SmartListComponent], + declarations: [DeviceListComponent, SmartListComponent] }) export class CephSharedModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/fixtures/smart_data_version_1_0_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_response.json similarity index 100% rename from src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/fixtures/smart_data_version_1_0_response.json rename to src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_response.json diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html similarity index 100% rename from src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.html rename to src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss similarity index 100% rename from src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.scss rename to src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts similarity index 89% rename from src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts rename to src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts index 826728477c4..3f11c8c7faa 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts @@ -7,14 +7,14 @@ import { TabsetComponent, TabsetConfig, TabsModule } from 'ngx-bootstrap/tabs'; import _ = require('lodash'); import { of } from 'rxjs'; -import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper'; -import { OsdService } from '../../../../shared/api/osd.service'; -import { SharedModule } from '../../../../shared/shared.module'; -import { OsdSmartListComponent } from './osd-smart-list.component'; +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { OsdService } from '../../../shared/api/osd.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { SmartListComponent } from './smart-list.component'; describe('OsdSmartListComponent', () => { - let component: OsdSmartListComponent; - let fixture: ComponentFixture; + let component: SmartListComponent; + let fixture: ComponentFixture; let osdService: OsdService; const SMART_DATA_VERSION_1_0 = require('./fixtures/smart_data_version_1_0_response.json'); @@ -61,13 +61,13 @@ describe('OsdSmartListComponent', () => { }; configureTestBed({ - declarations: [OsdSmartListComponent], + declarations: [SmartListComponent], imports: [TabsModule, SharedModule, HttpClientTestingModule], providers: [i18nProviders, TabsetComponent, TabsetConfig] }); beforeEach(() => { - fixture = TestBed.createComponent(OsdSmartListComponent); + fixture = TestBed.createComponent(SmartListComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts new file mode 100644 index 00000000000..3938b887007 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts @@ -0,0 +1,136 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; +import { HostService } from '../../../shared/api/host.service'; +import { + OsdService, + SmartAttribute, + SmartDataV1, + SmartError +} from '../../../shared/api/osd.service'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; + +@Component({ + selector: 'cd-smart-list', + templateUrl: './smart-list.component.html', + styleUrls: ['./smart-list.component.scss'] +}) +export class SmartListComponent implements OnInit, OnChanges { + @Input() + osdId: number = null; + @Input() + hostname: string = null; + + loading = false; + incompatible = false; + + data: { + [deviceId: string]: { + info: { [key: string]: number | string | boolean }; + smart: { + revision: number; + table: SmartAttribute[]; + }; + }; + } = {}; + + columns: CdTableColumn[]; + + constructor( + private i18n: I18n, + private osdService: OsdService, + private hostService: HostService + ) {} + + private isSmartError(data: SmartDataV1 | SmartError): data is SmartError { + return (data as SmartError).error !== undefined; + } + + private fetchData(data) { + const result = {}; + _.each(data, (smartData, deviceId) => { + if (this.isSmartError(smartData)) { + let userMessage = ''; + if (smartData.smartctl_error_code === -22) { + userMessage = this.i18n( + 'Smartctl has received an unknown argument (error code ' + + '{{code}}). You may be using an ' + + 'incompatible version of smartmontools. Version >= 7.0 of ' + + 'smartmontools is required to successfully retrieve data.', + { code: smartData.smartctl_error_code } + ); + } else { + userMessage = this.i18n('An error with error code {{code}} occurred.', { + code: smartData.smartctl_error_code + }); + } + result[deviceId] = { + error: smartData.error, + smartctl_error_code: smartData.smartctl_error_code, + smartctl_output: smartData.smartctl_output, + userMessage: userMessage, + device: smartData.dev, + identifier: smartData.nvme_vendor + }; + return; + } + + // Prepare S.M.A.R.T data + if (smartData.json_format_version[0] === 1) { + // Version 1.x + const excludes = [ + 'ata_smart_attributes', + 'ata_smart_selective_self_test_log', + 'ata_smart_data' + ]; + const info = _.pickBy(smartData, (_value, key) => !excludes.includes(key)); + // Build result + result[deviceId] = { + info: info, + smart: { + attributes: smartData.ata_smart_attributes, + data: smartData.ata_smart_data + }, + device: info.device.name, + identifier: info.serial_number + }; + } else { + this.incompatible = true; + } + }); + this.data = result; + this.loading = false; + } + + private updateData() { + this.loading = true; + + if (this.osdId !== null) { + this.osdService.getSmartData(this.osdId).subscribe(this.fetchData.bind(this)); + } else if (this.hostname !== null) { + this.hostService.getSmartData(this.hostname).subscribe(this.fetchData.bind(this)); + } + } + + ngOnInit() { + this.columns = [ + { prop: 'id', name: this.i18n('ID') }, + { prop: 'name', name: this.i18n('Name') }, + { prop: 'raw.value', name: this.i18n('Raw') }, + { prop: 'thresh', name: this.i18n('Threshold') }, + { prop: 'value', name: this.i18n('Value') }, + { prop: 'when_failed', name: this.i18n('When Failed') }, + { prop: 'worst', name: this.i18n('Worst') } + ]; + } + + ngOnChanges(changes: SimpleChanges): void { + this.data = {}; // Clear previous data + if (changes.osdId) { + this.osdId = changes.osdId.currentValue; + } else if (changes.hostname) { + this.hostname = changes.hostname.currentValue; + } + this.updateData(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts index 26e440d8120..b1dcaec12f8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts @@ -33,4 +33,8 @@ export class HostService { .get(`${this.baseURL}/${hostname}/devices`) .pipe(map((devices) => devices.map((device) => this.deviceService.prepareDevice(device)))); } + + getSmartData(hostname) { + return this.http.get(`${this.baseURL}/${hostname}/smart`); + } } 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 824bc1c66fd..e8429342dfd 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 @@ -202,7 +202,7 @@ export class OsdService { */ getSmartData(id: number) { return this.http.get<{ [deviceId: string]: SmartDataV1 | SmartError }>( - `${this.path}/${id}/get_smart_data` + `${this.path}/${id}/smart` ); } diff --git a/src/pybind/mgr/dashboard/services/ceph_service.py b/src/pybind/mgr/dashboard/services/ceph_service.py index e74b7f70702..11f19090db7 100644 --- a/src/pybind/mgr/dashboard/services/ceph_service.py +++ b/src/pybind/mgr/dashboard/services/ceph_service.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import - import json import logging +from six.moves import reduce import rados @@ -169,6 +169,7 @@ class CephService(object): if r != 0: logger.error("send_command '%s' failed. (r=%s, outs=\"%s\", kwargs=%s)", prefix, r, outs, kwargs) + raise SendCommandError(outs, prefix, argdict, r) try: @@ -176,6 +177,48 @@ class CephService(object): except Exception: # pylint: disable=broad-except return outb + @staticmethod + def get_smart_data_by_device(device): + dev_id = device['devid'] + if 'daemons' in device and device['daemons']: + daemons = [daemon for daemon in device['daemons'] if daemon.startswith('osd')] + if daemons: + svc_type, svc_id = daemons[0].split('.') + dev_smart_data = CephService.send_command(svc_type, 'smart', svc_id, devid=dev_id) + for dev_id, dev_data in dev_smart_data.items(): + if 'error' in dev_data: + logger.warning( + '[SMART] error retrieving smartctl data for device ID "%s": %s', dev_id, + dev_data) + return dev_smart_data + logger.warning('[SMART] no OSD service found for device ID "%s"', dev_id) + return {} + logger.warning('[SMART] key "daemon" not found for device ID "%s"', dev_id) + return {} + + @staticmethod + def get_devices_by_host(hostname): + # (str) -> dict + return CephService.send_command('mon', 'device ls-by-host', host=hostname) + + @staticmethod + def get_smart_data_by_host(hostname): + # type: (str) -> dict + return reduce(lambda a, b: a.update(b) or a, [ + CephService.get_smart_data_by_device(device) + for device in CephService.get_devices_by_host(hostname) + ], {}) + + @staticmethod + def get_smart_data_by_daemon(daemon_type, daemon_id): + # type: (str, str) -> dict + smart_data = CephService.send_command(daemon_type, 'smart', daemon_id) + for _, dev_data in smart_data.items(): + if 'error' in dev_data: + logger.warning('[SMART] Error retrieving smartctl data for daemon "%s.%s"', + daemon_type, daemon_id) + return smart_data + @classmethod def get_rates(cls, svc_type, svc_name, path): """ -- 2.39.5