]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add smart data to hosts page 30594/head
authorPatrick Seidensal <pseidensal@suse.com>
Tue, 24 Sep 2019 20:22:22 +0000 (22:22 +0200)
committerPatrick Seidensal <pseidensal@suse.com>
Fri, 6 Dec 2019 14:23:15 +0000 (15:23 +0100)
Fixes: https://tracker.ceph.com/issues/42064
Signed-off-by: Patrick Seidensal <pseidensal@suse.com>
23 files changed:
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-details/osd-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/fixtures/smart_data_version_1_0_response.json [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-smart-list/osd-smart-list.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/fixtures/smart_data_version_1_0_response.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/smart-list/smart-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts
src/pybind/mgr/dashboard/services/ceph_service.py

index ba72bdc32ef0d72f08084beb119fed49c8955d84..b04a3d05a9f6f5b74c6f2170fcfb5c2e7b809316 100644 (file)
@@ -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)
index b36681b982058b709c7449333890155abbe2441e..70fed1d865c6be6c15e1358507d800f848baa5ae 100644 (file)
@@ -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)
 
index e3bae17c0cafe6ae6152fb46ed03cf46052b0fca..4a11c8e63bc29119578a3bab222bb9451496ea7b 100644 (file)
@@ -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 {}
index c38b669c82b74d9f35ba4df5a0ce97f21d5817c2..416aa2de3fee266435eb8c97153dec37d683b6ca 100644 (file)
@@ -6,23 +6,32 @@
   <tab i18n-heading
        heading="Inventory"
        *ngIf="permissions.hosts.read">
-    <cd-inventory [hostname]="selection.first()['hostname']">
-    </cd-inventory>
+    <cd-inventory [hostname]="selectedHostname"></cd-inventory>
   </tab>
   <tab i18n-heading
        heading="Services"
        *ngIf="permissions.hosts.read">
     <cd-services
-      [hostname]="selection.first()['hostname']"
+      [hostname]="selectedHostname"
       [hiddenColumns]="['nodename']">
     </cd-services>
   </tab>
   <tab i18n-heading
        heading="Performance Details"
        *ngIf="permissions.grafana.read">
-    <cd-grafana [grafanaPath]="'host-details?var-ceph_hosts=' + selection.first()['hostname']"
+    <cd-grafana [grafanaPath]="'host-details?var-ceph_hosts=' + selectedHostname"
                 uid="rtOg0AiWz"
                 grafanaStyle="three">
     </cd-grafana>
   </tab>
+  <tab heading="Device health"
+       i18n-heading>
+    <cd-smart-list *ngIf="selectedHostname; else noHostname"
+                   [hostname]="selectedHostname"></cd-smart-list>
+  </tab>
 </tabset>
+
+<ng-template #noHostname>
+  <cd-alert-panel type="error"
+                  i18n>No hostname found.</cd-alert-panel>
+</ng-template>
index ebe9f097e7204efdc32682444389854d8f4d86f0..224f4c5f3f69d603e56ccf37b45c1e48d2cf0a88 100644 (file)
@@ -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'
+      ]);
     });
   });
 });
index d66a7f87fd359f8475dc5d6c414902648cd86480..469c4c3ab7ed1b42fe5a2187ad8429f6abd3f268 100644 (file)
@@ -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() {}
 }
index 9ad1fde69443257ce14277eecbfbe2db9744b392..b2247f90b9ad974a87122ad89c13c4939319dc4d 100644 (file)
@@ -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(),
index 1f4b137aa6f193a301e4f989e0d58ca5016a7b93..67ca9683be9ef3a62e5f8e14d9b8dcab935f8705 100644 (file)
@@ -27,7 +27,7 @@
 
   <tab heading="Device health"
        i18n-heading>
-    <cd-osd-smart-list [osdId]="osd.id"></cd-osd-smart-list>
+    <cd-smart-list [osdId]="osd.id"></cd-smart-list>
   </tab>
 
   <tab heading="Performance counter"
index 5ab90157ed95f83e773cd5c92197c12e101f1ef6..b372dde17fa4f283a86f801d62c2ed810e3b5055 100644 (file)
@@ -2,16 +2,17 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { DebugElement } from '@angular/core';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { of } from 'rxjs';
-
 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 { OsdService } from '../../../../shared/api/osd.service';
 import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { CephModule } from '../../../ceph.module';
