mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-01 05:47:48 +02:00
fix(agent): handle partial NVMe SMART data (#2340)
This commit is contained in:
@@ -311,11 +311,13 @@ func (sm *SmartManager) filterExcludedDevices(devices []*DeviceInfo) []*DeviceIn
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectSmartOutputType inspects sections that are unique to each smartctl
|
// detectSmartOutputType inspects protocol-specific sections and the reported
|
||||||
// JSON schema (NVMe, ATA/SATA, SCSI) to determine which parser should be used
|
// device type to choose a parser, including when the NVMe health log is missing.
|
||||||
// when the reported device type is ambiguous or missing.
|
|
||||||
func detectSmartOutputType(output []byte) string {
|
func detectSmartOutputType(output []byte) string {
|
||||||
var hints struct {
|
var hints struct {
|
||||||
|
Device struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
} `json:"device"`
|
||||||
AtaSmartAttributes json.RawMessage `json:"ata_smart_attributes"`
|
AtaSmartAttributes json.RawMessage `json:"ata_smart_attributes"`
|
||||||
NVMeSmartHealthInformationLog json.RawMessage `json:"nvme_smart_health_information_log"`
|
NVMeSmartHealthInformationLog json.RawMessage `json:"nvme_smart_health_information_log"`
|
||||||
ScsiErrorCounterLog json.RawMessage `json:"scsi_error_counter_log"`
|
ScsiErrorCounterLog json.RawMessage `json:"scsi_error_counter_log"`
|
||||||
@@ -326,7 +328,7 @@ func detectSmartOutputType(output []byte) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case hasJSONValue(hints.NVMeSmartHealthInformationLog):
|
case hasJSONValue(hints.NVMeSmartHealthInformationLog), normalizeParserType(hints.Device.Type) == "nvme":
|
||||||
return "nvme"
|
return "nvme"
|
||||||
case hasJSONValue(hints.AtaSmartAttributes):
|
case hasJSONValue(hints.AtaSmartAttributes):
|
||||||
return "sat"
|
return "sat"
|
||||||
@@ -397,11 +399,11 @@ func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only run the type detection when we do not yet know which parser works
|
// Inspect every response so a failed NVMe query cannot reach other parsers.
|
||||||
// or the previous attempt failed.
|
structureType := detectSmartOutputType(output)
|
||||||
|
// Update the stored parser only when it is not yet verified.
|
||||||
needsDetection := deviceType == "" || !deviceInfo.typeVerified
|
needsDetection := deviceType == "" || !deviceInfo.typeVerified
|
||||||
if needsDetection {
|
if needsDetection {
|
||||||
structureType := detectSmartOutputType(output)
|
|
||||||
if deviceType != structureType {
|
if deviceType != structureType {
|
||||||
deviceType = structureType
|
deviceType = structureType
|
||||||
deviceInfo.parserType = structureType
|
deviceInfo.parserType = structureType
|
||||||
@@ -442,6 +444,11 @@ func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte)
|
|||||||
|
|
||||||
// Try the selected parsers in order until we find one that succeeds.
|
// Try the selected parsers in order until we find one that succeeds.
|
||||||
for _, parser := range selectedParsers {
|
for _, parser := range selectedParsers {
|
||||||
|
// A failed NVMe response may still contain a serial number, which is
|
||||||
|
// enough for the SATA and SCSI parsers to accept incorrect zero values.
|
||||||
|
if structureType == "nvme" && parser.Type != "nvme" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
hasData, _ := parser.Parse(output)
|
hasData, _ := parser.Parse(output)
|
||||||
if hasData {
|
if hasData {
|
||||||
deviceInfo.parserType = parser.Type
|
deviceInfo.parserType = parser.Type
|
||||||
@@ -1145,6 +1152,16 @@ func (sm *SmartManager) parseSmartForNvme(output []byte, deviceType string) (boo
|
|||||||
return false, data.Smartctl.ExitStatus
|
return false, data.Smartctl.ExitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// smartctl may return device identity fields before failing to read the NVMe
|
||||||
|
// health log (for example, on an unsupported controller path or with insufficient
|
||||||
|
// permissions). Do not accept that partial response as valid SMART data: doing
|
||||||
|
// so stores incorrect zero values and prevents the namespace-path fallback.
|
||||||
|
log := data.NVMeSmartHealthInformationLog
|
||||||
|
if log == nil {
|
||||||
|
slog.Debug("no NVMe SMART health information", "device", data.Device.Name)
|
||||||
|
return false, data.Smartctl.ExitStatus
|
||||||
|
}
|
||||||
|
|
||||||
sm.Lock()
|
sm.Lock()
|
||||||
defer sm.Unlock()
|
defer sm.Unlock()
|
||||||
|
|
||||||
@@ -1167,7 +1184,7 @@ func (sm *SmartManager) parseSmartForNvme(output []byte, deviceType string) (boo
|
|||||||
if smartData.Capacity == 0 && (runtime.GOOS == "darwin" || sm.darwinNvmeProvider != nil) {
|
if smartData.Capacity == 0 && (runtime.GOOS == "darwin" || sm.darwinNvmeProvider != nil) {
|
||||||
smartData.Capacity = sm.lookupDarwinNvmeCapacity(data.SerialNumber)
|
smartData.Capacity = sm.lookupDarwinNvmeCapacity(data.SerialNumber)
|
||||||
}
|
}
|
||||||
smartData.Temperature = data.NVMeSmartHealthInformationLog.Temperature
|
smartData.Temperature = log.Temperature
|
||||||
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
|
||||||
@@ -1177,7 +1194,6 @@ func (sm *SmartManager) parseSmartForNvme(output []byte, deviceType string) (boo
|
|||||||
|
|
||||||
// nvme attributes does not follow the same format as ata attributes,
|
// nvme attributes does not follow the same format as ata attributes,
|
||||||
// so we manually map each field to SmartAttributes
|
// so we manually map each field to SmartAttributes
|
||||||
log := data.NVMeSmartHealthInformationLog
|
|
||||||
smartData.Attributes = []*smart.SmartAttribute{
|
smartData.Attributes = []*smart.SmartAttribute{
|
||||||
{Name: "CriticalWarning", RawValue: uint64(log.CriticalWarning)},
|
{Name: "CriticalWarning", RawValue: uint64(log.CriticalWarning)},
|
||||||
{Name: "Temperature", RawValue: uint64(log.Temperature)},
|
{Name: "Temperature", RawValue: uint64(log.Temperature)},
|
||||||
|
|||||||
@@ -837,6 +837,27 @@ func TestParseSmartOutputMarksVerified(t *testing.T) {
|
|||||||
assert.True(t, device.typeVerified)
|
assert.True(t, device.typeVerified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSmartForNvmeRejectsIdentityOnlyResponse(t *testing.T) {
|
||||||
|
jsonPayload := []byte(`{
|
||||||
|
"smartctl": {"exit_status": 2},
|
||||||
|
"device": {"name": "/dev/nvme0", "type": "nvme"},
|
||||||
|
"model_name": "Netac NVMe SSD 500GB",
|
||||||
|
"serial_number": "IDENTITY-ONLY",
|
||||||
|
"user_capacity": {"bytes": 500107862016}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
|
hasData, exitStatus := sm.parseSmartForNvme(jsonPayload, "")
|
||||||
|
|
||||||
|
assert.False(t, hasData)
|
||||||
|
assert.Equal(t, 2, exitStatus)
|
||||||
|
assert.NotContains(t, sm.SmartDataMap, "IDENTITY-ONLY")
|
||||||
|
|
||||||
|
device := &DeviceInfo{Name: "/dev/nvme0", Type: "nvme"}
|
||||||
|
assert.False(t, sm.parseSmartOutput(device, jsonPayload))
|
||||||
|
assert.NotContains(t, sm.SmartDataMap, "IDENTITY-ONLY")
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseSmartOutputKeepsCustomType(t *testing.T) {
|
func TestParseSmartOutputKeepsCustomType(t *testing.T) {
|
||||||
fixturePath := filepath.Join("test-data", "smart", "sda.json")
|
fixturePath := filepath.Join("test-data", "smart", "sda.json")
|
||||||
data, err := os.ReadFile(fixturePath)
|
data, err := os.ReadFile(fixturePath)
|
||||||
|
|||||||
@@ -503,11 +503,11 @@ type SmartInfoForNvme struct {
|
|||||||
UserCapacity UserCapacity `json:"user_capacity"`
|
UserCapacity UserCapacity `json:"user_capacity"`
|
||||||
// LogicalBlockSize int `json:"logical_block_size"`
|
// LogicalBlockSize int `json:"logical_block_size"`
|
||||||
// LocalTime LocalTime `json:"local_time"`
|
// LocalTime LocalTime `json:"local_time"`
|
||||||
SmartStatus SmartStatusInfoNvme `json:"smart_status"`
|
SmartStatus SmartStatusInfoNvme `json:"smart_status"`
|
||||||
NVMeSmartHealthInformationLog NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
NVMeSmartHealthInformationLog *NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
||||||
Temperature TemperatureInfoNvme `json:"temperature"`
|
Temperature TemperatureInfoNvme `json:"temperature"`
|
||||||
PowerCycleCount uint16 `json:"power_cycle_count"`
|
PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||||
PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"`
|
PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemperatureInfoNvme struct {
|
type TemperatureInfoNvme struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user