Files
ITWorx-Pulse-Public/internal/disk/smart.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

188 lines
5.8 KiB
Go

package disk
import (
"errors"
"sort"
"strings"
"time"
)
type RawSMARTAttribute struct {
ID string `json:"id"`
Name string `json:"name"`
RawValue int64 `json:"rawValue"`
NormalizedValue *float64 `json:"normalizedValue,omitempty"`
Unit string `json:"unit,omitempty"`
}
type SMARTAttribute struct {
ID string `json:"id"`
Name string `json:"name"`
RawValue int64 `json:"rawValue"`
NormalizedValue *float64 `json:"normalizedValue,omitempty"`
Unit string `json:"unit,omitempty"`
Status string `json:"status"`
Critical bool `json:"critical"`
Reason string `json:"reason,omitempty"`
}
type RawSelfTest struct {
Supported bool `json:"supported"`
Result string `json:"result"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type SelfTest struct {
Supported bool `json:"supported"`
Result string `json:"result"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
AgeSeconds *float64 `json:"ageSeconds,omitempty"`
}
type RawSMART struct {
Available bool `json:"available"`
Overall string `json:"overall"`
ObservedAt time.Time `json:"observedAt"`
Attributes []RawSMARTAttribute `json:"attributes,omitempty"`
SelfTest *RawSelfTest `json:"selfTest,omitempty"`
}
type SMART struct {
State string `json:"state"`
Overall string `json:"overall"`
ObservedAt *time.Time `json:"observedAt,omitempty"`
Attributes []SMARTAttribute `json:"attributes,omitempty"`
SelfTest *SelfTest `json:"selfTest,omitempty"`
Reasons []string `json:"reasons,omitempty"`
}
const (
SMARTAvailable = "available"
SMARTUnknown = "unknown"
SMARTAttention = "attention"
SMARTFailed = "failed"
SMARTPassed = "passed"
)
func normalizeSMART(raw *RawSMART, now time.Time, policy Policy) (*SMART, error) {
if raw == nil {
return nil, nil
}
result := &SMART{State: SMARTUnknown, Overall: SMARTUnknown, Attributes: []SMARTAttribute{}, Reasons: []string{}}
if !raw.Available {
result.Reasons = []string{"smart_unavailable"}
return result, nil
}
observed := raw.ObservedAt
if observed.IsZero() {
observed = now
}
observed = observed.UTC()
result.ObservedAt = &observed
if now.Sub(observed) > policy.SMARTFreshnessMaxAge {
result.Reasons = []string{"smart_stale"}
return result, nil
}
result.State = SMARTAvailable
result.Overall = normalizeOverall(raw.Overall)
attrs := make([]SMARTAttribute, 0, len(raw.Attributes))
reasons := map[string]bool{}
for _, item := range raw.Attributes {
if strings.TrimSpace(item.ID) == "" || len(item.ID) > 64 || len(item.Name) > 128 {
return nil, errors.New("SMART attribute identity is invalid")
}
canonical := canonicalAttribute(item.ID, item.Name)
critical, reason := criticalAttribute(canonical, item.RawValue)
status := "normal"
if critical {
status = SMARTAttention
reasons[reason] = true
}
attrs = append(attrs, SMARTAttribute{ID: item.ID, Name: item.Name, RawValue: item.RawValue, NormalizedValue: item.NormalizedValue, Unit: bounded(item.Unit, ""), Status: status, Critical: critical, Reason: reason})
}
sort.Slice(attrs, func(i, j int) bool {
if attrs[i].Critical != attrs[j].Critical {
return attrs[i].Critical
}
return attrs[i].ID < attrs[j].ID
})
if raw.SelfTest != nil {
self, err := normalizeSelfTest(raw.SelfTest, now)
if err != nil {
return nil, err
}
result.SelfTest = self
}
for _, reason := range []string{"reallocated_sectors", "pending_sectors", "offline_uncorrectable", "crc_errors", "wear"} {
if reasons[reason] {
result.Reasons = append(result.Reasons, reason)
}
}
if len(result.Reasons) > 0 && result.Overall == SMARTPassed {
result.Overall = SMARTAttention
}
if result.Overall == SMARTFailed {
result.Reasons = append(result.Reasons, "smart_overall_failed")
}
return result, nil
}
func normalizeOverall(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case SMARTPassed:
return SMARTPassed
case SMARTFailed:
return SMARTFailed
case SMARTAttention, "warning", "warn":
return SMARTAttention
default:
return SMARTUnknown
}
}
func canonicalAttribute(id, name string) string {
value := strings.ToLower(strings.TrimSpace(id) + " " + strings.TrimSpace(name))
switch {
case strings.Contains(value, "197") || strings.Contains(value, "pending"):
return "pending"
case strings.Contains(value, "198") || strings.Contains(value, "offline") || strings.Contains(value, "uncorrectable"):
return "offline"
case strings.Contains(value, "199") || strings.Contains(value, "crc") || strings.Contains(value, "interface"):
return "crc"
case strings.Contains(value, "realloc") || strings.Contains(value, " 5 "):
return "reallocated"
case strings.Contains(value, "wear") || strings.Contains(value, "life") || strings.Contains(value, "percent_used") || strings.Contains(value, "177"):
return "wear"
default:
return "other"
}
}
func criticalAttribute(kind string, value int64) (bool, string) {
if value <= 0 {
return false, ""
}
switch kind {
case "reallocated":
return true, "reallocated_sectors"
case "pending":
return true, "pending_sectors"
case "offline":
return true, "offline_uncorrectable"
case "crc":
return true, "crc_errors"
case "wear":
return true, "wear"
default:
return false, ""
}
}
func normalizeSelfTest(raw *RawSelfTest, now time.Time) (*SelfTest, error) {
result := &SelfTest{Supported: raw.Supported, Result: normalizeOverall(raw.Result)}
if raw.CompletedAt != nil {
value := raw.CompletedAt.UTC()
if value.After(now.Add(time.Minute)) {
return nil, errors.New("SMART self-test is materially in the future")
}
result.CompletedAt = &value
age := now.Sub(value).Seconds()
if age < 0 {
age = 0
}
result.AgeSeconds = &age
}
return result, nil
}