mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-22 06:12:33 +00:00
refactor(agent): consolidate smartctl JSON types and SMART parsers (#2584)
This commit is contained in:
+123
-216
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user