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):
@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)
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)
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';
ServicesComponent,
InventoryComponent,
HostFormComponent,
- OsdSmartListComponent,
OsdFormComponent,
OsdDevicesSelectionModalComponent,
InventoryDevicesComponent,
OsdCreationPreviewModalComponent,
RulesListComponent,
ActiveAlertListComponent,
- MonitoringListComponent
+ MonitoringListComponent,
+ HostFormComponent
]
})
export class ClusterModule {}
<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>
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', () => {
NgBootstrapFormValidationModule.forRoot(),
RouterTestingModule,
CephModule,
- CoreModule
+ CoreModule,
+ CephSharedModule,
+ SharedModule
],
declarations: [],
providers: [i18nProviders]
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', () => {
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'
+ ]);
});
});
});
@ViewChild(TabsetComponent, { static: false })
tabsetChild: TabsetComponent;
+ get selectedHostname(): string {
+ return this.selection.hasSelection ? this.selection.first()['hostname'] : null;
+ }
+
constructor() {}
}
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', () => {
configureTestBed({
imports: [
+ CephSharedModule,
SharedModule,
HttpClientTestingModule,
TabsModule.forRoot(),
<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"
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', () => {
let getDetailsSpy;
configureTestBed({
- imports: [
- HttpClientTestingModule,
- TabsModule.forRoot(),
- PerformanceCounterModule,
- CephModule,
- CoreModule
+ imports: [HttpClientTestingModule, TabsModule.forRoot(), SharedModule],
+ declarations: [
+ OsdDetailsComponent,
+ DeviceListComponent,
+ SmartListComponent,
+ TablePerformanceCounterComponent,
+ OsdPerformanceHistogramComponent
],
- declarations: [],
providers: i18nProviders
});
+++ /dev/null
-{
- "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
- }
- }
-}
+++ /dev/null
-<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>
+++ /dev/null
-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');
- });
-});
+++ /dev/null
-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);
- }
-}
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 {}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+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');
+ });
+});
--- /dev/null
+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();
+ }
+}
.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`);
+ }
}
*/
getSmartData(id: number) {
return this.http.get<{ [deviceId: string]: SmartDataV1 | SmartError }>(
- `${this.path}/${id}/get_smart_data`
+ `${this.path}/${id}/smart`
);
}
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-
import json
import logging
+from six.moves import reduce
import rados
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:
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):
"""