mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-26 22:41:50 +02:00
[Agent] feat: parse ATA device statistics for temperature and future metrics (#1689)
* feat: add ATA Device Statistics parsing and fall back for SMART temp reading * simplify ata device statistics structs and fix smartctl args tests * simplify ata device statistics lookup to use page number only --------- Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
@@ -515,10 +515,12 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
|||||||
// smartctlArgs returns the arguments for the smartctl command
|
// smartctlArgs returns the arguments for the smartctl command
|
||||||
// based on the device type and whether to include standby mode
|
// based on the device type and whether to include standby mode
|
||||||
func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool) []string {
|
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 {
|
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
|
// types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345
|
||||||
if deviceType != "" && deviceType != "scsi" && deviceType != "ata" {
|
if deviceType != "" && deviceType != "scsi" && deviceType != "ata" {
|
||||||
args = append(args, "-d", deviceInfo.Type)
|
args = append(args, "-d", deviceInfo.Type)
|
||||||
@@ -526,6 +528,13 @@ func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-a", "--json=c")
|
args = append(args, "-a", "--json=c")
|
||||||
|
effectiveType := parserType
|
||||||
|
if effectiveType == "" {
|
||||||
|
effectiveType = deviceType
|
||||||
|
}
|
||||||
|
if effectiveType == "sat" || effectiveType == "ata" {
|
||||||
|
args = append(args, "-l", "devstat")
|
||||||
|
}
|
||||||
|
|
||||||
if includeStandby {
|
if includeStandby {
|
||||||
args = append(args, "-n", "standby")
|
args = append(args, "-n", "standby")
|
||||||
@@ -829,6 +838,11 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
|||||||
smartData.FirmwareVersion = data.FirmwareVersion
|
smartData.FirmwareVersion = data.FirmwareVersion
|
||||||
smartData.Capacity = data.UserCapacity.Bytes
|
smartData.Capacity = data.UserCapacity.Bytes
|
||||||
smartData.Temperature = data.Temperature.Current
|
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.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||||
smartData.DiskName = data.Device.Name
|
smartData.DiskName = data.Device.Name
|
||||||
smartData.DiskType = data.Device.Type
|
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) {
|
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
||||||
var data smart.SmartInfoForScsi
|
var data smart.SmartInfoForScsi
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
||||||
jsonPayload := []byte(`{
|
jsonPayload := []byte(`{
|
||||||
"smartctl": {"exit_status": 0},
|
"smartctl": {"exit_status": 0},
|
||||||
@@ -267,15 +300,21 @@ func TestSmartctlArgs(t *testing.T) {
|
|||||||
|
|
||||||
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
|
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
|
||||||
assert.Equal(t,
|
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),
|
sm.smartctlArgs(sataDevice, true),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Equal(t,
|
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),
|
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,
|
assert.Equal(t,
|
||||||
[]string{"-a", "--json=c", "-n", "standby"},
|
[]string{"-a", "--json=c", "-n", "standby"},
|
||||||
sm.smartctlArgs(nil, true),
|
sm.smartctlArgs(nil, true),
|
||||||
@@ -516,18 +555,18 @@ func TestUpdateSmartDevicesPreservesRAIDDrives(t *testing.T) {
|
|||||||
},
|
},
|
||||||
SmartDataMap: map[string]*smart.SmartData{
|
SmartDataMap: map[string]*smart.SmartData{
|
||||||
"serial-0": {
|
"serial-0": {
|
||||||
DiskName: "/dev/sda",
|
DiskName: "/dev/sda",
|
||||||
DiskType: "megaraid,0",
|
DiskType: "megaraid,0",
|
||||||
SerialNumber: "serial-0",
|
SerialNumber: "serial-0",
|
||||||
},
|
},
|
||||||
"serial-1": {
|
"serial-1": {
|
||||||
DiskName: "/dev/sda",
|
DiskName: "/dev/sda",
|
||||||
DiskType: "megaraid,1",
|
DiskType: "megaraid,1",
|
||||||
SerialNumber: "serial-1",
|
SerialNumber: "serial-1",
|
||||||
},
|
},
|
||||||
"serial-stale": {
|
"serial-stale": {
|
||||||
DiskName: "/dev/sda",
|
DiskName: "/dev/sda",
|
||||||
DiskType: "megaraid,2",
|
DiskType: "megaraid,2",
|
||||||
SerialNumber: "serial-stale",
|
SerialNumber: "serial-stale",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -130,10 +130,23 @@ type SummaryInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AtaSmartAttributes struct {
|
type AtaSmartAttributes struct {
|
||||||
// Revision int `json:"revision"`
|
|
||||||
Table []AtaSmartAttribute `json:"table"`
|
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 {
|
type AtaSmartAttribute struct {
|
||||||
ID uint16 `json:"id"`
|
ID uint16 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -343,7 +356,8 @@ type SmartInfoForSata struct {
|
|||||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||||
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
||||||
// AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"`
|
// 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"`
|
// PowerOnTime PowerOnTimeInfo `json:"power_on_time"`
|
||||||
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||||
Temperature TemperatureInfo `json:"temperature"`
|
Temperature TemperatureInfo `json:"temperature"`
|
||||||
|
|||||||
Reference in New Issue
Block a user