-import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
+import { SharedModule } from '../../../../shared/shared.module';
+import { TablePerformanceCounterComponent } from '../../../performance-counter/table-performance-counter/table-performance-counter.component';
+import { DeviceListComponent } from '../../../shared/device-list/device-list.component';
+import { SmartListComponent } from '../../../shared/smart-list/smart-list.component';
+import { OsdPerformanceHistogramComponent } from '../osd-performance-histogram/osd-performance-histogram.component';
 import { OsdDetailsComponent } from './osd-details.component';
 
 describe('OsdDetailsComponent', () => {
@@ -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 (file)
index 514c296..0000000
+++ /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 (file)
index 588eb0f..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-<ng-container *ngIf="!loading; else isLoading">
-  <ng-container *ngIf="incompatible; else isCompatible">
-    <cd-alert-panel type="warning"
-                    i18n>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</cd-alert-panel>
-  </ng-container>
-  <ng-template #isCompatible>
-    <tabset *ngFor="let device of data | keyvalue">
-      <tab [heading]="device.value.device + ' (' + device.value.identifier + ')'">
-        <ng-container *ngIf="device.value.error; else noError">
-          <cd-alert-panel type="warning">{{ device.value.userMessage }}</cd-alert-panel>
-        </ng-container>
-        <ng-template #noError>
-          <ng-container *ngIf="device.value.smart.data.self_test.status.passed; else selfTestFailed">
-            <cd-alert-panel
-              size="slim"
-              type="info"
-              i18n-title
-              title="SMART overall-health self-assessment test result">
-              {{ device.value.smart.data.self_test.status.string }}
-            </cd-alert-panel>
-          </ng-container>
-          <ng-template #selfTestFailed>
-            <cd-alert-panel
-              size="slim"
-              type="warning"
-              i18n-title
-              title="SMART overall-health self-assessment test result">
-              {{ device.value.smart.data.self_test.status.string }}
-            </cd-alert-panel>
-          </ng-template>
-          <tabset>
-            <tab i18n-heading
-                 heading="Device Information">
-              <cd-table-key-value [renderObjects]="true"
-                                  [data]="device.value.info"></cd-table-key-value>
-            </tab>
-
-            <tab i18n-heading
-                 heading="S.M.A.R.T">
-              <cd-table [data]="device.value.smart.attributes.table"
-                        updateSelectionOnRefresh="never"
-                        [columns]="columns"></cd-table>
-            </tab>
-          </tabset>
-        </ng-template>
-      </tab>
-    </tabset>
-  </ng-template>
-</ng-container>
-<ng-template #isLoading>
-  <cd-loading-panel i18n>S.M.A.R.T data is loading.</cd-loading-panel>
-</ng-template>
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 (file)
index e69de29..0000000
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 (file)
index 8267284..0000000
+++ /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<OsdSmartListComponent>;
-  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 (file)
index d92186f..0000000
+++ /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);
-  }
-}
index 93f4ad77e8bee524ed6481c83f9a0bfb41d81aa9..e595f07f3d9ac4bc1ef3b19986a48ca12fca1495 100644 (file)
@@ -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 (file)
index 0000000..514c296
--- /dev/null
@@ -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 (file)
index 0000000..588eb0f
--- /dev/null
@@ -0,0 +1,52 @@
+<ng-container *ngIf="!loading; else isLoading">
+  <ng-container *ngIf="incompatible; else isCompatible">
+    <cd-alert-panel type="warning"
+                    i18n>The data received has the JSON format version 2.x and is currently incompatible with the dashboard.</cd-alert-panel>
+  </ng-container>
+  <ng-template #isCompatible>
+    <tabset *ngFor="let device of data | keyvalue">
+      <tab [heading]="device.value.device + ' (' + device.value.identifier + ')'">
+        <ng-container *ngIf="device.value.error; else noError">
+          <cd-alert-panel type="warning">{{ device.value.userMessage }}</cd-alert-panel>
+        </ng-container>
+        <ng-template #noError>
+          <ng-container *ngIf="device.value.smart.data.self_test.status.passed; else selfTestFailed">
+            <cd-alert-panel
+              size="slim"
+              type="info"
+              i18n-title
+              title="SMART overall-health self-assessment test result">
+              {{ device.value.smart.data.self_test.status.string }}
+            </cd-alert-panel>
+          </ng-container>
+          <ng-template #selfTestFailed>
+            <cd-alert-panel
+              size="slim"
+              type="warning"
+              i18n-title
+              title="SMART overall-health self-assessment test result">
+              {{ device.value.smart.data.self_test.status.string }}
+            </cd-alert-panel>
+          </ng-template>
+          <tabset>
+            <tab i18n-heading
+                 heading="Device Information">
+              <cd-table-key-value [renderObjects]="true"
+                                  [data]="device.value.info"></cd-table-key-value>
+            </tab>
+
+            <tab i18n-heading
+                 heading="S.M.A.R.T">
+              <cd-table [data]="device.value.smart.attributes.table"
+                        updateSelectionOnRefresh="never"
+                        [columns]="columns"></cd-table>
+            </tab>
+          </tabset>
+        </ng-template>
+      </tab>
+    </tabset>
+  </ng-template>
+</ng-container>
+<ng-template #isLoading>
+  <cd-loading-panel i18n>S.M.A.R.T data is loading.</cd-loading-panel>
+</ng-template>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..3f11c8c
--- /dev/null
@@ -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<SmartListComponent>;
+  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 (file)
index 0000000..3938b88
--- /dev/null
@@ -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();
+  }
+}
index 26e440d8120392d56bff0fe4574e737aa5b73b4c..b1dcaec12f8d689c8b6089ad35a84a308d89f244 100644 (file)
@@ -33,4 +33,8 @@ export class HostService {
       .get<CdDevice[]>(`${this.baseURL}/${hostname}/devices`)
       .pipe(map((devices) => devices.map((device) => this.deviceService.prepareDevice(device))));
   }
+
+  getSmartData(hostname) {
+    return this.http.get(`${this.baseURL}/${hostname}/smart`);
+  }
 }
index 824bc1c66fdb509081c8be644f9d1149d3c389fa..e8429342dfd7c039eb1c7b79a7162cdb7a1e3218 100644 (file)
@@ -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`
     );
   }
 
index e74b7f7070275c82db48b2df49caf4d9baffc76e..11f19090db7ae678a2da2e6b05bbabcf679f3191 100644 (file)
@@ -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):
         """