diff --git a/.gitignore b/.gitignore index 357e66f..338cb7a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ debug/ Manifest smartctl_exporter *.exe +.aider* diff --git a/go.mod b/go.mod index 1309cb5..6efeeaf 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.22 require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/prometheus/client_golang v1.20.5 + github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.61.0 github.com/prometheus/exporter-toolkit v0.13.2 + github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.18.0 ) @@ -15,13 +17,14 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/prometheus/client_model v0.6.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect @@ -34,4 +37,5 @@ require ( golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/metrics.go b/metrics.go index 0ac083f..b6a1711 100644 --- a/metrics.go +++ b/metrics.go @@ -354,4 +354,27 @@ var ( }, nil, ) + metricDeviceLastSelfTest = prometheus.NewDesc( + "smartctl_device_last_self_test", + "Device last SMART self test status", + []string{ + "device", + "type", + "lifetime_hours", + }, + nil, + ) + + metricDeviceLastSelfTestInfo = prometheus.NewDesc( + "smartctl_device_last_self_test_info", + "Device last SMART self test info", + []string{ + "device", + "type", + "lifetime_hours", + "status", + "description", + }, + nil, + ) ) diff --git a/readjson.go b/readjson.go index 81f7b12..fffcbdd 100644 --- a/readjson.go +++ b/readjson.go @@ -63,7 +63,7 @@ func readFakeSMARTctl(logger *slog.Logger, device Device) gjson.Result { // Get json from smartctl and parse it func readSMARTctl(logger *slog.Logger, device Device) (gjson.Result, bool) { start := time.Now() - var smartctlArgs = []string{"--json", "--info", "--health", "--attributes", "--tolerance=verypermissive", "--nocheck=standby", "--format=brief", "--log=error", "--device=" + device.Type, device.Name} + var smartctlArgs = []string{"--json", "--info", "--health", "--attributes", "--tolerance=verypermissive", "--nocheck=standby", "--format=brief", "--log=error", "--log=selftest", "--device=" + device.Type, device.Name} logger.Debug("Calling smartctl with args", "args", strings.Join(smartctlArgs, " ")) out, err := exec.Command(*smartctlPath, smartctlArgs...).Output() diff --git a/smartctl.go b/smartctl.go index eb94130..1641eb0 100644 --- a/smartctl.go +++ b/smartctl.go @@ -541,6 +541,48 @@ func (smart *SMARTctl) mineDeviceErrorLog() { } func (smart *SMARTctl) mineDeviceSelfTestLog() { + // For SCSI devices + if smart.device.interface_ == "scsi" { + var lastTest gjson.Result + var maxHours int64 + + // Find the most recent test by power_on_time.hours + smart.json.ForEach(func(key, entry gjson.Result) bool { + if strings.HasPrefix(key.String(), "scsi_self_test_") { + hours := entry.Get("power_on_time.hours").Int() + if hours > maxHours { + maxHours = hours + lastTest = entry + } + } + return true + }) + + if lastTest.Exists() { + smart.ch <- prometheus.MustNewConstMetric( + metricDeviceLastSelfTest, + prometheus.GaugeValue, + lastTest.Get("result.value").Float(), + smart.device.device, + lastTest.Get("code.string").String(), + fmt.Sprintf("%d", maxHours), + ) + + smart.ch <- prometheus.MustNewConstMetric( + metricDeviceLastSelfTestInfo, + prometheus.GaugeValue, + 1.0, + smart.device.device, + lastTest.Get("code.string").String(), + fmt.Sprintf("%d", maxHours), + fmt.Sprintf("%d", lastTest.Get("result.value").Int()), + lastTest.Get("result.string").String(), + ) + } + return + } + + // Original ATA/SAS handling for logType, status := range smart.json.Get("ata_smart_self_test_log").Map() { smart.ch <- prometheus.MustNewConstMetric( metricDeviceSelfTestLogCount, @@ -556,6 +598,31 @@ func (smart *SMARTctl) mineDeviceSelfTestLog() { smart.device.device, logType, ) + + // Get the first (most recent) test from the table if it exists + table := status.Get("table") + if table.Exists() && len(table.Array()) > 0 { + lastTest := table.Array()[0] + smart.ch <- prometheus.MustNewConstMetric( + metricDeviceLastSelfTest, + prometheus.GaugeValue, + lastTest.Get("status.value").Float(), + smart.device.device, + lastTest.Get("type.string").String(), + fmt.Sprintf("%d", lastTest.Get("lifetime_hours").Int()), + ) + + smart.ch <- prometheus.MustNewConstMetric( + metricDeviceLastSelfTestInfo, + prometheus.GaugeValue, + 1, + smart.device.device, + lastTest.Get("type.string").String(), + fmt.Sprintf("%d", lastTest.Get("lifetime_hours").Int()), + fmt.Sprintf("%d", lastTest.Get("status.value").Int()), + lastTest.Get("status.string").String(), + ) + } } } diff --git a/smartctl_test.go b/smartctl_test.go index 8c9836c..ccb94ad 100644 --- a/smartctl_test.go +++ b/smartctl_test.go @@ -14,9 +14,126 @@ package main import ( + "bytes" + "fmt" + "os" + "strconv" + "strings" "testing" + + "github.com/prometheus/client_golang/prometheus" + io_prometheus_client "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "github.com/stretchr/testify/assert" ) +// channelCollector adapts a channel-based metric producer to Prometheus Collector interface +type channelCollector struct { + metrics <-chan prometheus.Metric +} + +func (c *channelCollector) Describe(ch chan<- *prometheus.Desc) { + // No-op implementation since we're dealing with dynamic metrics from smartctl. +} + +func (c *channelCollector) Collect(ch chan<- prometheus.Metric) { + // Forward metrics from our internal channel to Prometheus + for m := range c.metrics { + ch <- m + } +} + +type MetricFamilies []*io_prometheus_client.MetricFamily + +// gathermetrics handles metric collection for testing purposes using Prometheus registry +func gathermetrics(t *testing.T, ch <-chan prometheus.Metric) MetricFamilies { + reg := prometheus.NewRegistry() + + collector := &channelCollector{metrics: ch} + if err := reg.Register(collector); err != nil { + t.Fatalf("Failed to register collector: %v", err) + } + + mfs, err := reg.Gather() + if err != nil { + t.Fatalf("Failed to gather metrics: %v", err) + } + + // Encode gathered metrics into the text format. + var buf bytes.Buffer + enc := expfmt.NewEncoder(&buf, expfmt.FmtText) + for _, mf := range mfs { + if err := enc.Encode(mf); err != nil { + t.Fatalf("failed to encode metric family: %v", err) + } + } + output := buf.String() + t.Log("Gathered metrics output:\n", output) + + return mfs +} + +func (m MetricFamilies) GetMetricFamily(name string) (*io_prometheus_client.MetricFamily, error) { + for _, mf := range m { + if mf.GetName() == name { + return mf, nil + } + } + return nil, fmt.Errorf("metric family %s not found", name) +} + +func (m MetricFamilies) GetMetricWithLabelMap(name string, labels map[string]string) (*io_prometheus_client.Metric, error) { + // Look up the metric family by name. + family, ok := m.GetMetricFamily(name) + if ok != nil { + return nil, fmt.Errorf("metric family %q not found", name) + } + + // Iterate over each metric in the family. + for _, metric := range family.Metric { + matches := true + + // For each key/value pair in the labels map, check if it exists in the metric. + for key, val := range labels { + labelFound := false + + // Check the metric's labels. + for _, lp := range metric.Label { + if lp.GetName() == key { + if lp.GetValue() == val { + labelFound = true + } + break // Found the label key, break out of the inner loop. + } + } + + // If the current label was not found or didn't match the expected value, skip this metric. + if !labelFound { + matches = false + break + } + } + + // Return the metric if all provided labels match. + if matches { + return metric, nil + } + } + + // If we get here, no matching metric was found. Build error message with available metrics. + var availableLabels []string + for _, metric := range family.Metric { + labelPairs := make([]string, 0, len(metric.Label)) + for _, lp := range metric.Label { + labelPairs = append(labelPairs, fmt.Sprintf("%s=%s", lp.GetName(), lp.GetValue())) + } + availableLabels = append(availableLabels, fmt.Sprintf("{%s}", strings.Join(labelPairs, ", "))) + } + + return nil, fmt.Errorf("metric %q with labels %v not found. Available metrics had labels: %s", + name, labels, strings.Join(availableLabels, "; ")) +} + func TestBuildDeviceLabel(t *testing.T) { tests := []struct { deviceName string @@ -42,3 +159,121 @@ func TestBuildDeviceLabel(t *testing.T) { } } } + +func readTestFile(path string) []byte { + data, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("Error reading test file: %v", err)) + } + return data +} + +func TestMineDeviceSelfTestLog(t *testing.T) { + tests := []struct { + name string + jsonFile string + want struct { + count float64 + errorTotal float64 + logType string + lastTestType string + lastTestHours string + lastTestStatus float64 + lastTestStatusDesc string + } + }{ + { + name: "Exos X16 self-test log parsing", + jsonFile: "testdata/sat-Segate_Exos_X16-ST10000NM001G-2MW103.json", + want: struct { + count float64 + errorTotal float64 + logType string + lastTestType string + lastTestHours string + lastTestStatus float64 + lastTestStatusDesc string + }{ + + count: 21, + errorTotal: 0, + logType: "standard", + lastTestType: "Short offline", + lastTestHours: "33600", + lastTestStatus: 0, + lastTestStatusDesc: "Completed without error", + }, + }, + { + name: "Scsi-seagate_ST18000NM004J", + jsonFile: "testdata/scsi-seagate_ST18000NM004J.json", + want: struct { + count float64 + errorTotal float64 + logType string + lastTestType string + lastTestHours string + lastTestStatus float64 + lastTestStatusDesc string + }{ + + count: -999, + errorTotal: -999, + logType: "", + lastTestType: "Background short", + lastTestHours: "1239", + lastTestStatus: 0, + lastTestStatusDesc: "Completed", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Read and parse test JSON + jsonRaw := readTestFile(tt.jsonFile) + jsonData := parseJSON(string(jsonRaw)) + + // Extract device name from JSON + deviceName := jsonData.Get("device.name").String() + deviceName = strings.TrimPrefix(deviceName, "/dev/") + + // Create collector and mine data + ch := make(chan prometheus.Metric, 20) + smart := NewSMARTctl(nil, jsonData, ch) + smart.mineDeviceSelfTestLog() + close(ch) + + // Get registry and metrics + mfs := gathermetrics(t, ch) + + expected := tt.want + + // Validate self test count metric if expected + if expected.count >= 0 { + metric, err := mfs.GetMetricWithLabelMap("smartctl_device_self_test_log_count", + map[string]string{"device": deviceName, "self_test_log_type": expected.logType}) + assert.NoError(t, err, "metric smartctl_device_self_test_log_count not found") + assert.Equal(t, expected.count, metric.GetGauge().GetValue(), "metric smartctl_device_self_test_log_count value") + } + + // Execute if we expect an error total metric + if expected.errorTotal >= 0 { + metric, err := mfs.GetMetricWithLabelMap("smartctl_device_self_test_log_error_count", + map[string]string{"device": deviceName, "self_test_log_type": expected.logType}) + assert.NoError(t, err, "metric smartctl_device_self_test_log_error_count not found") + assert.Equal(t, expected.errorTotal, metric.GetGauge().GetValue(), "metric smartctl_device_self_test_log_error_count value") + } + + metric, err := mfs.GetMetricWithLabelMap("smartctl_device_last_self_test", + map[string]string{"device": deviceName, "lifetime_hours": expected.lastTestHours}) + assert.NoError(t, err, "metric smartctl_device_last_self_test not found") + assert.Equal(t, expected.lastTestStatus, metric.GetGauge().GetValue(), "metric smartctl_device_last_self_test value") + + metric, err = mfs.GetMetricWithLabelMap("smartctl_device_last_self_test_info", + map[string]string{"device": deviceName, "lifetime_hours": expected.lastTestHours, "status": strconv.FormatFloat(expected.lastTestStatus, 'f', -1, 64), "description": expected.lastTestStatusDesc}) + assert.NoError(t, err, "metric smartctl_device_last_self_test_info not found") + assert.Equal(t, 1.0, metric.GetGauge().GetValue(), "metric smartctl_device_last_self_test_info value") + }) + } +} diff --git a/testdata/sat-Segate_Exos_X16-ST10000NM001G-2MW103.json b/testdata/sat-Segate_Exos_X16-ST10000NM001G-2MW103.json new file mode 100644 index 0000000..e8b1786 --- /dev/null +++ b/testdata/sat-Segate_Exos_X16-ST10000NM001G-2MW103.json @@ -0,0 +1,861 @@ +{ + "json_format_version": [ + 1, + 0 + ], + "smartctl": { + "version": [ + 7, + 4 + ], + "pre_release": false, + "svn_revision": "5530", + "platform_info": "x86_64-linux-6.8.0-51-generic", + "build_info": "(local build)", + "argv": [ + "smartctl", + "--json", + "--info", + "--health", + "--attributes", + "--tolerance=verypermissive", + "--nocheck=standby", + "--format=brief", + "--log=error", + "--log=selftest", + "/dev/sdc" + ], + "drive_database_version": { + "string": "7.3/5528" + }, + "exit_status": 0 + }, + "local_time": { + "time_t": 1738573244, + "asctime": "Mon Feb 3 10:00:44 2025 CET" + }, + "device": { + "name": "/dev/sdc", + "info_name": "/dev/sdc [SAT]", + "type": "sat", + "protocol": "ATA" + }, + "model_family": "Seagate Exos X16", + "model_name": "ST10000NM001G-2MW103", + "serial_number": "ZLW26G7Q", + "wwn": { + "naa": 5, + "oui": 3152, + "id": 3363359502 + }, + "firmware_version": "SN03", + "user_capacity": { + "blocks": 19532873728, + "bytes": 10000831348736 + }, + "logical_block_size": 512, + "physical_block_size": 4096, + "rotation_rate": 7200, + "form_factor": { + "ata_value": 2, + "name": "3.5 inches" + }, + "trim": { + "supported": false + }, + "in_smartctl_database": true, + "ata_version": { + "string": "ACS-4 (minor revision not indicated)", + "major_value": 4064, + "minor_value": 65535 + }, + "sata_version": { + "string": "SATA 3.3", + "value": 511 + }, + "interface_speed": { + "max": { + "sata_value": 14, + "string": "6.0 Gb/s", + "units_per_second": 60, + "bits_per_unit": 100000000 + }, + "current": { + "sata_value": 3, + "string": "6.0 Gb/s", + "units_per_second": 60, + "bits_per_unit": 100000000 + } + }, + "smart_support": { + "available": true, + "enabled": true + }, + "smart_status": { + "passed": true + }, + "ata_smart_attributes": { + "revision": 10, + "table": [ + { + "id": 1, + "name": "Raw_Read_Error_Rate", + "value": 79, + "worst": 64, + "thresh": 44, + "when_failed": "", + "flags": { + "value": 15, + "string": "POSR-- ", + "prefailure": true, + "updated_online": true, + "performance": true, + "error_rate": true, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 79398736, + "string": "79398736" + } + }, + { + "id": 3, + "name": "Spin_Up_Time", + "value": 91, + "worst": 91, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 3, + "string": "PO---- ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 4, + "name": "Start_Stop_Count", + "value": 100, + "worst": 100, + "thresh": 20, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 199, + "string": "199" + } + }, + { + "id": 5, + "name": "Reallocated_Sector_Ct", + "value": 100, + "worst": 100, + "thresh": 10, + "when_failed": "", + "flags": { + "value": 51, + "string": "PO--CK ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 7, + "name": "Seek_Error_Rate", + "value": 83, + "worst": 60, + "thresh": 45, + "when_failed": "", + "flags": { + "value": 15, + "string": "POSR-- ", + "prefailure": true, + "updated_online": true, + "performance": true, + "error_rate": true, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 185225413, + "string": "185225413" + } + }, + { + "id": 9, + "name": "Power_On_Hours", + "value": 62, + "worst": 62, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 33608, + "string": "33608" + } + }, + { + "id": 10, + "name": "Spin_Retry_Count", + "value": 100, + "worst": 100, + "thresh": 97, + "when_failed": "", + "flags": { + "value": 19, + "string": "PO--C- ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 12, + "name": "Power_Cycle_Count", + "value": 100, + "worst": 100, + "thresh": 20, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 196, + "string": "196" + } + }, + { + "id": 18, + "name": "Head_Health", + "value": 100, + "worst": 100, + "thresh": 50, + "when_failed": "", + "flags": { + "value": 11, + "string": "PO-R-- ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": true, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 187, + "name": "Reported_Uncorrect", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 188, + "name": "Command_Timeout", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 190, + "name": "Airflow_Temperature_Cel", + "value": 61, + "worst": 42, + "thresh": 40, + "when_failed": "", + "flags": { + "value": 34, + "string": "-O---K ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": true + }, + "raw": { + "value": 757137447, + "string": "39 (Min/Max 33/45)" + } + }, + { + "id": 192, + "name": "Power-Off_Retract_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 121, + "string": "121" + } + }, + { + "id": 193, + "name": "Load_Cycle_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 1554, + "string": "1554" + } + }, + { + "id": 194, + "name": "Temperature_Celsius", + "value": 39, + "worst": 58, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 34, + "string": "-O---K ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": true + }, + "raw": { + "value": 73014444071, + "string": "39 (0 17 0 0 0)" + } + }, + { + "id": 197, + "name": "Current_Pending_Sector", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 198, + "name": "Offline_Uncorrectable", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 16, + "string": "----C- ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 199, + "name": "UDMA_CRC_Error_Count", + "value": 200, + "worst": 200, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 62, + "string": "-OSRCK ", + "prefailure": false, + "updated_online": true, + "performance": true, + "error_rate": true, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 1, + "string": "1" + } + }, + { + "id": 200, + "name": "Pressure_Limit", + "value": 100, + "worst": 100, + "thresh": 1, + "when_failed": "", + "flags": { + "value": 35, + "string": "PO---K ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 240, + "name": "Head_Flying_Hours", + "value": 100, + "worst": 253, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 11143202455192327, + "string": "33543h+43m+14.479s" + } + }, + { + "id": 241, + "name": "Total_LBAs_Written", + "value": 100, + "worst": 253, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 157871296425, + "string": "157871296425" + } + }, + { + "id": 242, + "name": "Total_LBAs_Read", + "value": 100, + "worst": 253, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 500133612720, + "string": "500133612720" + } + } + ] + }, + "power_on_time": { + "hours": 33608 + }, + "power_cycle_count": 196, + "temperature": { + "current": 39 + }, + "ata_smart_error_log": { + "summary": { + "revision": 1, + "count": 0 + } + }, + "ata_smart_self_test_log": { + "standard": { + "revision": 1, + "table": [ + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33600 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33576 + }, + { + "type": { + "value": 2, + "string": "Extended offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33569 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33552 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33529 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33505 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33481 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33456 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33433 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33409 + }, + { + "type": { + "value": 2, + "string": "Extended offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33402 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33385 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33361 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33337 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33313 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33289 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33265 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33241 + }, + { + "type": { + "value": 2, + "string": "Extended offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33234 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33217 + }, + { + "type": { + "value": 1, + "string": "Short offline" + }, + "status": { + "value": 0, + "string": "Completed without error", + "passed": true + }, + "lifetime_hours": 33193 + } + ], + "count": 21, + "error_count_total": 0, + "error_count_outdated": 0 + } + } +} diff --git a/testdata/scsi-seagate_ST18000NM004J.json b/testdata/scsi-seagate_ST18000NM004J.json new file mode 100644 index 0000000..0f26171 --- /dev/null +++ b/testdata/scsi-seagate_ST18000NM004J.json @@ -0,0 +1,411 @@ +{ + "json_format_version": [ + 1, + 0 + ], + "smartctl": { + "version": [ + 7, + 4 + ], + "pre_release": false, + "svn_revision": "5530", + "platform_info": "x86_64-linux-6.8.0-51-generic", + "build_info": "(local build)", + "argv": [ + "smartctl", + "--json", + "--info", + "--health", + "--attributes", + "--tolerance=verypermissive", + "--nocheck=standby", + "--format=brief", + "--log=error", + "--log=selftest", + "/dev/sdb" + ], + "exit_status": 0 + }, + "local_time": { + "time_t": 1738581435, + "asctime": "Mon Feb 3 12:17:15 2025 CET" + }, + "device": { + "name": "/dev/sdb", + "info_name": "/dev/sdb", + "type": "scsi", + "protocol": "SCSI" + }, + "scsi_vendor": "SEAGATE", + "scsi_product": "ST18000NM004J", + "scsi_model_name": "SEAGATE ST18000NM004J", + "scsi_revision": "E002", + "scsi_version": "SPC-5", + "user_capacity": { + "blocks": 35156656128, + "bytes": 18000207937536 + }, + "logical_block_size": 512, + "physical_block_size": 4096, + "scsi_lb_provisioning": { + "name": "fully provisioned", + "value": 0, + "management_enabled": { + "name": "LBPME", + "value": 0 + }, + "read_zeros": { + "name": "LBPRZ", + "value": 0 + } + }, + "rotation_rate": 7200, + "form_factor": { + "scsi_value": 2, + "name": "3.5 inches" + }, + "logical_unit_id": "0x5000c500d7c94dff", + "serial_number": "ZR51ZEKN0000C2061A1W", + "device_type": { + "scsi_terminology": "Peripheral Device Type [PDT]", + "scsi_value": 0, + "name": "disk" + }, + "scsi_transport_protocol": { + "name": "SAS (SPL-4)", + "value": 6 + }, + "smart_support": { + "available": true, + "enabled": true + }, + "temperature_warning": { + "enabled": true + }, + "smart_status": { + "passed": true + }, + "temperature": { + "current": 33, + "drive_trip": 60 + }, + "power_on_time": { + "hours": 1249, + "minutes": 29 + }, + "scsi_start_stop_cycle_counter": { + "year_of_manufacture": "2021", + "week_of_manufacture": "36", + "specified_cycle_count_over_device_lifetime": 50000, + "accumulated_start_stop_cycles": 4, + "specified_load_unload_count_over_device_lifetime": 600000, + "accumulated_load_unload_cycles": 54 + }, + "scsi_grown_defect_list": 0, + "scsi_error_counter_log": { + "read": { + "errors_corrected_by_eccfast": 0, + "errors_corrected_by_eccdelayed": 0, + "errors_corrected_by_rereads_rewrites": 0, + "total_errors_corrected": 0, + "correction_algorithm_invocations": 0, + "gigabytes_processed": "9235.838", + "total_uncorrected_errors": 0 + }, + "write": { + "errors_corrected_by_eccfast": 0, + "errors_corrected_by_eccdelayed": 0, + "errors_corrected_by_rereads_rewrites": 0, + "total_errors_corrected": 0, + "correction_algorithm_invocations": 0, + "gigabytes_processed": "13467.004", + "total_uncorrected_errors": 0 + } + }, + "scsi_pending_defects": { + "count": 0 + }, + "scsi_self_test_0": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1239, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_1": { + "code": { + "value": 2, + "string": "Background long" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1218, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_2": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1191, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_3": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1168, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_4": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1144, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_5": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1120, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_6": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1095, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_7": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1072, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_8": { + "code": { + "value": 2, + "string": "Background long" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1053, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_9": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1024, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_10": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 1000, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_11": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 976, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_12": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 952, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_13": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 928, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_14": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 904, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_15": { + "code": { + "value": 2, + "string": "Background long" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 884, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_16": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 856, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_17": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 832, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_18": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 808, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_self_test_19": { + "code": { + "value": 1, + "string": "Background short" + }, + "result": { + "value": 0, + "string": "Completed" + }, + "power_on_time": { + "hours": 784, + "aka": "accumulated_power_on_hours" + } + }, + "scsi_extended_self_test_seconds": 93300 + } + \ No newline at end of file