From fa9de55433c81ae8bf79bf0414c6a802cb9814de Mon Sep 17 00:00:00 2001 From: Aditya Raj Singh Date: Sun, 30 Aug 2026 20:53:52 +0530 Subject: [PATCH] fix(agent): warn on critical ATA SMART attributes (#2275) --- agent/smart.go | 3 +++ agent/smart_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/agent/smart.go b/agent/smart.go index 28a5ad81..bcbdd91a 100644 --- a/agent/smart.go +++ b/agent/smart.go @@ -931,6 +931,9 @@ func (sm *SmartManager) parseSmartForSata(output []byte, deviceType string) (boo if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok { rawValue = parsed } + if smartData.SmartStatus == "PASSED" && rawValue > 0 && (attr.ID == 5 || attr.ID == 197 || attr.ID == 198) { + smartData.SmartStatus = "WARNING" + } smartAttr := &smart.SmartAttribute{ ID: attr.ID, Name: attr.Name, diff --git a/agent/smart_test.go b/agent/smart_test.go index e2296db9..4850368e 100644 --- a/agent/smart_test.go +++ b/agent/smart_test.go @@ -4,8 +4,10 @@ package agent import ( "errors" + "fmt" "os" "path/filepath" + "strconv" "testing" "github.com/henrygd/beszel/internal/entities/smart" @@ -88,6 +90,52 @@ func TestParseSmartForSata(t *testing.T) { } } +func TestParseSmartForSataWarnsForCriticalAttributes(t *testing.T) { + for _, attrID := range []int{5, 197, 198} { + t.Run("attribute "+strconv.Itoa(attrID), func(t *testing.T) { + jsonPayload := []byte(fmt.Sprintf(`{ + "smartctl": {"exit_status": 0}, + "device": {"name": "/dev/sda", "type": "sat"}, + "model_name": "Example", + "serial_number": "WARNING%d", + "smart_status": {"passed": true}, + "temperature": {"current": 30}, + "ata_smart_attributes": {"table": [{"id": %d, "raw": {"value": 1, "string": "1"}}]} + }`, attrID, attrID)) + + sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)} + hasData, _ := sm.parseSmartForSata(jsonPayload, "") + require.True(t, hasData) + assert.Equal(t, "WARNING", sm.SmartDataMap[fmt.Sprintf("WARNING%d", attrID)].SmartStatus) + }) + } +} + +func TestParseSmartForSataPreservesFailedAndUnknownStatus(t *testing.T) { + for _, test := range []struct { + name string + temperature int + want string + }{ + {name: "failed", temperature: 30, want: "FAILED"}, + {name: "unknown", want: "UNKNOWN"}, + } { + t.Run(test.name, func(t *testing.T) { + jsonPayload := []byte(fmt.Sprintf(`{ + "device": {"name": "/dev/sda", "type": "sat"}, + "serial_number": "PRESERVE%s", + "temperature": {"current": %d}, + "ata_smart_attributes": {"table": [{"id": 197, "raw": {"value": 1, "string": "1"}}]} + }`, test.name, test.temperature)) + + sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)} + hasData, _ := sm.parseSmartForSata(jsonPayload, "") + require.True(t, hasData) + assert.Equal(t, test.want, sm.SmartDataMap["PRESERVE"+test.name].SmartStatus) + }) + } +} + func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) { jsonPayload := []byte(`{ "smartctl": {"exit_status": 0},