mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-18 08:17:47 +02:00
Compare commits
1 Commits
v0.18.8
...
smart-devi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
214180a431 |
@@ -65,6 +65,14 @@ type deviceKey struct {
|
|||||||
|
|
||||||
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
var errNoValidSmartData = fmt.Errorf("no valid SMART data found") // Error for missing data
|
||||||
|
|
||||||
|
// isBridgePassthroughType reports whether the device type uses a USB bridge
|
||||||
|
// passthrough driver that may fail transiently (exit 2) due to wakeup data
|
||||||
|
// or other bridge-specific quirks. A single retry is attempted in CollectSmart.
|
||||||
|
func isBridgePassthroughType(deviceType string) bool {
|
||||||
|
dt := strings.ToLower(deviceType)
|
||||||
|
return strings.HasPrefix(dt, "jms56x") || strings.HasPrefix(dt, "jmb39x")
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh updates SMART data for all known devices
|
// Refresh updates SMART data for all known devices
|
||||||
func (sm *SmartManager) Refresh(forceScan bool) error {
|
func (sm *SmartManager) Refresh(forceScan bool) error {
|
||||||
sm.refreshMutex.Lock()
|
sm.refreshMutex.Lock()
|
||||||
@@ -368,9 +376,15 @@ func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte)
|
|||||||
Type string
|
Type string
|
||||||
Parse func([]byte) (bool, int)
|
Parse func([]byte) (bool, int)
|
||||||
}{
|
}{
|
||||||
{Type: "nvme", Parse: sm.parseSmartForNvme},
|
{Type: "nvme", Parse: func(output []byte) (bool, int) {
|
||||||
{Type: "sat", Parse: sm.parseSmartForSata},
|
return sm.parseSmartForNvme(output, deviceInfo.Type)
|
||||||
{Type: "scsi", Parse: sm.parseSmartForScsi},
|
}},
|
||||||
|
{Type: "sat", Parse: func(output []byte) (bool, int) {
|
||||||
|
return sm.parseSmartForSata(output, deviceInfo.Type)
|
||||||
|
}},
|
||||||
|
{Type: "scsi", Parse: func(output []byte) (bool, int) {
|
||||||
|
return sm.parseSmartForScsi(output, deviceInfo.Type)
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceType := normalizeParserType(deviceInfo.parserType)
|
deviceType := normalizeParserType(deviceInfo.parserType)
|
||||||
@@ -495,15 +509,22 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
|||||||
// Check if device is in standby (exit status 2)
|
// Check if device is in standby (exit status 2)
|
||||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 2 {
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 2 {
|
||||||
if hasExistingData {
|
if hasExistingData {
|
||||||
// Device is in standby and we have cached data, keep using cache
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// No cached data, need to collect initial data by bypassing standby
|
// No cached data, retry without -n standby
|
||||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
|
||||||
defer cancel2()
|
|
||||||
args = sm.smartctlArgs(deviceInfo, false)
|
args = sm.smartctlArgs(deviceInfo, false)
|
||||||
cmd = exec.CommandContext(ctx2, sm.smartctlPath, args...)
|
cmd = exec.CommandContext(ctx, sm.smartctlPath, args...)
|
||||||
output, err = cmd.CombinedOutput()
|
output, err = cmd.CombinedOutput()
|
||||||
|
|
||||||
|
// Bridge passthrough drivers (jms56x, jmb39x) can fail siently due
|
||||||
|
// to wakeup data left by the bridge firmware. A second retry succeeds
|
||||||
|
// after the first attempt has cleared the bridge state.
|
||||||
|
if deviceInfo != nil && isBridgePassthroughType(deviceInfo.Type) {
|
||||||
|
if exitErr2, ok2 := errors.AsType[*exec.ExitError](err); ok2 && exitErr2.ExitCode() == 2 {
|
||||||
|
cmd = exec.CommandContext(ctx, sm.smartctlPath, args...)
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidData := sm.parseSmartOutput(deviceInfo, output)
|
hasValidData := sm.parseSmartOutput(deviceInfo, output)
|
||||||
@@ -733,6 +754,7 @@ func mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo
|
|||||||
}
|
}
|
||||||
if existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {
|
if existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {
|
||||||
applyConfiguredMetadata(existingDev, configuredDevice)
|
applyConfiguredMetadata(existingDev, configuredDevice)
|
||||||
|
delete(deviceIndexByName, configuredDevice.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,9 +858,12 @@ func (sm *SmartManager) isVirtualDeviceFromStrings(fields ...string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap
|
// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap.
|
||||||
// Returns hasValidData and exitStatus
|
// configuredType is the user-specified device type (e.g., "sat", "jms56x,0") from SMART_DEVICES.
|
||||||
func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
// When non-empty, it overrides the disk type reported by smartctl in SmartData.DiskType so that
|
||||||
|
// updateSmartDevices can match entries by the configured composite (name, type) key.
|
||||||
|
// Returns hasValidData and exitStatus.
|
||||||
|
func (sm *SmartManager) parseSmartForSata(output []byte, configuredType string) (bool, int) {
|
||||||
var data smart.SmartInfoForSata
|
var data smart.SmartInfoForSata
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
if err := json.Unmarshal(output, &data); err != nil {
|
||||||
@@ -877,6 +902,9 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
|||||||
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
|
||||||
|
if configuredType != "" {
|
||||||
|
smartData.DiskType = normalizeParserType(configuredType)
|
||||||
|
}
|
||||||
|
|
||||||
// get values from ata_device_statistics if necessary
|
// get values from ata_device_statistics if necessary
|
||||||
var ataDeviceStats smart.AtaDeviceStatistics
|
var ataDeviceStats smart.AtaDeviceStatistics
|
||||||
@@ -950,7 +978,7 @@ func findAtaDeviceStatisticsValue(data *smart.SmartInfoForSata, ataDeviceStats *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
func (sm *SmartManager) parseSmartForScsi(output []byte, configuredType string) (bool, int) {
|
||||||
var data smart.SmartInfoForScsi
|
var data smart.SmartInfoForScsi
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
if err := json.Unmarshal(output, &data); err != nil {
|
||||||
@@ -985,6 +1013,9 @@ func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
|||||||
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
|
||||||
|
if configuredType != "" {
|
||||||
|
smartData.DiskType = normalizeParserType(configuredType)
|
||||||
|
}
|
||||||
|
|
||||||
attributes := make([]*smart.SmartAttribute, 0, 10)
|
attributes := make([]*smart.SmartAttribute, 0, 10)
|
||||||
attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours})
|
attributes = append(attributes, &smart.SmartAttribute{Name: "PowerOnHours", RawValue: data.PowerOnTime.Hours})
|
||||||
@@ -1082,9 +1113,12 @@ func (sm *SmartManager) lookupDarwinNvmeCapacity(serial string) uint64 {
|
|||||||
return sm.darwinNvmeCapacity[serial]
|
return sm.darwinNvmeCapacity[serial]
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap
|
// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap.
|
||||||
// Returns hasValidData and exitStatus
|
// configuredType is the user-specified device type (e.g., "nvme") from SMART_DEVICES.
|
||||||
func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
|
// When non-empty, it overrides the disk type reported by smartctl in SmartData.DiskType so that
|
||||||
|
// updateSmartDevices can match entries by the configured composite (name, type) key.
|
||||||
|
// Returns hasValidData and exitStatus.
|
||||||
|
func (sm *SmartManager) parseSmartForNvme(output []byte, configuredType string) (bool, int) {
|
||||||
data := &smart.SmartInfoForNvme{}
|
data := &smart.SmartInfoForNvme{}
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &data); err != nil {
|
if err := json.Unmarshal(output, &data); err != nil {
|
||||||
@@ -1128,6 +1162,9 @@ func (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {
|
|||||||
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
|
||||||
|
if configuredType != "" {
|
||||||
|
smartData.DiskType = normalizeParserType(configuredType)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestParseSmartForScsi(t *testing.T) {
|
|||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForScsi(data)
|
hasData, exitStatus := sm.parseSmartForScsi(data, "")
|
||||||
if !hasData {
|
if !hasData {
|
||||||
t.Fatalf("expected SCSI data to parse successfully")
|
t.Fatalf("expected SCSI data to parse successfully")
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func TestParseSmartForSata(t *testing.T) {
|
|||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForSata(data)
|
hasData, exitStatus := sm.parseSmartForSata(data, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 64, exitStatus)
|
assert.Equal(t, 64, exitStatus)
|
||||||
|
|
||||||
@@ -88,6 +88,26 @@ func TestParseSmartForSata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSmartForSataWithConfiguredType(t *testing.T) {
|
||||||
|
fixturePath := filepath.Join("test-data", "smart", "sda.json")
|
||||||
|
data, err := os.ReadFile(fixturePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sm := &SmartManager{
|
||||||
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
|
}
|
||||||
|
|
||||||
|
hasData, exitStatus := sm.parseSmartForSata(data, "sat+megaraid")
|
||||||
|
require.True(t, hasData)
|
||||||
|
assert.Equal(t, 64, exitStatus)
|
||||||
|
|
||||||
|
deviceData, ok := sm.SmartDataMap["9C40918040082"]
|
||||||
|
require.True(t, ok, "expected smart data entry for serial 9C40918040082")
|
||||||
|
|
||||||
|
assert.Equal(t, "sat+megaraid", deviceData.DiskType,
|
||||||
|
"DiskType should be overridden by configuredType")
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
||||||
jsonPayload := []byte(`{
|
jsonPayload := []byte(`{
|
||||||
"smartctl": {"exit_status": 0},
|
"smartctl": {"exit_status": 0},
|
||||||
@@ -112,7 +132,7 @@ func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -147,7 +167,7 @@ func TestParseSmartForSataAtaDeviceStatistics(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -184,7 +204,7 @@ func TestParseSmartForSataNegativeDeviceStatistics(t *testing.T) {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -223,7 +243,7 @@ func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
|||||||
|
|
||||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -245,7 +265,7 @@ func TestParseSmartForNvme(t *testing.T) {
|
|||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, exitStatus := sm.parseSmartForNvme(data)
|
hasData, exitStatus := sm.parseSmartForNvme(data, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
assert.Equal(t, 0, exitStatus)
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
@@ -1225,7 +1245,7 @@ func TestParseSmartForNvmeAppleSSD(t *testing.T) {
|
|||||||
darwinNvmeProvider: fakeProvider,
|
darwinNvmeProvider: fakeProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
hasData, _ := sm.parseSmartForNvme(data)
|
hasData, _ := sm.parseSmartForNvme(data, "")
|
||||||
require.True(t, hasData)
|
require.True(t, hasData)
|
||||||
|
|
||||||
deviceData, ok := sm.SmartDataMap["0ba0147940253c15"]
|
deviceData, ok := sm.SmartDataMap["0ba0147940253c15"]
|
||||||
@@ -1237,7 +1257,7 @@ func TestParseSmartForNvmeAppleSSD(t *testing.T) {
|
|||||||
assert.Equal(t, 1, providerCalls, "system_profiler should be called once")
|
assert.Equal(t, 1, providerCalls, "system_profiler should be called once")
|
||||||
|
|
||||||
// Second parse: provider should NOT be called again (cache hit)
|
// Second parse: provider should NOT be called again (cache hit)
|
||||||
_, _ = sm.parseSmartForNvme(data)
|
_, _ = sm.parseSmartForNvme(data, "")
|
||||||
assert.Equal(t, 1, providerCalls, "system_profiler should not be called again after caching")
|
assert.Equal(t, 1, providerCalls, "system_profiler should not be called again after caching")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user