refactor(agent): consolidate smartctl JSON types and SMART parsers (#2584)

This commit is contained in:
Sven van Ginkel
2026-10-12 10:34:37 -04:00
committed by GitHub
parent 3d33d5eabb
commit 665bcb3e53
3 changed files with 306 additions and 681 deletions
+123 -216
View File
@@ -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
+94 -100
View File
@@ -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)
+89 -365
View File
@@ -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"`