Public source validation / validate (push) Failing after 3m8s
188 lines
5.8 KiB
Go
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
|
|
}
|