diff --git a/agent/smart.go b/agent/smart.go index 94cd23ab..6b38d438 100644 --- a/agent/smart.go +++ b/agent/smart.go @@ -515,10 +515,12 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error { // smartctlArgs returns the arguments for the smartctl command // based on the device type and whether to include standby mode func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool) []string { - args := make([]string, 0, 7) + args := make([]string, 0, 9) + var deviceType, parserType string if deviceInfo != nil { - deviceType := strings.ToLower(deviceInfo.Type) + deviceType = strings.ToLower(deviceInfo.Type) + parserType = strings.ToLower(deviceInfo.parserType) // types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345 if deviceType != "" && deviceType != "scsi" && deviceType != "ata" { args = append(args, "-d", deviceInfo.Type) @@ -526,6 +528,13 @@ func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool } args = append(args, "-a", "--json=c") + effectiveType := parserType + if effectiveType == "" { + effectiveType = deviceType + } + if effectiveType == "sat" || effectiveType == "ata" { + args = append(args, "-l", "devstat") + } if includeStandby { args = append(args, "-n", "standby") @@ -829,6 +838,11 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) { smartData.FirmwareVersion = data.FirmwareVersion smartData.Capacity = data.UserCapacity.Bytes smartData.Temperature = data.Temperature.Current + if smartData.Temperature == 0 { + if temp, ok := temperatureFromAtaDeviceStatistics(data.AtaDeviceStatistics); ok { + smartData.Temperature = temp + } + } smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed) smartData.DiskName = data.Device.Name smartData.DiskType = data.Device.Type @@ -867,6 +881,36 @@ func getSmartStatus(temperature uint8, passed bool) string { } } +func temperatureFromAtaDeviceStatistics(stats smart.AtaDeviceStatistics) (uint8, bool) { + entry := findAtaDeviceStatisticsEntry(stats, 5, "Current Temperature") + if entry == nil || entry.Value == nil { + return 0, false + } + if *entry.Value > 255 { + return 0, false + } + return uint8(*entry.Value), true +} + +// findAtaDeviceStatisticsEntry centralizes ATA devstat lookups so additional +// metrics can be pulled from the same structure in the future. +func findAtaDeviceStatisticsEntry(stats smart.AtaDeviceStatistics, pageNumber uint8, entryName string) *smart.AtaDeviceStatisticsEntry { + for pageIdx := range stats.Pages { + page := &stats.Pages[pageIdx] + if page.Number != pageNumber { + continue + } + for entryIdx := range page.Table { + entry := &page.Table[entryIdx] + if !strings.EqualFold(entry.Name, entryName) { + continue + } + return entry + } + } + return nil +} + func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) { var data smart.SmartInfoForScsi diff --git a/agent/smart_test.go b/agent/smart_test.go index e75ec688..594c5fcc 100644 --- a/agent/smart_test.go +++ b/agent/smart_test.go @@ -89,6 +89,39 @@ func TestParseSmartForSata(t *testing.T) { } } +func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) { + jsonPayload := []byte(`{ + "smartctl": {"exit_status": 0}, + "device": {"name": "/dev/sdb", "type": "sat"}, + "model_name": "SanDisk SSD U110 16GB", + "serial_number": "DEVSTAT123", + "firmware_version": "U21B001", + "user_capacity": {"bytes": 16013942784}, + "smart_status": {"passed": true}, + "ata_smart_attributes": {"table": []}, + "ata_device_statistics": { + "pages": [ + { + "number": 5, + "name": "Temperature Statistics", + "table": [ + {"name": "Current Temperature", "value": 22, "flags": {"valid": true}} + ] + } + ] + } + }`) + + sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)} + hasData, exitStatus := sm.parseSmartForSata(jsonPayload) + require.True(t, hasData) + assert.Equal(t, 0, exitStatus) + + deviceData, ok := sm.SmartDataMap["DEVSTAT123"] + require.True(t, ok, "expected smart data entry for serial DEVSTAT123") + assert.Equal(t, uint8(22), deviceData.Temperature) +} + func TestParseSmartForSataParentheticalRawValue(t *testing.T) { jsonPayload := []byte(`{ "smartctl": {"exit_status": 0}, @@ -267,15 +300,21 @@ func TestSmartctlArgs(t *testing.T) { sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"} assert.Equal(t, - []string{"-d", "sat", "-a", "--json=c", "-n", "standby", "/dev/sda"}, + []string{"-d", "sat", "-a", "--json=c", "-l", "devstat", "-n", "standby", "/dev/sda"}, sm.smartctlArgs(sataDevice, true), ) assert.Equal(t, - []string{"-d", "sat", "-a", "--json=c", "/dev/sda"}, + []string{"-d", "sat", "-a", "--json=c", "-l", "devstat", "/dev/sda"}, sm.smartctlArgs(sataDevice, false), ) + nvmeDevice := &DeviceInfo{Name: "/dev/nvme0", Type: "nvme"} + assert.Equal(t, + []string{"-d", "nvme", "-a", "--json=c", "-n", "standby", "/dev/nvme0"}, + sm.smartctlArgs(nvmeDevice, true), + ) + assert.Equal(t, []string{"-a", "--json=c", "-n", "standby"}, sm.smartctlArgs(nil, true), @@ -516,18 +555,18 @@ func TestUpdateSmartDevicesPreservesRAIDDrives(t *testing.T) { }, SmartDataMap: map[string]*smart.SmartData{ "serial-0": { - DiskName: "/dev/sda", - DiskType: "megaraid,0", + DiskName: "/dev/sda", + DiskType: "megaraid,0", SerialNumber: "serial-0", }, "serial-1": { - DiskName: "/dev/sda", - DiskType: "megaraid,1", + DiskName: "/dev/sda", + DiskType: "megaraid,1", SerialNumber: "serial-1", }, "serial-stale": { - DiskName: "/dev/sda", - DiskType: "megaraid,2", + DiskName: "/dev/sda", + DiskType: "megaraid,2", SerialNumber: "serial-stale", }, }, diff --git a/internal/entities/smart/smart.go b/internal/entities/smart/smart.go index 48c8f1f4..2a7b11f3 100644 --- a/internal/entities/smart/smart.go +++ b/internal/entities/smart/smart.go @@ -130,10 +130,23 @@ type SummaryInfo struct { } type AtaSmartAttributes struct { - // Revision int `json:"revision"` Table []AtaSmartAttribute `json:"table"` } +type AtaDeviceStatistics struct { + Pages []AtaDeviceStatisticsPage `json:"pages"` +} + +type AtaDeviceStatisticsPage struct { + Number uint8 `json:"number"` + Table []AtaDeviceStatisticsEntry `json:"table"` +} + +type AtaDeviceStatisticsEntry struct { + Name string `json:"name"` + Value *uint64 `json:"value,omitempty"` +} + type AtaSmartAttribute struct { ID uint16 `json:"id"` Name string `json:"name"` @@ -343,7 +356,8 @@ type SmartInfoForSata struct { SmartStatus SmartStatusInfo `json:"smart_status"` // AtaSmartData AtaSmartData `json:"ata_smart_data"` // AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"` - AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"` + AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"` + AtaDeviceStatistics AtaDeviceStatistics `json:"ata_device_statistics"` // PowerOnTime PowerOnTimeInfo `json:"power_on_time"` // PowerCycleCount uint16 `json:"power_cycle_count"` Temperature TemperatureInfo `json:"temperature"`