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 }