diff --git a/agent/smart.go b/agent/smart.go index fb7e97776..57dae3a8a 100644 --- a/agent/smart.go +++ b/agent/smart.go @@ -108,14 +108,6 @@ func (sm *SmartManager) devicesSnapshot() []*DeviceInfo { return devices } -// hasSmartData reports whether any SMART data has been collected. -// func (sm *SmartManager) hasSmartData() bool { -// sm.Lock() -// defer sm.Unlock() - -// return len(sm.SmartDataMap) > 0 -// } - // resolveRefreshError determines the proper error to return after a refresh. func (sm *SmartManager) resolveRefreshError(scanErr, collectErr error) error { sm.Lock() @@ -535,8 +527,6 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error { return errNoValidSmartData } - // slog.Info("collecting SMART data", "device", deviceInfo.Name, "type", deviceInfo.Type, "has_existing_data", sm.hasDataForDevice(deviceInfo)) - // Check if we have existing data for this exact device identity. Multiple // bridge slots can share a path, so a name-only match is not sufficient. hasExistingData := sm.hasDataForDevice(deviceInfo) @@ -877,124 +867,110 @@ func (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) { } // isVirtualDevice checks if a device is a virtual disk that should be filtered out -func (sm *SmartManager) isVirtualDevice(data *smart.SmartInfoForSata) bool { - vendorUpper := strings.ToUpper(data.ScsiVendor) - productUpper := strings.ToUpper(data.ScsiProduct) - modelUpper := strings.ToUpper(data.ModelName) - - return sm.isVirtualDeviceFromStrings(vendorUpper, productUpper, modelUpper) -} - -// isVirtualDeviceNvme checks if an NVMe device is a virtual disk that should be filtered out -func (sm *SmartManager) isVirtualDeviceNvme(data *smart.SmartInfoForNvme) bool { - modelUpper := strings.ToUpper(data.ModelName) - - return sm.isVirtualDeviceFromStrings(modelUpper) -} - -// isVirtualDeviceScsi checks if a SCSI device is a virtual disk that should be filtered out -func (sm *SmartManager) isVirtualDeviceScsi(data *smart.SmartInfoForScsi) bool { - vendorUpper := strings.ToUpper(data.ScsiVendor) - productUpper := strings.ToUpper(data.ScsiProduct) - modelUpper := strings.ToUpper(data.ScsiModelName) - - return sm.isVirtualDeviceFromStrings(vendorUpper, productUpper, modelUpper) -} - -// isVirtualDeviceFromStrings checks if any of the provided strings indicate a virtual device -func (sm *SmartManager) isVirtualDeviceFromStrings(fields ...string) bool { - for _, field := range fields { - fieldUpper := strings.ToUpper(field) +// (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.) +func isVirtualDevice(data *smart.SmartctlOutput) bool { + for _, field := range []string{data.ScsiVendor, data.ScsiProduct, data.ModelName, data.ScsiModelName} { + field = strings.ToUpper(field) switch { - case strings.Contains(fieldUpper, "IET"), // iSCSI Enterprise Target - strings.Contains(fieldUpper, "VIRTUAL"), - strings.Contains(fieldUpper, "QEMU"), - strings.Contains(fieldUpper, "VBOX"), - strings.Contains(fieldUpper, "VMWARE"), - strings.Contains(fieldUpper, "MSFT"): // Microsoft Hyper-V + case strings.Contains(field, "IET"), // iSCSI Enterprise Target + strings.Contains(field, "VIRTUAL"), + strings.Contains(field, "QEMU"), + strings.Contains(field, "VBOX"), + strings.Contains(field, "VMWARE"), + strings.Contains(field, "MSFT"): // Microsoft Hyper-V return true } } return false } -// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap. -// deviceType is the exact type used to identify and query the device; when set, -// it takes precedence over the generic type reported by smartctl. -// Returns hasValidData and exitStatus -func (sm *SmartManager) parseSmartForSata(output []byte, deviceType string) (bool, int) { - var data smart.SmartInfoForSata - +// decodeSmartctlOutput unmarshals smartctl JSON output and rejects devices +// without a serial number and virtual devices. It also returns the smartctl exit status. +func decodeSmartctlOutput(output []byte) (*smart.SmartctlOutput, int, bool) { + var data smart.SmartctlOutput if err := json.Unmarshal(output, &data); err != nil { - return false, 0 + slog.Debug("failed to parse smartctl output", "err", err) + return nil, 0, false } + exitStatus := data.Smartctl.ExitStatus if data.SerialNumber == "" { slog.Debug("no serial number", "device", data.Device.Name) - return false, data.Smartctl.ExitStatus + return nil, exitStatus, false } - - // Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.) - if sm.isVirtualDevice(&data) { + if isVirtualDevice(&data) { slog.Debug("skipping smart", "device", data.Device.Name, "model", data.ModelName) - return false, data.Smartctl.ExitStatus + return nil, exitStatus, false } + return &data, exitStatus, true +} - sm.Lock() - defer sm.Unlock() - - keyName := data.SerialNumber - - // if device does not exist in SmartDataMap, initialize it - if _, ok := sm.SmartDataMap[keyName]; !ok { - sm.SmartDataMap[keyName] = &smart.SmartData{} +// newSmartData fills the fields shared by all smartctl protocols. deviceType is +// the exact type used to identify and query the device; when set, it takes +// precedence over the generic type reported by smartctl. +func newSmartData(data *smart.SmartctlOutput, deviceType, model, firmware string, temperature int64) *smart.SmartData { + temp := uint8(min(max(temperature, 0), 255)) + smartData := &smart.SmartData{ + ModelName: model, + SerialNumber: data.SerialNumber, + FirmwareVersion: firmware, + Capacity: data.UserCapacity.Bytes, + Temperature: temp, + SmartStatus: getSmartStatus(temp, data.SmartStatus.Passed), + DiskName: data.Device.Name, + DiskType: data.Device.Type, } - - // update SmartData - smartData := sm.SmartDataMap[keyName] - // smartData.ModelFamily = data.ModelFamily - smartData.ModelName = data.ModelName - smartData.SerialNumber = data.SerialNumber - smartData.FirmwareVersion = data.FirmwareVersion - smartData.Capacity = data.UserCapacity.Bytes - smartData.Temperature = data.Temperature.Current - smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed) - smartData.DiskName = data.Device.Name - smartData.DiskType = data.Device.Type if deviceType != "" { smartData.DiskType = deviceType } + return smartData +} + +// storeSmartData saves parsed smartctl data, keyed by serial number. +func (sm *SmartManager) storeSmartData(smartData *smart.SmartData) { + sm.Lock() + defer sm.Unlock() + sm.SmartDataMap[smartData.SerialNumber] = smartData +} + +// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap. +// Returns hasValidData and exitStatus +func (sm *SmartManager) parseSmartForSata(output []byte, deviceType string) (bool, int) { + data, exitStatus, ok := decodeSmartctlOutput(output) + if !ok { + return false, exitStatus + } + + smartData := newSmartData(data, deviceType, data.ModelName, data.FirmwareVersion, data.Temperature.Current) // get values from ata_device_statistics if necessary var ataDeviceStats smart.AtaDeviceStatistics if smartData.Temperature == 0 { - if temp := findAtaDeviceStatisticsValue(&data, &ataDeviceStats, 5, "Current Temperature", 0, 255); temp != nil { + if temp := findAtaDeviceStatisticsValue(data, &ataDeviceStats, 5, "Current Temperature", 0, 255); temp != nil { smartData.Temperature = uint8(*temp) } } - // update SmartAttributes - smartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table)) - for _, attr := range data.AtaSmartAttributes.Table { - rawValue := uint64(attr.Raw.Value) - if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok { - rawValue = parsed - } - smartAttr := &smart.SmartAttribute{ + var table []smart.AtaSmartAttribute + if data.AtaSmartAttributes != nil { + table = data.AtaSmartAttributes.Table + } + smartData.Attributes = make([]*smart.SmartAttribute, 0, len(table)) + for _, attr := range table { + smartData.Attributes = append(smartData.Attributes, &smart.SmartAttribute{ ID: attr.ID, Name: attr.Name, Value: attr.Value, Worst: attr.Worst, Threshold: attr.Thresh, - RawValue: rawValue, + RawValue: uint64(attr.Raw.Value), // already parsed from raw.string when possible RawString: attr.Raw.String, WhenFailed: attr.WhenFailed, - } - smartData.Attributes = append(smartData.Attributes, smartAttr) + }) } - sm.SmartDataMap[keyName] = smartData - return true, data.Smartctl.ExitStatus + sm.storeSmartData(smartData) + return true, exitStatus } func getSmartStatus(temperature uint8, passed bool) string { @@ -1009,7 +985,7 @@ func getSmartStatus(temperature uint8, passed bool) string { // findAtaDeviceStatisticsEntry centralizes ATA devstat lookups so additional // metrics can be pulled from the same structure in the future. -func findAtaDeviceStatisticsValue(data *smart.SmartInfoForSata, ataDeviceStats *smart.AtaDeviceStatistics, entryNumber uint8, entryName string, minValue, maxValue int64) *int64 { +func findAtaDeviceStatisticsValue(data *smart.SmartctlOutput, ataDeviceStats *smart.AtaDeviceStatistics, entryNumber uint8, entryName string, minValue, maxValue int64) *int64 { if len(ataDeviceStats.Pages) == 0 { if len(data.AtaDeviceStatistics) == 0 { return nil @@ -1037,81 +1013,48 @@ func findAtaDeviceStatisticsValue(data *smart.SmartInfoForSata, ataDeviceStats * return nil } +// parseSmartForScsi parses the output of smartctl --all -j for SCSI devices and updates the SmartDataMap. +// Returns hasValidData and exitStatus func (sm *SmartManager) parseSmartForScsi(output []byte, deviceType string) (bool, int) { - var data smart.SmartInfoForScsi - - if err := json.Unmarshal(output, &data); err != nil { - return false, 0 + data, exitStatus, ok := decodeSmartctlOutput(output) + if !ok { + return false, exitStatus } - if data.SerialNumber == "" { - slog.Debug("no serial number", "device", data.Device.Name) - return false, data.Smartctl.ExitStatus + smartData := newSmartData(data, deviceType, data.ScsiModelName, data.ScsiRevision, data.Temperature.Current) + + cycles := data.ScsiStartStopCycleCounter + attributes := []*smart.SmartAttribute{ + {Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours}, + {Name: "PowerOnMinutes", RawValue: data.PowerOnTime.Minutes}, + {Name: "GrownDefectList", RawValue: data.ScsiGrownDefectList}, + {Name: "StartStopCycles", RawValue: cycles.AccumulatedStartStopCycles}, + {Name: "LoadUnloadCycles", RawValue: cycles.AccumulatedLoadUnloadCycles}, + {Name: "StartStopSpecified", RawValue: cycles.SpecifiedCycleCountOverDeviceLifetime}, + {Name: "LoadUnloadSpecified", RawValue: cycles.SpecifiedLoadUnloadCountOverDeviceLifetime}, } - // Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.) - if sm.isVirtualDeviceScsi(&data) { - slog.Debug("skipping smart", "device", data.Device.Name, "model", data.ScsiModelName) - return false, data.Smartctl.ExitStatus + var errorLog smart.ScsiErrorCounterLog + if data.ScsiErrorCounterLog != nil { + errorLog = *data.ScsiErrorCounterLog } - - sm.Lock() - defer sm.Unlock() - - keyName := data.SerialNumber - if _, ok := sm.SmartDataMap[keyName]; !ok { - sm.SmartDataMap[keyName] = &smart.SmartData{} + for _, counter := range []struct { + prefix string + stats smart.ScsiErrorCounter + }{{"Read", errorLog.Read}, {"Write", errorLog.Write}, {"Verify", errorLog.Verify}} { + attributes = append(attributes, + &smart.SmartAttribute{Name: counter.prefix + "TotalErrorsCorrected", RawValue: counter.stats.TotalErrorsCorrected}, + &smart.SmartAttribute{Name: counter.prefix + "TotalUncorrectedErrors", RawValue: counter.stats.TotalUncorrectedErrors}, + &smart.SmartAttribute{Name: counter.prefix + "CorrectionAlgorithmInvocations", RawValue: counter.stats.CorrectionAlgorithmInvocations}, + ) + if val := parseScsiGigabytesProcessed(counter.stats.GigabytesProcessed); val >= 0 { + attributes = append(attributes, &smart.SmartAttribute{Name: counter.prefix + "GigabytesProcessed", RawValue: uint64(val)}) + } } - - smartData := sm.SmartDataMap[keyName] - smartData.ModelName = data.ScsiModelName - smartData.SerialNumber = data.SerialNumber - smartData.FirmwareVersion = data.ScsiRevision - smartData.Capacity = data.UserCapacity.Bytes - smartData.Temperature = data.Temperature.Current - smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed) - smartData.DiskName = data.Device.Name - smartData.DiskType = data.Device.Type - if deviceType != "" { - smartData.DiskType = deviceType - } - - attributes := make([]*smart.SmartAttribute, 0, 10) - attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours}) - attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnMinutes", RawValue: data.PowerOnTime.Minutes}) - attributes = append(attributes, &smart.SmartAttribute{Name: "GrownDefectList", RawValue: data.ScsiGrownDefectList}) - attributes = append(attributes, &smart.SmartAttribute{Name: "StartStopCycles", RawValue: data.ScsiStartStopCycleCounter.AccumulatedStartStopCycles}) - attributes = append(attributes, &smart.SmartAttribute{Name: "LoadUnloadCycles", RawValue: data.ScsiStartStopCycleCounter.AccumulatedLoadUnloadCycles}) - attributes = append(attributes, &smart.SmartAttribute{Name: "StartStopSpecified", RawValue: data.ScsiStartStopCycleCounter.SpecifiedCycleCountOverDeviceLifetime}) - attributes = append(attributes, &smart.SmartAttribute{Name: "LoadUnloadSpecified", RawValue: data.ScsiStartStopCycleCounter.SpecifiedLoadUnloadCountOverDeviceLifetime}) - - readStats := data.ScsiErrorCounterLog.Read - writeStats := data.ScsiErrorCounterLog.Write - verifyStats := data.ScsiErrorCounterLog.Verify - - attributes = append(attributes, &smart.SmartAttribute{Name: "ReadTotalErrorsCorrected", RawValue: readStats.TotalErrorsCorrected}) - attributes = append(attributes, &smart.SmartAttribute{Name: "ReadTotalUncorrectedErrors", RawValue: readStats.TotalUncorrectedErrors}) - attributes = append(attributes, &smart.SmartAttribute{Name: "ReadCorrectionAlgorithmInvocations", RawValue: readStats.CorrectionAlgorithmInvocations}) - if val := parseScsiGigabytesProcessed(readStats.GigabytesProcessed); val >= 0 { - attributes = append(attributes, &smart.SmartAttribute{Name: "ReadGigabytesProcessed", RawValue: uint64(val)}) - } - attributes = append(attributes, &smart.SmartAttribute{Name: "WriteTotalErrorsCorrected", RawValue: writeStats.TotalErrorsCorrected}) - attributes = append(attributes, &smart.SmartAttribute{Name: "WriteTotalUncorrectedErrors", RawValue: writeStats.TotalUncorrectedErrors}) - attributes = append(attributes, &smart.SmartAttribute{Name: "WriteCorrectionAlgorithmInvocations", RawValue: writeStats.CorrectionAlgorithmInvocations}) - if val := parseScsiGigabytesProcessed(writeStats.GigabytesProcessed); val >= 0 { - attributes = append(attributes, &smart.SmartAttribute{Name: "WriteGigabytesProcessed", RawValue: uint64(val)}) - } - attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyTotalErrorsCorrected", RawValue: verifyStats.TotalErrorsCorrected}) - attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyTotalUncorrectedErrors", RawValue: verifyStats.TotalUncorrectedErrors}) - attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyCorrectionAlgorithmInvocations", RawValue: verifyStats.CorrectionAlgorithmInvocations}) - if val := parseScsiGigabytesProcessed(verifyStats.GigabytesProcessed); val >= 0 { - attributes = append(attributes, &smart.SmartAttribute{Name: "VerifyGigabytesProcessed", RawValue: uint64(val)}) - } - smartData.Attributes = attributes - sm.SmartDataMap[keyName] = smartData - return true, data.Smartctl.ExitStatus + sm.storeSmartData(smartData) + return true, exitStatus } func parseScsiGigabytesProcessed(value string) int64 { @@ -1177,22 +1120,9 @@ func (sm *SmartManager) lookupDarwinNvmeCapacity(serial string) uint64 { // it takes precedence over the generic type reported by smartctl. // Returns hasValidData and exitStatus func (sm *SmartManager) parseSmartForNvme(output []byte, deviceType string) (bool, int) { - data := &smart.SmartInfoForNvme{} - - if err := json.Unmarshal(output, &data); err != nil { - slog.Debug("failed to parse NVMe smartctl output", "err", err) - return false, 0 - } - - if data.SerialNumber == "" { - slog.Debug("no serial number", "device", data.Device.Name) - return false, data.Smartctl.ExitStatus - } - - // Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.) - if sm.isVirtualDeviceNvme(data) { - slog.Debug("skipping smart", "device", data.Device.Name, "model", data.ModelName) - return false, data.Smartctl.ExitStatus + data, exitStatus, ok := decodeSmartctlOutput(output) + if !ok { + return false, exitStatus } // smartctl may return device identity fields before failing to read the NVMe @@ -1202,64 +1132,41 @@ func (sm *SmartManager) parseSmartForNvme(output []byte, deviceType string) (boo log := data.NVMeSmartHealthInformationLog if log == nil { slog.Debug("no NVMe SMART health information", "device", data.Device.Name) - return false, data.Smartctl.ExitStatus + return false, exitStatus } - sm.Lock() - defer sm.Unlock() - - keyName := data.SerialNumber - - // if device does not exist in SmartDataMap, initialize it - if _, ok := sm.SmartDataMap[keyName]; !ok { - sm.SmartDataMap[keyName] = &smart.SmartData{} - } - - // update SmartData - smartData := sm.SmartDataMap[keyName] - smartData.ModelName = data.ModelName - smartData.SerialNumber = data.SerialNumber - smartData.FirmwareVersion = data.FirmwareVersion - smartData.Capacity = data.UserCapacity.Bytes + smartData := newSmartData(data, deviceType, data.ModelName, data.FirmwareVersion, log.Temperature) if smartData.Capacity == 0 { smartData.Capacity = data.NVMeTotalCapacity } if smartData.Capacity == 0 && (runtime.GOOS == "darwin" || sm.darwinNvmeProvider != nil) { smartData.Capacity = sm.lookupDarwinNvmeCapacity(data.SerialNumber) } - smartData.Temperature = log.Temperature - smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed) - smartData.DiskName = data.Device.Name - smartData.DiskType = data.Device.Type - if deviceType != "" { - smartData.DiskType = deviceType - } // nvme attributes does not follow the same format as ata attributes, // so we manually map each field to SmartAttributes smartData.Attributes = []*smart.SmartAttribute{ - {Name: "CriticalWarning", RawValue: uint64(log.CriticalWarning)}, - {Name: "Temperature", RawValue: uint64(log.Temperature)}, - {Name: "AvailableSpare", RawValue: uint64(log.AvailableSpare)}, - {Name: "AvailableSpareThreshold", RawValue: uint64(log.AvailableSpareThreshold)}, - {Name: "PercentageUsed", RawValue: uint64(log.PercentageUsed)}, + {Name: "CriticalWarning", RawValue: log.CriticalWarning}, + {Name: "Temperature", RawValue: uint64(smartData.Temperature)}, + {Name: "AvailableSpare", RawValue: log.AvailableSpare}, + {Name: "AvailableSpareThreshold", RawValue: log.AvailableSpareThreshold}, + {Name: "PercentageUsed", RawValue: log.PercentageUsed}, {Name: "DataUnitsRead", RawValue: log.DataUnitsRead}, {Name: "DataUnitsWritten", RawValue: log.DataUnitsWritten}, - {Name: "HostReads", RawValue: uint64(log.HostReads)}, - {Name: "HostWrites", RawValue: uint64(log.HostWrites)}, - {Name: "ControllerBusyTime", RawValue: uint64(log.ControllerBusyTime)}, - {Name: "PowerCycles", RawValue: uint64(log.PowerCycles)}, - {Name: "PowerOnHours", RawValue: uint64(log.PowerOnHours)}, - {Name: "UnsafeShutdowns", RawValue: uint64(log.UnsafeShutdowns)}, - {Name: "MediaErrors", RawValue: uint64(log.MediaErrors)}, - {Name: "NumErrLogEntries", RawValue: uint64(log.NumErrLogEntries)}, - {Name: "WarningTempTime", RawValue: uint64(log.WarningTempTime)}, - {Name: "CriticalCompTime", RawValue: uint64(log.CriticalCompTime)}, + {Name: "HostReads", RawValue: log.HostReads}, + {Name: "HostWrites", RawValue: log.HostWrites}, + {Name: "ControllerBusyTime", RawValue: log.ControllerBusyTime}, + {Name: "PowerCycles", RawValue: log.PowerCycles}, + {Name: "PowerOnHours", RawValue: log.PowerOnHours}, + {Name: "UnsafeShutdowns", RawValue: log.UnsafeShutdowns}, + {Name: "MediaErrors", RawValue: log.MediaErrors}, + {Name: "NumErrLogEntries", RawValue: log.NumErrLogEntries}, + {Name: "WarningTempTime", RawValue: log.WarningTempTime}, + {Name: "CriticalCompTime", RawValue: log.CriticalCompTime}, } - sm.SmartDataMap[keyName] = smartData - - return true, data.Smartctl.ExitStatus + sm.storeSmartData(smartData) + return true, exitStatus } // detectSmartctl checks if smartctl is installed, returns an error if not diff --git a/agent/smart_test.go b/agent/smart_test.go index 7bbc5673d..c7b7ee960 100644 --- a/agent/smart_test.go +++ b/agent/smart_test.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strings" "testing" "time" @@ -60,6 +59,11 @@ func TestParseSmartForScsi(t *testing.T) { assertAttrValue(t, deviceData.Attributes, "GrownDefectList", 0) assertAttrValue(t, deviceData.Attributes, "StartStopCycles", 2) assertAttrValue(t, deviceData.Attributes, "LoadUnloadCycles", 418) + assertAttrValue(t, deviceData.Attributes, "StartStopSpecified", 50000) + assertAttrValue(t, deviceData.Attributes, "LoadUnloadSpecified", 600000) + assertAttrValue(t, deviceData.Attributes, "ReadCorrectionAlgorithmInvocations", 346) + assertAttrValue(t, deviceData.Attributes, "WriteCorrectionAlgorithmInvocations", 4052) + assertAttrValue(t, deviceData.Attributes, "VerifyCorrectionAlgorithmInvocations", 223) assertAttrValue(t, deviceData.Attributes, "ReadGigabytesProcessed", 3641) assertAttrValue(t, deviceData.Attributes, "WriteGigabytesProcessed", 2124590) assertAttrValue(t, deviceData.Attributes, "VerifyGigabytesProcessed", 0) @@ -295,27 +299,79 @@ func TestParseSmartForNvme(t *testing.T) { } } -func TestParseSmartForNvmeLargeUnsafeShutdowns(t *testing.T) { - fixturePath := filepath.Join("test-data", "smart", "nvme0.json") - data, err := os.ReadFile(fixturePath) - require.NoError(t, err) +// Counters larger than their old narrow Go types used to fail json.Unmarshal and +// drop the whole device (#2484, #2582). +func TestParseSmartForNvmeLargeCounters(t *testing.T) { + jsonPayload := []byte(`{ + "smartctl": {"exit_status": 0}, + "device": {"name": "/dev/nvme0", "type": "nvme"}, + "model_name": "Test NVMe", + "serial_number": "NVME-LARGE", + "smart_status": {"passed": true}, + "nvme_smart_health_information_log": { + "temperature": 40, + "percentage_used": 300, + "power_cycles": 70000, + "power_on_hours": 5000000000, + "unsafe_shutdowns": 159754 + } + }`) - // Some drives report counts that overflow uint16, which previously made the - // whole record fail to unmarshal and get discarded. - re := regexp.MustCompile(`"unsafe_shutdowns":\s*\d+`) - require.True(t, re.Match(data), "fixture should contain unsafe_shutdowns") - data = re.ReplaceAll(data, []byte(`"unsafe_shutdowns": 159754`)) - - sm := &SmartManager{ - SmartDataMap: make(map[string]*smart.SmartData), - } - - hasData, _ := sm.parseSmartForNvme(data, "") + sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)} + hasData, _ := sm.parseSmartForNvme(jsonPayload, "") require.True(t, hasData) - deviceData, ok := sm.SmartDataMap["2024031600129"] - require.True(t, ok, "expected smart data entry for serial 2024031600129") + deviceData := sm.SmartDataMap["NVME-LARGE"] + require.NotNil(t, deviceData) assertAttrValue(t, deviceData.Attributes, "UnsafeShutdowns", 159754) + assertAttrValue(t, deviceData.Attributes, "PowerCycles", 70000) + assertAttrValue(t, deviceData.Attributes, "PowerOnHours", 5000000000) + assertAttrValue(t, deviceData.Attributes, "PercentageUsed", 300) +} + +// Out-of-range temperatures are clamped instead of failing the whole parse. +func TestParseSmartTemperatureOutOfRange(t *testing.T) { + sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)} + + hasData, _ := sm.parseSmartForSata([]byte(`{ + "device": {"name": "/dev/sda", "type": "sat"}, + "serial_number": "SATA-NEG", + "smart_status": {"passed": true}, + "temperature": {"current": -5}, + "ata_smart_attributes": {"table": []} + }`), "") + require.True(t, hasData) + assert.Equal(t, uint8(0), sm.SmartDataMap["SATA-NEG"].Temperature) + + hasData, _ = sm.parseSmartForNvme([]byte(`{ + "device": {"name": "/dev/nvme0", "type": "nvme"}, + "serial_number": "NVME-HOT", + "smart_status": {"passed": true}, + "nvme_smart_health_information_log": {"temperature": 300} + }`), "") + require.True(t, hasData) + assert.Equal(t, uint8(255), sm.SmartDataMap["NVME-HOT"].Temperature) + assertAttrValue(t, sm.SmartDataMap["NVME-HOT"].Attributes, "Temperature", 255) +} + +func TestParseSmartForScsiWithoutErrorCounterLog(t *testing.T) { + sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)} + + hasData, _ := sm.parseSmartForScsi([]byte(`{ + "device": {"name": "/dev/sdb", "type": "scsi"}, + "scsi_model_name": "SEAGATE ST4000NM0023", + "serial_number": "SCSI-NOLOG", + "smart_status": {"passed": true}, + "temperature": {"current": 30}, + "power_on_time": {"hours": 1234} + }`), "") + require.True(t, hasData) + + deviceData := sm.SmartDataMap["SCSI-NOLOG"] + require.NotNil(t, deviceData) + assertAttrValue(t, deviceData.Attributes, "PowerOnHours", 1234) + assertAttrValue(t, deviceData.Attributes, "ReadTotalUncorrectedErrors", 0) + assert.Nil(t, findAttr(deviceData.Attributes, "ReadGigabytesProcessed")) } func TestHasDataForDevice(t *testing.T) { @@ -943,91 +999,29 @@ func findAttr(attributes []*smart.SmartAttribute, name string) *smart.SmartAttri } func TestIsVirtualDevice(t *testing.T) { - sm := &SmartManager{} - tests := []struct { name string - vendor string - product string - model string + data smart.SmartctlOutput expected bool }{ - {"regular drive", "SEAGATE", "ST1000DM003", "ST1000DM003-1CH162", false}, - {"qemu virtual", "QEMU", "QEMU HARDDISK", "QEMU HARDDISK", true}, - {"virtualbox virtual", "VBOX", "HARDDISK", "VBOX HARDDISK", true}, - {"vmware virtual", "VMWARE", "Virtual disk", "VMWARE Virtual disk", true}, - {"virtual in model", "ATA", "VIRTUAL", "VIRTUAL DISK", true}, - {"iet virtual", "IET", "VIRTUAL-DISK", "VIRTUAL-DISK", true}, - {"hyper-v virtual", "MSFT", "VIRTUAL HD", "VIRTUAL HD", true}, + {"regular sata", smart.SmartctlOutput{ScsiVendor: "SEAGATE", ScsiProduct: "ST1000DM003", ModelName: "ST1000DM003-1CH162"}, false}, + {"qemu sata", smart.SmartctlOutput{ScsiVendor: "QEMU", ScsiProduct: "QEMU HARDDISK", ModelName: "QEMU HARDDISK"}, true}, + {"virtualbox sata", smart.SmartctlOutput{ScsiVendor: "VBOX", ScsiProduct: "HARDDISK", ModelName: "VBOX HARDDISK"}, true}, + {"vmware sata", smart.SmartctlOutput{ScsiVendor: "VMWARE", ScsiProduct: "Virtual disk", ModelName: "VMWARE Virtual disk"}, true}, + {"virtual in model", smart.SmartctlOutput{ScsiVendor: "ATA", ScsiProduct: "VIRTUAL", ModelName: "VIRTUAL DISK"}, true}, + {"iet virtual", smart.SmartctlOutput{ScsiVendor: "IET", ScsiProduct: "VIRTUAL-DISK", ModelName: "VIRTUAL-DISK"}, true}, + {"hyper-v virtual", smart.SmartctlOutput{ScsiVendor: "MSFT", ScsiProduct: "VIRTUAL HD", ModelName: "VIRTUAL HD"}, true}, + {"regular nvme", smart.SmartctlOutput{ModelName: "Samsung SSD 970 EVO Plus 1TB"}, false}, + {"qemu nvme", smart.SmartctlOutput{ModelName: "QEMU NVMe Ctrl"}, true}, + {"virtual nvme", smart.SmartctlOutput{ModelName: "Virtual NVMe Device"}, true}, + {"regular scsi", smart.SmartctlOutput{ScsiVendor: "SEAGATE", ScsiProduct: "ST1000DM003", ScsiModelName: "ST1000DM003-1CH162"}, false}, + {"vmware scsi", smart.SmartctlOutput{ScsiVendor: "VMWARE", ScsiProduct: "Virtual disk", ScsiModelName: "VMWARE Virtual disk"}, true}, + {"virtual scsi model only", smart.SmartctlOutput{ScsiModelName: "QEMU HARDDISK"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - data := &smart.SmartInfoForSata{ - ScsiVendor: tt.vendor, - ScsiProduct: tt.product, - ModelName: tt.model, - } - result := sm.isVirtualDevice(data) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestIsVirtualDeviceNvme(t *testing.T) { - sm := &SmartManager{} - - tests := []struct { - name string - model string - expected bool - }{ - {"regular nvme", "Samsung SSD 970 EVO Plus 1TB", false}, - {"qemu virtual", "QEMU NVMe Ctrl", true}, - {"virtualbox virtual", "VBOX NVMe", true}, - {"vmware virtual", "VMWARE NVMe", true}, - {"virtual in model", "Virtual NVMe Device", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := &smart.SmartInfoForNvme{ - ModelName: tt.model, - } - result := sm.isVirtualDeviceNvme(data) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestIsVirtualDeviceScsi(t *testing.T) { - sm := &SmartManager{} - - tests := []struct { - name string - vendor string - product string - model string - expected bool - }{ - {"regular scsi", "SEAGATE", "ST1000DM003", "ST1000DM003-1CH162", false}, - {"qemu virtual", "QEMU", "QEMU HARDDISK", "QEMU HARDDISK", true}, - {"virtualbox virtual", "VBOX", "HARDDISK", "VBOX HARDDISK", true}, - {"vmware virtual", "VMWARE", "Virtual disk", "VMWARE Virtual disk", true}, - {"virtual in model", "ATA", "VIRTUAL", "VIRTUAL DISK", true}, - {"iet virtual", "IET", "VIRTUAL-DISK", "VIRTUAL-DISK", true}, - {"hyper-v virtual", "MSFT", "VIRTUAL HD", "VIRTUAL HD", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data := &smart.SmartInfoForScsi{ - ScsiVendor: tt.vendor, - ScsiProduct: tt.product, - ScsiModelName: tt.model, - } - result := sm.isVirtualDeviceScsi(data) - assert.Equal(t, tt.expected, result) + assert.Equal(t, tt.expected, isVirtualDevice(&tt.data)) }) } } @@ -1039,7 +1033,7 @@ func TestFindAtaDeviceStatisticsValue(t *testing.T) { tests := []struct { name string - data smart.SmartInfoForSata + data smart.SmartctlOutput ataDeviceStats smart.AtaDeviceStatistics entryNumber uint8 entryName string @@ -1067,7 +1061,7 @@ func TestFindAtaDeviceStatisticsValue(t *testing.T) { }, { name: "value unmarshaled from data", - data: smart.SmartInfoForSata{ + data: smart.SmartctlOutput{ AtaDeviceStatistics: []byte(`{"pages":[{"number":5,"table":[{"name":"Current Temperature","value":100}]}]}`), }, entryNumber: 5, @@ -1114,7 +1108,7 @@ func TestFindAtaDeviceStatisticsValue(t *testing.T) { }, { name: "no statistics available", - data: smart.SmartInfoForSata{}, + data: smart.SmartctlOutput{}, entryNumber: 5, entryName: "Current Temperature", minValue: 0, @@ -1579,7 +1573,7 @@ func TestRefreshNvmeNamespaceFallbackSurvivesRescan(t *testing.T) { // rather than only preserving the previous cached data. fixtureData, err := os.ReadFile(fixture) require.NoError(t, err) - var updatedData smart.SmartInfoForNvme + var updatedData smart.SmartctlOutput require.NoError(t, json.Unmarshal(fixtureData, &updatedData)) require.NotNil(t, updatedData.NVMeSmartHealthInformationLog) updatedData.NVMeSmartHealthInformationLog.Temperature++ @@ -1598,7 +1592,7 @@ func TestRefreshNvmeNamespaceFallbackSurvivesRescan(t *testing.T) { assert.Equal(t, "/dev/nvme0n1", sm.SmartDevices[0].Name) require.Contains(t, sm.SmartDataMap, "2024031600129") assert.Equal(t, "/dev/nvme0n1", sm.SmartDataMap["2024031600129"].DiskName) - assert.Equal(t, updatedData.NVMeSmartHealthInformationLog.Temperature, sm.SmartDataMap["2024031600129"].Temperature) + assert.Equal(t, uint8(updatedData.NVMeSmartHealthInformationLog.Temperature), sm.SmartDataMap["2024031600129"].Temperature) assert.True(t, sm.SmartDevices[0].typeVerified) assert.Equal(t, "nvme", sm.SmartDevices[0].parserType) diff --git a/internal/entities/smart/smart.go b/internal/entities/smart/smart.go index 7fa94b8c9..0f961e42f 100644 --- a/internal/entities/smart/smart.go +++ b/internal/entities/smart/smart.go @@ -6,16 +6,44 @@ import ( "strings" ) -// Common types -type VersionInfo [2]int +// SmartctlOutput holds the fields we read from `smartctl -a --json` for any +// protocol. Sections that do not apply to a device are left nil or empty. +// +// Counters use wide integer types on purpose: a value that overflows its Go type +// makes json.Unmarshal fail and drops the whole device (see #2484, #2582). +type SmartctlOutput struct { + Smartctl struct { + ExitStatus int `json:"exit_status"` + } `json:"smartctl"` + Device DeviceInfo `json:"device"` + ModelName string `json:"model_name"` + SerialNumber string `json:"serial_number"` + FirmwareVersion string `json:"firmware_version"` + UserCapacity UserCapacity `json:"user_capacity"` + SmartStatus struct { + Passed bool `json:"passed"` + } `json:"smart_status"` + Temperature struct { + Current int64 `json:"current"` + } `json:"temperature"` -type SmartctlInfo struct { - Version VersionInfo `json:"version"` - SvnRevision string `json:"svn_revision"` - PlatformInfo string `json:"platform_info"` - BuildInfo string `json:"build_info"` - Argv []string `json:"argv"` - ExitStatus int `json:"exit_status"` + // ATA + AtaSmartAttributes *AtaSmartAttributes `json:"ata_smart_attributes"` + AtaDeviceStatistics json.RawMessage `json:"ata_device_statistics"` // decoded lazily, only when needed + + // NVMe + NVMeTotalCapacity uint64 `json:"nvme_total_capacity"` + NVMeSmartHealthInformationLog *NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"` + + // SCSI (scsi_vendor/scsi_product are also reported for SATA disks behind SAT) + ScsiVendor string `json:"scsi_vendor"` + ScsiProduct string `json:"scsi_product"` + ScsiModelName string `json:"scsi_model_name"` + ScsiRevision string `json:"scsi_revision"` + PowerOnTime PowerOnTimeScsi `json:"power_on_time"` + ScsiStartStopCycleCounter ScsiStartStopCycleCounter `json:"scsi_start_stop_cycle_counter"` + ScsiGrownDefectList uint64 `json:"scsi_grown_defect_list"` + ScsiErrorCounterLog *ScsiErrorCounterLog `json:"scsi_error_counter_log"` } type DeviceInfo struct { @@ -30,109 +58,20 @@ type UserCapacity struct { Bytes uint64 `json:"bytes"` } -// type LocalTime struct { -// TimeT int64 `json:"time_t"` -// Asctime string `json:"asctime"` -// } - -// type WwnInfo struct { -// Naa int `json:"naa"` -// Oui int `json:"oui"` -// ID int `json:"id"` -// } - -// type FormFactorInfo struct { -// AtaValue int `json:"ata_value"` -// Name string `json:"name"` -// } - -// type TrimInfo struct { -// Supported bool `json:"supported"` -// } - -// type AtaVersionInfo struct { -// String string `json:"string"` -// MajorValue int `json:"major_value"` -// MinorValue int `json:"minor_value"` -// } - -// type VersionStringInfo struct { -// String string `json:"string"` -// Value int `json:"value"` -// } - -// type SpeedInfo struct { -// SataValue int `json:"sata_value"` -// String string `json:"string"` -// UnitsPerSecond int `json:"units_per_second"` -// BitsPerUnit int `json:"bits_per_unit"` -// } - -// type InterfaceSpeedInfo struct { -// Max SpeedInfo `json:"max"` -// Current SpeedInfo `json:"current"` -// } - -type SmartStatusInfo struct { - Passed bool `json:"passed"` -} - -type StatusInfo struct { - Value int `json:"value"` - String string `json:"string"` - Passed bool `json:"passed"` -} - -type PollingMinutes struct { - Short int `json:"short"` - Extended int `json:"extended"` -} - -type CapabilitiesInfo struct { - Values []int `json:"values"` - ExecOfflineImmediateSupported bool `json:"exec_offline_immediate_supported"` - OfflineIsAbortedUponNewCmd bool `json:"offline_is_aborted_upon_new_cmd"` - OfflineSurfaceScanSupported bool `json:"offline_surface_scan_supported"` - SelfTestsSupported bool `json:"self_tests_supported"` - ConveyanceSelfTestSupported bool `json:"conveyance_self_test_supported"` - SelectiveSelfTestSupported bool `json:"selective_self_test_supported"` - AttributeAutosaveEnabled bool `json:"attribute_autosave_enabled"` - ErrorLoggingSupported bool `json:"error_logging_supported"` - GpLoggingSupported bool `json:"gp_logging_supported"` -} - -// type AtaSmartData struct { -// OfflineDataCollection OfflineDataCollectionInfo `json:"offline_data_collection"` -// SelfTest SelfTestInfo `json:"self_test"` -// Capabilities CapabilitiesInfo `json:"capabilities"` -// } - -// type OfflineDataCollectionInfo struct { -// Status StatusInfo `json:"status"` -// CompletionSeconds int `json:"completion_seconds"` -// } - -// type SelfTestInfo struct { -// Status StatusInfo `json:"status"` -// PollingMinutes PollingMinutes `json:"polling_minutes"` -// } - -// type AtaSctCapabilities struct { -// Value int `json:"value"` -// ErrorRecoveryControlSupported bool `json:"error_recovery_control_supported"` -// FeatureControlSupported bool `json:"feature_control_supported"` -// DataTableSupported bool `json:"data_table_supported"` -// } - -type SummaryInfo struct { - Revision int `json:"revision"` - Count int `json:"count"` -} - type AtaSmartAttributes struct { Table []AtaSmartAttribute `json:"table"` } +type AtaSmartAttribute struct { + ID uint16 `json:"id"` + Name string `json:"name"` + Value uint16 `json:"value"` + Worst uint16 `json:"worst"` + Thresh uint16 `json:"thresh"` + WhenFailed string `json:"when_failed"` + Raw RawValue `json:"raw"` +} + type AtaDeviceStatistics struct { Pages []AtaDeviceStatisticsPage `json:"pages"` } @@ -147,27 +86,50 @@ type AtaDeviceStatisticsEntry struct { Value *int64 `json:"value,omitempty"` } -type AtaSmartAttribute struct { - ID uint16 `json:"id"` - Name string `json:"name"` - Value uint16 `json:"value"` - Worst uint16 `json:"worst"` - Thresh uint16 `json:"thresh"` - WhenFailed string `json:"when_failed"` - // Flags AttributeFlags `json:"flags"` - Raw RawValue `json:"raw"` +type NVMeSmartHealthInformationLog struct { + CriticalWarning uint64 `json:"critical_warning"` + Temperature int64 `json:"temperature"` + AvailableSpare uint64 `json:"available_spare"` + AvailableSpareThreshold uint64 `json:"available_spare_threshold"` + PercentageUsed uint64 `json:"percentage_used"` + DataUnitsRead uint64 `json:"data_units_read"` + DataUnitsWritten uint64 `json:"data_units_written"` + HostReads uint64 `json:"host_reads"` + HostWrites uint64 `json:"host_writes"` + ControllerBusyTime uint64 `json:"controller_busy_time"` + PowerCycles uint64 `json:"power_cycles"` + PowerOnHours uint64 `json:"power_on_hours"` + UnsafeShutdowns uint64 `json:"unsafe_shutdowns"` + MediaErrors uint64 `json:"media_errors"` + NumErrLogEntries uint64 `json:"num_err_log_entries"` + WarningTempTime uint64 `json:"warning_temp_time"` + CriticalCompTime uint64 `json:"critical_comp_time"` } -// type AttributeFlags struct { -// Value int `json:"value"` -// String string `json:"string"` -// Prefailure bool `json:"prefailure"` -// UpdatedOnline bool `json:"updated_online"` -// Performance bool `json:"performance"` -// ErrorRate bool `json:"error_rate"` -// EventCount bool `json:"event_count"` -// AutoKeep bool `json:"auto_keep"` -// } +type ScsiErrorCounter struct { + TotalErrorsCorrected uint64 `json:"total_errors_corrected"` + CorrectionAlgorithmInvocations uint64 `json:"correction_algorithm_invocations"` + GigabytesProcessed string `json:"gigabytes_processed"` + TotalUncorrectedErrors uint64 `json:"total_uncorrected_errors"` +} + +type ScsiErrorCounterLog struct { + Read ScsiErrorCounter `json:"read"` + Write ScsiErrorCounter `json:"write"` + Verify ScsiErrorCounter `json:"verify"` +} + +type ScsiStartStopCycleCounter struct { + SpecifiedCycleCountOverDeviceLifetime uint64 `json:"specified_cycle_count_over_device_lifetime"` + AccumulatedStartStopCycles uint64 `json:"accumulated_start_stop_cycles"` + SpecifiedLoadUnloadCountOverDeviceLifetime uint64 `json:"specified_load_unload_count_over_device_lifetime"` + AccumulatedLoadUnloadCycles uint64 `json:"accumulated_load_unload_cycles"` +} + +type PowerOnTimeScsi struct { + Hours uint64 `json:"hours"` + Minutes uint64 `json:"minutes"` +} type RawValue struct { Value SmartRawValue `json:"value"` @@ -280,244 +242,6 @@ func ParseSmartRawValueString(value string) (uint64, bool) { return 0, false } -// type PowerOnTimeInfo struct { -// Hours uint32 `json:"hours"` -// } - -type TemperatureInfo struct { - Current uint8 `json:"current"` -} - -type TemperatureInfoScsi struct { - Current uint8 `json:"current"` - DriveTrip uint8 `json:"drive_trip"` -} - -// type SelectiveSelfTestTable struct { -// LbaMin int `json:"lba_min"` -// LbaMax int `json:"lba_max"` -// Status StatusInfo `json:"status"` -// } - -// type SelectiveSelfTestFlags struct { -// Value int `json:"value"` -// RemainderScanEnabled bool `json:"remainder_scan_enabled"` -// } - -// type AtaSmartSelectiveSelfTestLog struct { -// Revision int `json:"revision"` -// Table []SelectiveSelfTestTable `json:"table"` -// Flags SelectiveSelfTestFlags `json:"flags"` -// PowerUpScanResumeMinutes int `json:"power_up_scan_resume_minutes"` -// } - -// BaseSmartInfo contains common fields shared between SATA and NVMe drives -// type BaseSmartInfo struct { -// Device DeviceInfo `json:"device"` -// ModelName string `json:"model_name"` -// SerialNumber string `json:"serial_number"` -// FirmwareVersion string `json:"firmware_version"` -// UserCapacity UserCapacity `json:"user_capacity"` -// LogicalBlockSize int `json:"logical_block_size"` -// LocalTime LocalTime `json:"local_time"` -// } - -type SmartctlInfoLegacy struct { - Version VersionInfo `json:"version"` - SvnRevision string `json:"svn_revision"` - PlatformInfo string `json:"platform_info"` - BuildInfo string `json:"build_info"` - Argv []string `json:"argv"` - ExitStatus int `json:"exit_status"` -} - -type SmartInfoForSata struct { - // JSONFormatVersion VersionInfo `json:"json_format_version"` - Smartctl SmartctlInfoLegacy `json:"smartctl"` - Device DeviceInfo `json:"device"` - // ModelFamily string `json:"model_family"` - ModelName string `json:"model_name"` - SerialNumber string `json:"serial_number"` - // Wwn WwnInfo `json:"wwn"` - FirmwareVersion string `json:"firmware_version"` - UserCapacity UserCapacity `json:"user_capacity"` - ScsiVendor string `json:"scsi_vendor"` - ScsiProduct string `json:"scsi_product"` - // LogicalBlockSize int `json:"logical_block_size"` - // PhysicalBlockSize int `json:"physical_block_size"` - // RotationRate int `json:"rotation_rate"` - // FormFactor FormFactorInfo `json:"form_factor"` - // Trim TrimInfo `json:"trim"` - // InSmartctlDatabase bool `json:"in_smartctl_database"` - // AtaVersion AtaVersionInfo `json:"ata_version"` - // SataVersion VersionStringInfo `json:"sata_version"` - // InterfaceSpeed InterfaceSpeedInfo `json:"interface_speed"` - // LocalTime LocalTime `json:"local_time"` - SmartStatus SmartStatusInfo `json:"smart_status"` - // AtaSmartData AtaSmartData `json:"ata_smart_data"` - // AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"` - AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"` - AtaDeviceStatistics json.RawMessage `json:"ata_device_statistics"` - // PowerOnTime PowerOnTimeInfo `json:"power_on_time"` - // PowerCycleCount uint16 `json:"power_cycle_count"` - Temperature TemperatureInfo `json:"temperature"` - // AtaSmartErrorLog AtaSmartErrorLog `json:"ata_smart_error_log"` - // AtaSmartSelfTestLog AtaSmartSelfTestLog `json:"ata_smart_self_test_log"` - // AtaSmartSelectiveSelfTestLog AtaSmartSelectiveSelfTestLog `json:"ata_smart_selective_self_test_log"` -} - -type ScsiErrorCounter struct { - ErrorsCorrectedByECCFast uint64 `json:"errors_corrected_by_eccfast"` - ErrorsCorrectedByECCDelayed uint64 `json:"errors_corrected_by_eccdelayed"` - ErrorsCorrectedByRereadsRewrites uint64 `json:"errors_corrected_by_rereads_rewrites"` - TotalErrorsCorrected uint64 `json:"total_errors_corrected"` - CorrectionAlgorithmInvocations uint64 `json:"correction_algorithm_invocations"` - GigabytesProcessed string `json:"gigabytes_processed"` - TotalUncorrectedErrors uint64 `json:"total_uncorrected_errors"` -} - -type ScsiErrorCounterLog struct { - Read ScsiErrorCounter `json:"read"` - Write ScsiErrorCounter `json:"write"` - Verify ScsiErrorCounter `json:"verify"` -} - -type ScsiStartStopCycleCounter struct { - YearOfManufacture string `json:"year_of_manufacture"` - WeekOfManufacture string `json:"week_of_manufacture"` - SpecifiedCycleCountOverDeviceLifetime uint64 `json:"specified_cycle_count_over_device_lifetime"` - AccumulatedStartStopCycles uint64 `json:"accumulated_start_stop_cycles"` - SpecifiedLoadUnloadCountOverDeviceLifetime uint64 `json:"specified_load_unload_count_over_device_lifetime"` - AccumulatedLoadUnloadCycles uint64 `json:"accumulated_load_unload_cycles"` -} - -type PowerOnTimeScsi struct { - Hours uint64 `json:"hours"` - Minutes uint64 `json:"minutes"` -} - -type SmartInfoForScsi struct { - Smartctl SmartctlInfoLegacy `json:"smartctl"` - Device DeviceInfo `json:"device"` - ScsiVendor string `json:"scsi_vendor"` - ScsiProduct string `json:"scsi_product"` - ScsiModelName string `json:"scsi_model_name"` - ScsiRevision string `json:"scsi_revision"` - ScsiVersion string `json:"scsi_version"` - SerialNumber string `json:"serial_number"` - UserCapacity UserCapacity `json:"user_capacity"` - Temperature TemperatureInfoScsi `json:"temperature"` - SmartStatus SmartStatusInfo `json:"smart_status"` - PowerOnTime PowerOnTimeScsi `json:"power_on_time"` - ScsiStartStopCycleCounter ScsiStartStopCycleCounter `json:"scsi_start_stop_cycle_counter"` - ScsiGrownDefectList uint64 `json:"scsi_grown_defect_list"` - ScsiErrorCounterLog ScsiErrorCounterLog `json:"scsi_error_counter_log"` -} - -// type AtaSmartErrorLog struct { -// Summary SummaryInfo `json:"summary"` -// } - -// type AtaSmartSelfTestLog struct { -// Standard SummaryInfo `json:"standard"` -// } - -type SmartctlInfoNvme struct { - Version VersionInfo `json:"version"` - SVNRevision string `json:"svn_revision"` - PlatformInfo string `json:"platform_info"` - BuildInfo string `json:"build_info"` - Argv []string `json:"argv"` - ExitStatus int `json:"exit_status"` -} - -// type NVMePCIVendor struct { -// ID int `json:"id"` -// SubsystemID int `json:"subsystem_id"` -// } - -// type SizeCapacityInfo struct { -// Blocks uint64 `json:"blocks"` -// Bytes uint64 `json:"bytes"` -// } - -// type EUI64Info struct { -// OUI int `json:"oui"` -// ExtID int `json:"ext_id"` -// } - -// type NVMeNamespace struct { -// ID uint32 `json:"id"` -// Size SizeCapacityInfo `json:"size"` -// Capacity SizeCapacityInfo `json:"capacity"` -// Utilization SizeCapacityInfo `json:"utilization"` -// FormattedLBASize uint32 `json:"formatted_lba_size"` -// EUI64 EUI64Info `json:"eui64"` -// } - -type SmartStatusInfoNvme struct { - Passed bool `json:"passed"` - NVMe SmartStatusNVMe `json:"nvme"` -} - -type SmartStatusNVMe struct { - Value int `json:"value"` -} - -type NVMeSmartHealthInformationLog struct { - CriticalWarning uint `json:"critical_warning"` - Temperature uint8 `json:"temperature"` - AvailableSpare uint `json:"available_spare"` - AvailableSpareThreshold uint `json:"available_spare_threshold"` - PercentageUsed uint8 `json:"percentage_used"` - DataUnitsRead uint64 `json:"data_units_read"` - DataUnitsWritten uint64 `json:"data_units_written"` - HostReads uint `json:"host_reads"` - HostWrites uint `json:"host_writes"` - ControllerBusyTime uint `json:"controller_busy_time"` - PowerCycles uint `json:"power_cycles"` - PowerOnHours uint32 `json:"power_on_hours"` - UnsafeShutdowns uint `json:"unsafe_shutdowns"` - MediaErrors uint `json:"media_errors"` - NumErrLogEntries uint `json:"num_err_log_entries"` - WarningTempTime uint `json:"warning_temp_time"` - CriticalCompTime uint `json:"critical_comp_time"` - TemperatureSensors []uint8 `json:"temperature_sensors"` -} - -type SmartInfoForNvme struct { - // JSONFormatVersion VersionInfo `json:"json_format_version"` - Smartctl SmartctlInfoNvme `json:"smartctl"` - Device DeviceInfo `json:"device"` - ModelName string `json:"model_name"` - SerialNumber string `json:"serial_number"` - FirmwareVersion string `json:"firmware_version"` - // NVMePCIVendor NVMePCIVendor `json:"nvme_pci_vendor"` - // NVMeIEEEOUIIdentifier uint32 `json:"nvme_ieee_oui_identifier"` - NVMeTotalCapacity uint64 `json:"nvme_total_capacity"` - // NVMeUnallocatedCapacity uint64 `json:"nvme_unallocated_capacity"` - // NVMeControllerID uint16 `json:"nvme_controller_id"` - // NVMeVersion VersionStringInfo `json:"nvme_version"` - // NVMeNumberOfNamespaces uint8 `json:"nvme_number_of_namespaces"` - // NVMeNamespaces []NVMeNamespace `json:"nvme_namespaces"` - UserCapacity UserCapacity `json:"user_capacity"` - // LogicalBlockSize int `json:"logical_block_size"` - // LocalTime LocalTime `json:"local_time"` - SmartStatus SmartStatusInfoNvme `json:"smart_status"` - NVMeSmartHealthInformationLog *NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"` - Temperature TemperatureInfoNvme `json:"temperature"` - PowerCycleCount uint `json:"power_cycle_count"` - PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"` -} - -type TemperatureInfoNvme struct { - Current int `json:"current"` -} - -type PowerOnTimeInfoNvme struct { - Hours int `json:"hours"` -} - type SmartData struct { // ModelFamily string `json:"mf,omitempty" cbor:"0,keyasint,omitempty"` ModelName string `json:"mn,omitempty" cbor:"1,keyasint,omitempty"`