mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 02:36:17 +01:00
improve parsing of edge case smart power on times (#1347)
This commit is contained in:
@@ -426,7 +426,7 @@ func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-aj")
|
args = append(args, "-a", "--json=c")
|
||||||
|
|
||||||
if includeStandby {
|
if includeStandby {
|
||||||
args = append(args, "-n", "standby")
|
args = append(args, "-n", "standby")
|
||||||
@@ -688,13 +688,17 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
|||||||
// update SmartAttributes
|
// update SmartAttributes
|
||||||
smartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table))
|
smartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table))
|
||||||
for _, attr := range data.AtaSmartAttributes.Table {
|
for _, attr := range data.AtaSmartAttributes.Table {
|
||||||
|
rawValue := uint64(attr.Raw.Value)
|
||||||
|
if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok {
|
||||||
|
rawValue = parsed
|
||||||
|
}
|
||||||
smartAttr := &smart.SmartAttribute{
|
smartAttr := &smart.SmartAttribute{
|
||||||
ID: attr.ID,
|
ID: attr.ID,
|
||||||
Name: attr.Name,
|
Name: attr.Name,
|
||||||
Value: attr.Value,
|
Value: attr.Value,
|
||||||
Worst: attr.Worst,
|
Worst: attr.Worst,
|
||||||
Threshold: attr.Thresh,
|
Threshold: attr.Thresh,
|
||||||
RawValue: uint64(attr.Raw.Value),
|
RawValue: rawValue,
|
||||||
RawString: attr.Raw.String,
|
RawString: attr.Raw.String,
|
||||||
WhenFailed: attr.WhenFailed,
|
WhenFailed: attr.WhenFailed,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,49 @@ func TestParseSmartForSata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
||||||
|
jsonPayload := []byte(`{
|
||||||
|
"smartctl": {"exit_status": 0},
|
||||||
|
"device": {"name": "/dev/sdz", "type": "sat"},
|
||||||
|
"model_name": "Example",
|
||||||
|
"serial_number": "PARENTHESES123",
|
||||||
|
"firmware_version": "1.0",
|
||||||
|
"user_capacity": {"bytes": 1024},
|
||||||
|
"smart_status": {"passed": true},
|
||||||
|
"temperature": {"current": 25},
|
||||||
|
"ata_smart_attributes": {
|
||||||
|
"table": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "Power_On_Hours",
|
||||||
|
"value": 93,
|
||||||
|
"worst": 55,
|
||||||
|
"thresh": 0,
|
||||||
|
"when_failed": "",
|
||||||
|
"raw": {
|
||||||
|
"value": 57891864217128,
|
||||||
|
"string": "39925 (212 206 0)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
|
|
||||||
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
||||||
|
require.True(t, hasData)
|
||||||
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
|
data, ok := sm.SmartDataMap["PARENTHESES123"]
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, data.Attributes, 1)
|
||||||
|
|
||||||
|
attr := data.Attributes[0]
|
||||||
|
assert.Equal(t, uint64(39925), attr.RawValue)
|
||||||
|
assert.Equal(t, "39925 (212 206 0)", attr.RawString)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseSmartForNvme(t *testing.T) {
|
func TestParseSmartForNvme(t *testing.T) {
|
||||||
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
|
||||||
data, err := os.ReadFile(fixturePath)
|
data, err := os.ReadFile(fixturePath)
|
||||||
@@ -198,7 +241,7 @@ func TestSmartctlArgsWithoutType(t *testing.T) {
|
|||||||
sm := &SmartManager{}
|
sm := &SmartManager{}
|
||||||
|
|
||||||
args := sm.smartctlArgs(device, true)
|
args := sm.smartctlArgs(device, true)
|
||||||
assert.Equal(t, []string{"-aj", "-n", "standby", "/dev/sda"}, args)
|
assert.Equal(t, []string{"-a", "--json=c", "-n", "standby", "/dev/sda"}, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmartctlArgs(t *testing.T) {
|
func TestSmartctlArgs(t *testing.T) {
|
||||||
@@ -206,17 +249,17 @@ 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", "-aj", "-n", "standby", "/dev/sda"},
|
[]string{"-d", "sat", "-a", "--json=c", "-n", "standby", "/dev/sda"},
|
||||||
sm.smartctlArgs(sataDevice, true),
|
sm.smartctlArgs(sataDevice, true),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{"-d", "sat", "-aj", "/dev/sda"},
|
[]string{"-d", "sat", "-a", "--json=c", "/dev/sda"},
|
||||||
sm.smartctlArgs(sataDevice, false),
|
sm.smartctlArgs(sataDevice, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{"-aj", "-n", "standby"},
|
[]string{"-a", "--json=c", "-n", "standby"},
|
||||||
sm.smartctlArgs(nil, true),
|
sm.smartctlArgs(nil, true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package smart
|
package smart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -160,6 +161,33 @@ type RawValue struct {
|
|||||||
String string `json:"string"`
|
String string `json:"string"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RawValue) UnmarshalJSON(data []byte) error {
|
||||||
|
var tmp struct {
|
||||||
|
Value json.RawMessage `json:"value"`
|
||||||
|
String string `json:"string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tmp.Value) > 0 {
|
||||||
|
if err := r.Value.UnmarshalJSON(tmp.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
r.String = tmp.String
|
||||||
|
|
||||||
|
if parsed, ok := ParseSmartRawValueString(tmp.String); ok {
|
||||||
|
r.Value = SmartRawValue(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type SmartRawValue uint64
|
type SmartRawValue uint64
|
||||||
|
|
||||||
// handles when drives report strings like "0h+0m+0.000s" or "7344 (253d 8h)" for power on hours
|
// handles when drives report strings like "0h+0m+0.000s" or "7344 (253d 8h)" for power on hours
|
||||||
@@ -170,61 +198,73 @@ func (v *SmartRawValue) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed[0] != '"' {
|
if trimmed[0] == '"' {
|
||||||
parsed, err := strconv.ParseUint(trimmed, 0, 64)
|
valueStr, err := strconv.Unquote(trimmed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*v = SmartRawValue(parsed)
|
parsed, ok := ParseSmartRawValueString(valueStr)
|
||||||
return nil
|
if ok {
|
||||||
}
|
*v = SmartRawValue(parsed)
|
||||||
|
return nil
|
||||||
valueStr, err := strconv.Unquote(trimmed)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if valueStr == "" {
|
|
||||||
*v = 0
|
*v = 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed, err := strconv.ParseUint(valueStr, 0, 64); err == nil {
|
if parsed, err := strconv.ParseUint(trimmed, 0, 64); err == nil {
|
||||||
*v = SmartRawValue(parsed)
|
*v = SmartRawValue(parsed)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx := strings.IndexRune(valueStr, 'h'); idx >= 0 {
|
if parsed, ok := ParseSmartRawValueString(trimmed); ok {
|
||||||
hoursPart := strings.TrimSpace(valueStr[:idx])
|
*v = SmartRawValue(parsed)
|
||||||
if hoursPart == "" {
|
return nil
|
||||||
*v = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {
|
|
||||||
*v = SmartRawValue(uint64(parsed))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if digits := leadingDigitPrefix(valueStr); digits != "" {
|
|
||||||
if parsed, err := strconv.ParseUint(digits, 0, 64); err == nil {
|
|
||||||
*v = SmartRawValue(parsed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*v = 0
|
*v = 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func leadingDigitPrefix(value string) string {
|
// ParseSmartRawValueString attempts to extract a numeric value from the raw value
|
||||||
var builder strings.Builder
|
// strings emitted by smartctl, which sometimes include human-friendly annotations
|
||||||
for _, r := range value {
|
// like "7344 (253d 8h)" or "0h+0m+0.000s". It returns the parsed value and a
|
||||||
if r < '0' || r > '9' {
|
// boolean indicating success.
|
||||||
break
|
func ParseSmartRawValueString(value string) (uint64, bool) {
|
||||||
}
|
value = strings.TrimSpace(value)
|
||||||
builder.WriteRune(r)
|
if value == "" {
|
||||||
|
return 0, false
|
||||||
}
|
}
|
||||||
return builder.String()
|
|
||||||
|
if parsed, err := strconv.ParseUint(value, 0, 64); err == nil {
|
||||||
|
return parsed, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.IndexRune(value, 'h'); idx > 0 {
|
||||||
|
hoursPart := strings.TrimSpace(value[:idx])
|
||||||
|
if hoursPart != "" {
|
||||||
|
if parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {
|
||||||
|
return uint64(parsed), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(value); i++ {
|
||||||
|
if value[i] < '0' || value[i] > '9' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end := i + 1
|
||||||
|
for end < len(value) && value[end] >= '0' && value[end] <= '9' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
digits := value[i:end]
|
||||||
|
if parsed, err := strconv.ParseUint(digits, 10, 64); err == nil {
|
||||||
|
return parsed, true
|
||||||
|
}
|
||||||
|
i = end
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// type PowerOnTimeInfo struct {
|
// type PowerOnTimeInfo struct {
|
||||||
|
|||||||
@@ -3,28 +3,60 @@ package smart
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSmartRawValueUnmarshalDuration(t *testing.T) {
|
func TestSmartRawValueUnmarshalDuration(t *testing.T) {
|
||||||
input := []byte(`{"value":"62312h+33m+50.907s","string":"62312h+33m+50.907s"}`)
|
input := []byte(`{"value":"62312h+33m+50.907s","string":"62312h+33m+50.907s"}`)
|
||||||
var raw RawValue
|
var raw RawValue
|
||||||
if err := json.Unmarshal(input, &raw); err != nil {
|
err := json.Unmarshal(input, &raw)
|
||||||
t.Fatalf("unexpected error unmarshalling raw value: %v", err)
|
assert.NoError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
if uint64(raw.Value) != 62312 {
|
assert.EqualValues(t, 62312, raw.Value)
|
||||||
t.Fatalf("expected hours to be 62312, got %d", raw.Value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmartRawValueUnmarshalNumericString(t *testing.T) {
|
func TestSmartRawValueUnmarshalNumericString(t *testing.T) {
|
||||||
input := []byte(`{"value":"7344","string":"7344"}`)
|
input := []byte(`{"value":"7344","string":"7344"}`)
|
||||||
var raw RawValue
|
var raw RawValue
|
||||||
if err := json.Unmarshal(input, &raw); err != nil {
|
err := json.Unmarshal(input, &raw)
|
||||||
t.Fatalf("unexpected error unmarshalling numeric string: %v", err)
|
assert.NoError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
if uint64(raw.Value) != 7344 {
|
assert.EqualValues(t, 7344, raw.Value)
|
||||||
t.Fatalf("expected hours to be 7344, got %d", raw.Value)
|
}
|
||||||
}
|
|
||||||
|
func TestSmartRawValueUnmarshalParenthetical(t *testing.T) {
|
||||||
|
input := []byte(`{"value":"39925 (212 206 0)","string":"39925 (212 206 0)"}`)
|
||||||
|
var raw RawValue
|
||||||
|
err := json.Unmarshal(input, &raw)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 39925, raw.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmartRawValueUnmarshalDurationWithFractions(t *testing.T) {
|
||||||
|
input := []byte(`{"value":"2748h+31m+49.560s","string":"2748h+31m+49.560s"}`)
|
||||||
|
var raw RawValue
|
||||||
|
err := json.Unmarshal(input, &raw)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 2748, raw.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmartRawValueUnmarshalParentheticalRawValue(t *testing.T) {
|
||||||
|
input := []byte(`{"value":57891864217128,"string":"39925 (212 206 0)"}`)
|
||||||
|
var raw RawValue
|
||||||
|
err := json.Unmarshal(input, &raw)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 39925, raw.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmartRawValueUnmarshalDurationRawValue(t *testing.T) {
|
||||||
|
input := []byte(`{"value":57891864217128,"string":"2748h+31m+49.560s"}`)
|
||||||
|
var raw RawValue
|
||||||
|
err := json.Unmarshal(input, &raw)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 2748, raw.Value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
## 0.15.3
|
||||||
|
|
||||||
|
- Improve parsing of edge case S.M.A.R.T. power on times. (#1347)
|
||||||
|
|
||||||
|
## 0.15.2
|
||||||
|
|
||||||
|
- Improve S.M.A.R.T. device detection logic (fix regression in 0.15.1) (#1345)
|
||||||
|
|
||||||
## 0.15.1
|
## 0.15.1
|
||||||
|
|
||||||
- Add `SMART_DEVICES` environment variable to specify devices and types. (#373, #1335)
|
- Add `SMART_DEVICES` environment variable to specify devices and types. (#373, #1335)
|
||||||
|
|||||||
Reference in New Issue
Block a user