From: Patrick Seidensal Date: Tue, 24 Sep 2019 20:22:22 +0000 (+0200) Subject: mgr/dashboard: add smart data to hosts page X-Git-Tag: v15.1.0~612^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=6b717d5fbab12b946cde53f378e9669b1f5a3772;p=ceph-ci.git mgr/dashboard: add smart data to hosts page Fixes: https://tracker.ceph.com/issues/42064 Signed-off-by: Patrick Seidensal --- 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/fixtures/smart_data_version_1_0_response.json b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/fixtures/smart_data_version_1_0_response.json deleted file mode 100644 index 514c2966c04..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/fixtures/smart_data_version_1_0_response.json +++ /dev/null @@ -1,570 +0,0 @@ -{ - "WDC_WD1003FBYX-01Y7B1_WD-WCAW11111111": { - "ata_sct_capabilities": { - "data_table_supported": true, - "error_recovery_control_supported": true, - "feature_control_supported": true, - "value": 12351 - }, - "ata_smart_attributes": { - "revision": 16, - "table": [ - { - "flags": { - "auto_keep": true, - "error_rate": true, - "event_count": false, - "performance": true, - "prefailure": true, - "string": "POSR-K ", - "updated_online": true, - "value": 47 - }, - "id": 1, - "name": "Raw_Read_Error_Rate", - "raw": { - "string": "1", - "value": 1 - }, - "thresh": 51, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": false, - "performance": true, - "prefailure": true, - "string": "POS--K ", - "updated_online": true, - "value": 39 - }, - "id": 3, - "name": "Spin_Up_Time", - "raw": { - "string": "4250", - "value": 4250 - }, - "thresh": 21, - "value": 175, - "when_failed": "", - "worst": 172 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 4, - "name": "Start_Stop_Count", - "raw": { - "string": "1657", - "value": 1657 - }, - "thresh": 0, - "value": 99, - "when_failed": "", - "worst": 99 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": true, - "string": "PO--CK ", - "updated_online": true, - "value": 51 - }, - "id": 5, - "name": "Reallocated_Sector_Ct", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 140, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": true, - "event_count": false, - "performance": true, - "prefailure": false, - "string": "-OSR-K ", - "updated_online": true, - "value": 46 - }, - "id": 7, - "name": "Seek_Error_Rate", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 9, - "name": "Power_On_Hours", - "raw": { - "string": "15807", - "value": 15807 - }, - "thresh": 0, - "value": 79, - "when_failed": "", - "worst": 79 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 10, - "name": "Spin_Retry_Count", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 100, - "when_failed": "", - "worst": 100 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 11, - "name": "Calibration_Retry_Count", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 100, - "when_failed": "", - "worst": 100 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 12, - "name": "Power_Cycle_Count", - "raw": { - "string": "1370", - "value": 1370 - }, - "thresh": 0, - "value": 99, - "when_failed": "", - "worst": 99 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 192, - "name": "Power-Off_Retract_Count", - "raw": { - "string": "111", - "value": 111 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 193, - "name": "Load_Cycle_Count", - "raw": { - "string": "1545", - "value": 1545 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": false, - "performance": false, - "prefailure": false, - "string": "-O---K ", - "updated_online": true, - "value": 34 - }, - "id": 194, - "name": "Temperature_Celsius", - "raw": { - "string": "47", - "value": 47 - }, - "thresh": 0, - "value": 100, - "when_failed": "", - "worst": 89 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 196, - "name": "Reallocated_Event_Count", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 197, - "name": "Current_Pending_Sector", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "----CK ", - "updated_online": false, - "value": 48 - }, - "id": 198, - "name": "Offline_Uncorrectable", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": true, - "error_rate": false, - "event_count": true, - "performance": false, - "prefailure": false, - "string": "-O--CK ", - "updated_online": true, - "value": 50 - }, - "id": 199, - "name": "UDMA_CRC_Error_Count", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - }, - { - "flags": { - "auto_keep": false, - "error_rate": true, - "event_count": false, - "performance": false, - "prefailure": false, - "string": "---R-- ", - "updated_online": false, - "value": 8 - }, - "id": 200, - "name": "Multi_Zone_Error_Rate", - "raw": { - "string": "0", - "value": 0 - }, - "thresh": 0, - "value": 200, - "when_failed": "", - "worst": 200 - } - ] - }, - "ata_smart_data": { - "capabilities": { - "attribute_autosave_enabled": true, - "conveyance_self_test_supported": true, - "error_logging_supported": true, - "exec_offline_immediate_supported": true, - "gp_logging_supported": true, - "offline_is_aborted_upon_new_cmd": false, - "offline_surface_scan_supported": true, - "selective_self_test_supported": true, - "self_tests_supported": true, - "values": [ - 123, - 3 - ] - }, - "offline_data_collection": { - "completion_seconds": 16500, - "status": { - "string": "was suspended by an interrupting command from host", - "value": 132 - } - }, - "self_test": { - "polling_minutes": { - "conveyance": 5, - "extended": 162, - "short": 2 - }, - "status": { - "passed": true, - "string": "completed without error", - "value": 0 - } - } - }, - "ata_smart_error_log": { - "summary": { - "count": 0, - "revision": 1 - } - }, - "ata_smart_selective_self_test_log": { - "flags": { - "remainder_scan_enabled": false, - "value": 0 - }, - "power_up_scan_resume_minutes": 0, - "revision": 1, - "table": [ - { - "lba_max": 0, - "lba_min": 0, - "status": { - "string": "Not_testing", - "value": 0 - } - }, - { - "lba_max": 0, - "lba_min": 0, - "status": { - "string": "Not_testing", - "value": 0 - } - }, - { - "lba_max": 0, - "lba_min": 0, - "status": { - "string": "Not_testing", - "value": 0 - } - }, - { - "lba_max": 0, - "lba_min": 0, - "status": { - "string": "Not_testing", - "value": 0 - } - }, - { - "lba_max": 0, - "lba_min": 0, - "status": { - "string": "Not_testing", - "value": 0 - } - } - ] - }, - "ata_smart_self_test_log": { - "standard": { - "count": 0, - "revision": 1 - } - }, - "ata_version": { - "major_value": 510, - "minor_value": 0, - "string": "ATA8-ACS (minor revision not indicated)" - }, - "device": { - "info_name": "/dev/sde [SAT]", - "name": "/dev/sde", - "protocol": "ATA", - "type": "sat" - }, - "firmware_version": "01.01V02", - "in_smartctl_database": true, - "interface_speed": { - "current": { - "bits_per_unit": 100000000, - "sata_value": 2, - "string": "3.0 Gb/s", - "units_per_second": 30 - }, - "max": { - "bits_per_unit": 100000000, - "sata_value": 6, - "string": "3.0 Gb/s", - "units_per_second": 30 - } - }, - "json_format_version": [ - 1, - 0 - ], - "local_time": { - "asctime": "Mon Sep 2 12:39:01 2019 UTC", - "time_t": 1567427941 - }, - "logical_block_size": 512, - "model_family": "Western Digital RE4", - "model_name": "WDC WD1003FBYX-01Y7B1", - "nvme_smart_health_information_add_log_error": "nvme returned an error: sudo: exit status: 1", - "nvme_smart_health_information_add_log_error_code": -22, - "nvme_vendor": "wdc_wd1003fbyx-01y7b1", - "physical_block_size": 512, - "power_cycle_count": 1370, - "power_on_time": { - "hours": 15807 - }, - "rotation_rate": 7200, - "sata_version": { - "string": "SATA 3.0", - "value": 63 - }, - "serial_number": "WD-WCAW11111111", - "smart_status": { - "passed": true - }, - "smartctl": { - "argv": [ - "smartctl", - "-a", - "/dev/sde", - "--json" - ], - "build_info": "(SUSE RPM)", - "exit_status": 0, - "platform_info": "x86_64-linux-5.0.0-25-generic", - "svn_revision": "4917", - "version": [ - 7, - 0 - ] - }, - "temperature": { - "current": 47 - }, - "user_capacity": { - "blocks": 1953525168, - "bytes": 1000204886016 - }, - "wwn": { - "id": 11601695629, - "naa": 5, - "oui": 5358 - } - } -} 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/cluster/osd/osd-smart-list/osd-smart-list.component.html deleted file mode 100644 index 588eb0fbd92..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.html +++ /dev/null @@ -1,52 +0,0 @@ - - - The data received has the JSON format version 2.x and is currently incompatible with the dashboard. - - - - - - {{ device.value.userMessage }} - - - - - {{ device.value.smart.data.self_test.status.string }} - - - - - {{ device.value.smart.data.self_test.status.string }} - - - - - - - - - - - - - - - - - - S.M.A.R.T data is loading. - 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/cluster/osd/osd-smart-list/osd-smart-list.component.scss deleted file mode 100644 index e69de29bb2d..00000000000 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/cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts deleted file mode 100644 index 826728477c4..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { SimpleChange, SimpleChanges } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -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'; - -describe('OsdSmartListComponent', () => { - let component: OsdSmartListComponent; - let fixture: ComponentFixture; - let osdService: OsdService; - - const SMART_DATA_VERSION_1_0 = require('./fixtures/smart_data_version_1_0_response.json'); - - const spyOnGetSmartData = (fn: (id: number) => any) => - spyOn(osdService, 'getSmartData').and.callFake(fn); - - /** - * Initializes the component after it spied upon the `getSmartData()` method of `OsdService`. - * @param data The data to be used to return when `getSmartData()` is called. - * @param simpleChanges (optional) The changes to be used for `ngOnChanges()` method. - */ - const initializeComponentWithData = (data?: any, simpleChanges?: SimpleChanges) => { - spyOnGetSmartData(() => of(data || SMART_DATA_VERSION_1_0)); - component.ngOnInit(); - const changes: SimpleChanges = simpleChanges || { - osdId: new SimpleChange(null, 0, true) - }; - component.ngOnChanges(changes); - }; - - /** - * Sets attributes for _all_ returned devices according to the given path. The syntax is the same - * as used in lodash.set(). - * - * @example - * patchData('json_format_version', [2, 0]) // sets the value of `json_format_version` to [2, 0] - * // for all devices - * - * patchData('json_format_version[0]', 2) // same result - * - * @param path The path to the attribute - * @param newValue The new value - */ - const patchData = (path: string, newValue: any): any => { - return _.reduce( - _.cloneDeep(SMART_DATA_VERSION_1_0), - (result, dataObj, deviceId) => { - result[deviceId] = _.set(dataObj, path, newValue); - return result; - }, - {} - ); - }; - - configureTestBed({ - declarations: [OsdSmartListComponent], - imports: [TabsModule, SharedModule, HttpClientTestingModule], - providers: [i18nProviders, TabsetComponent, TabsetConfig] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(OsdSmartListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - - osdService = TestBed.get(OsdService); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('tests version 1.x', () => { - beforeEach(() => initializeComponentWithData()); - - it('should return with proper keys', () => { - _.each(component.data, (smartData, _deviceId) => { - expect(_.keys(smartData)).toEqual(['info', 'smart', 'device', 'identifier']); - }); - }); - - it('should not contain excluded keys in `info`', () => { - const excludes = [ - 'ata_smart_attributes', - 'ata_smart_selective_self_test_log', - 'ata_smart_data' - ]; - _.each(component.data, (smartData, _deviceId) => { - _.each(excludes, (exclude) => expect(smartData.info[exclude]).toBeUndefined()); - }); - }); - }); - - it('should not work for version 2.x', () => { - initializeComponentWithData(patchData('json_format_version', [2, 0])); - expect(component.data).toEqual({}); - expect(component.incompatible).toBeTruthy(); - }); - - it('should display info panel for passed self test', () => { - initializeComponentWithData(); - fixture.detectChanges(); - const alertPanel = fixture.debugElement.query(By.css('cd-alert-panel')); - expect(component.incompatible).toBe(false); - expect(component.loading).toBe(false); - expect(alertPanel.attributes.size).toBe('slim'); - expect(alertPanel.attributes.title).toBe('SMART overall-health self-assessment test result'); - expect(alertPanel.attributes.type).toBe('info'); - }); - - it('should display warning panel for failed self test', () => { - initializeComponentWithData(patchData('ata_smart_data.self_test.status.passed', false)); - fixture.detectChanges(); - const alertPanel = fixture.debugElement.query(By.css('cd-alert-panel')); - expect(component.incompatible).toBe(false); - expect(component.loading).toBe(false); - expect(alertPanel.attributes.size).toBe('slim'); - expect(alertPanel.attributes.title).toBe('SMART overall-health self-assessment test result'); - expect(alertPanel.attributes.type).toBe('warning'); - }); -}); 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/shared/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 new file mode 100644 index 00000000000..514c2966c04 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_response.json @@ -0,0 +1,570 @@ +{ + "WDC_WD1003FBYX-01Y7B1_WD-WCAW11111111": { + "ata_sct_capabilities": { + "data_table_supported": true, + "error_recovery_control_supported": true, + "feature_control_supported": true, + "value": 12351 + }, + "ata_smart_attributes": { + "revision": 16, + "table": [ + { + "flags": { + "auto_keep": true, + "error_rate": true, + "event_count": false, + "performance": true, + "prefailure": true, + "string": "POSR-K ", + "updated_online": true, + "value": 47 + }, + "id": 1, + "name": "Raw_Read_Error_Rate", + "raw": { + "string": "1", + "value": 1 + }, + "thresh": 51, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": false, + "performance": true, + "prefailure": true, + "string": "POS--K ", + "updated_online": true, + "value": 39 + }, + "id": 3, + "name": "Spin_Up_Time", + "raw": { + "string": "4250", + "value": 4250 + }, + "thresh": 21, + "value": 175, + "when_failed": "", + "worst": 172 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 4, + "name": "Start_Stop_Count", + "raw": { + "string": "1657", + "value": 1657 + }, + "thresh": 0, + "value": 99, + "when_failed": "", + "worst": 99 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": true, + "string": "PO--CK ", + "updated_online": true, + "value": 51 + }, + "id": 5, + "name": "Reallocated_Sector_Ct", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 140, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": true, + "event_count": false, + "performance": true, + "prefailure": false, + "string": "-OSR-K ", + "updated_online": true, + "value": 46 + }, + "id": 7, + "name": "Seek_Error_Rate", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 9, + "name": "Power_On_Hours", + "raw": { + "string": "15807", + "value": 15807 + }, + "thresh": 0, + "value": 79, + "when_failed": "", + "worst": 79 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 10, + "name": "Spin_Retry_Count", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 100, + "when_failed": "", + "worst": 100 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 11, + "name": "Calibration_Retry_Count", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 100, + "when_failed": "", + "worst": 100 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 12, + "name": "Power_Cycle_Count", + "raw": { + "string": "1370", + "value": 1370 + }, + "thresh": 0, + "value": 99, + "when_failed": "", + "worst": 99 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 192, + "name": "Power-Off_Retract_Count", + "raw": { + "string": "111", + "value": 111 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 193, + "name": "Load_Cycle_Count", + "raw": { + "string": "1545", + "value": 1545 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": false, + "performance": false, + "prefailure": false, + "string": "-O---K ", + "updated_online": true, + "value": 34 + }, + "id": 194, + "name": "Temperature_Celsius", + "raw": { + "string": "47", + "value": 47 + }, + "thresh": 0, + "value": 100, + "when_failed": "", + "worst": 89 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 196, + "name": "Reallocated_Event_Count", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 197, + "name": "Current_Pending_Sector", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "----CK ", + "updated_online": false, + "value": 48 + }, + "id": 198, + "name": "Offline_Uncorrectable", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": true, + "error_rate": false, + "event_count": true, + "performance": false, + "prefailure": false, + "string": "-O--CK ", + "updated_online": true, + "value": 50 + }, + "id": 199, + "name": "UDMA_CRC_Error_Count", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + }, + { + "flags": { + "auto_keep": false, + "error_rate": true, + "event_count": false, + "performance": false, + "prefailure": false, + "string": "---R-- ", + "updated_online": false, + "value": 8 + }, + "id": 200, + "name": "Multi_Zone_Error_Rate", + "raw": { + "string": "0", + "value": 0 + }, + "thresh": 0, + "value": 200, + "when_failed": "", + "worst": 200 + } + ] + }, + "ata_smart_data": { + "capabilities": { + "attribute_autosave_enabled": true, + "conveyance_self_test_supported": true, + "error_logging_supported": true, + "exec_offline_immediate_supported": true, + "gp_logging_supported": true, + "offline_is_aborted_upon_new_cmd": false, + "offline_surface_scan_supported": true, + "selective_self_test_supported": true, + "self_tests_supported": true, + "values": [ + 123, + 3 + ] + }, + "offline_data_collection": { + "completion_seconds": 16500, + "status": { + "string": "was suspended by an interrupting command from host", + "value": 132 + } + }, + "self_test": { + "polling_minutes": { + "conveyance": 5, + "extended": 162, + "short": 2 + }, + "status": { + "passed": true, + "string": "completed without error", + "value": 0 + } + } + }, + "ata_smart_error_log": { + "summary": { + "count": 0, + "revision": 1 + } + }, + "ata_smart_selective_self_test_log": { + "flags": { + "remainder_scan_enabled": false, + "value": 0 + }, + "power_up_scan_resume_minutes": 0, + "revision": 1, + "table": [ + { + "lba_max": 0, + "lba_min": 0, + "status": { + "string": "Not_testing", + "value": 0 + } + }, + { + "lba_max": 0, + "lba_min": 0, + "status": { + "string": "Not_testing", + "value": 0 + } + }, + { + "lba_max": 0, + "lba_min": 0, + "status": { + "string": "Not_testing", + "value": 0 + } + }, + { + "lba_max": 0, + "lba_min": 0, + "status": { + "string": "Not_testing", + "value": 0 + } + }, + { + "lba_max": 0, + "lba_min": 0, + "status": { + "string": "Not_testing", + "value": 0 + } + } + ] + }, + "ata_smart_self_test_log": { + "standard": { + "count": 0, + "revision": 1 + } + }, + "ata_version": { + "major_value": 510, + "minor_value": 0, + "string": "ATA8-ACS (minor revision not indicated)" + }, + "device": { + "info_name": "/dev/sde [SAT]", + "name": "/dev/sde", + "protocol": "ATA", + "type": "sat" + }, + "firmware_version": "01.01V02", + "in_smartctl_database": true, + "interface_speed": { + "current": { + "bits_per_unit": 100000000, + "sata_value": 2, + "string": "3.0 Gb/s", + "units_per_second": 30 + }, + "max": { + "bits_per_unit": 100000000, + "sata_value": 6, + "string": "3.0 Gb/s", + "units_per_second": 30 + } + }, + "json_format_version": [ + 1, + 0 + ], + "local_time": { + "asctime": "Mon Sep 2 12:39:01 2019 UTC", + "time_t": 1567427941 + }, + "logical_block_size": 512, + "model_family": "Western Digital RE4", + "model_name": "WDC WD1003FBYX-01Y7B1", + "nvme_smart_health_information_add_log_error": "nvme returned an error: sudo: exit status: 1", + "nvme_smart_health_information_add_log_error_code": -22, + "nvme_vendor": "wdc_wd1003fbyx-01y7b1", + "physical_block_size": 512, + "power_cycle_count": 1370, + "power_on_time": { + "hours": 15807 + }, + "rotation_rate": 7200, + "sata_version": { + "string": "SATA 3.0", + "value": 63 + }, + "serial_number": "WD-WCAW11111111", + "smart_status": { + "passed": true + }, + "smartctl": { + "argv": [ + "smartctl", + "-a", + "/dev/sde", + "--json" + ], + "build_info": "(SUSE RPM)", + "exit_status": 0, + "platform_info": "x86_64-linux-5.0.0-25-generic", + "svn_revision": "4917", + "version": [ + 7, + 0 + ] + }, + "temperature": { + "current": 47 + }, + "user_capacity": { + "blocks": 1953525168, + "bytes": 1000204886016 + }, + "wwn": { + "id": 11601695629, + "naa": 5, + "oui": 5358 + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html new file mode 100644 index 00000000000..588eb0fbd92 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html @@ -0,0 +1,52 @@ + + + The data received has the JSON format version 2.x and is currently incompatible with the dashboard. + + + + + + {{ device.value.userMessage }} + + + + + {{ device.value.smart.data.self_test.status.string }} + + + + + {{ device.value.smart.data.self_test.status.string }} + + + + + + + + + + + + + + + + + + S.M.A.R.T data is loading. + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts new file mode 100644 index 00000000000..3f11c8c7faa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts @@ -0,0 +1,129 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SimpleChange, SimpleChanges } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +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 { SmartListComponent } from './smart-list.component'; + +describe('OsdSmartListComponent', () => { + let component: SmartListComponent; + let fixture: ComponentFixture; + let osdService: OsdService; + + const SMART_DATA_VERSION_1_0 = require('./fixtures/smart_data_version_1_0_response.json'); + + const spyOnGetSmartData = (fn: (id: number) => any) => + spyOn(osdService, 'getSmartData').and.callFake(fn); + + /** + * Initializes the component after it spied upon the `getSmartData()` method of `OsdService`. + * @param data The data to be used to return when `getSmartData()` is called. + * @param simpleChanges (optional) The changes to be used for `ngOnChanges()` method. + */ + const initializeComponentWithData = (data?: any, simpleChanges?: SimpleChanges) => { + spyOnGetSmartData(() => of(data || SMART_DATA_VERSION_1_0)); + component.ngOnInit(); + const changes: SimpleChanges = simpleChanges || { + osdId: new SimpleChange(null, 0, true) + }; + component.ngOnChanges(changes); + }; + + /** + * Sets attributes for _all_ returned devices according to the given path. The syntax is the same + * as used in lodash.set(). + * + * @example + * patchData('json_format_version', [2, 0]) // sets the value of `json_format_version` to [2, 0] + * // for all devices + * + * patchData('json_format_version[0]', 2) // same result + * + * @param path The path to the attribute + * @param newValue The new value + */ + const patchData = (path: string, newValue: any): any => { + return _.reduce( + _.cloneDeep(SMART_DATA_VERSION_1_0), + (result, dataObj, deviceId) => { + result[deviceId] = _.set(dataObj, path, newValue); + return result; + }, + {} + ); + }; + + configureTestBed({ + declarations: [SmartListComponent], + imports: [TabsModule, SharedModule, HttpClientTestingModule], + providers: [i18nProviders, TabsetComponent, TabsetConfig] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SmartListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + osdService = TestBed.get(OsdService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('tests version 1.x', () => { + beforeEach(() => initializeComponentWithData()); + + it('should return with proper keys', () => { + _.each(component.data, (smartData, _deviceId) => { + expect(_.keys(smartData)).toEqual(['info', 'smart', 'device', 'identifier']); + }); + }); + + it('should not contain excluded keys in `info`', () => { + const excludes = [ + 'ata_smart_attributes', + 'ata_smart_selective_self_test_log', + 'ata_smart_data' + ]; + _.each(component.data, (smartData, _deviceId) => { + _.each(excludes, (exclude) => expect(smartData.info[exclude]).toBeUndefined()); + }); + }); + }); + + it('should not work for version 2.x', () => { + initializeComponentWithData(patchData('json_format_version', [2, 0])); + expect(component.data).toEqual({}); + expect(component.incompatible).toBeTruthy(); + }); + + it('should display info panel for passed self test', () => { + initializeComponentWithData(); + fixture.detectChanges(); + const alertPanel = fixture.debugElement.query(By.css('cd-alert-panel')); + expect(component.incompatible).toBe(false); + expect(component.loading).toBe(false); + expect(alertPanel.attributes.size).toBe('slim'); + expect(alertPanel.attributes.title).toBe('SMART overall-health self-assessment test result'); + expect(alertPanel.attributes.type).toBe('info'); + }); + + it('should display warning panel for failed self test', () => { + initializeComponentWithData(patchData('ata_smart_data.self_test.status.passed', false)); + fixture.detectChanges(); + const alertPanel = fixture.debugElement.query(By.css('cd-alert-panel')); + expect(component.incompatible).toBe(false); + expect(component.loading).toBe(false); + expect(alertPanel.attributes.size).toBe('slim'); + expect(alertPanel.attributes.title).toBe('SMART overall-health self-assessment test result'); + expect(alertPanel.attributes.type).toBe('warning'); + }); +}); 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): """