86 lines
2.1 KiB
Go
86 lines
2.1 KiB
Go
package beach
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os/exec"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// parseSmartctl reads `smartctl -j -H -i -A` output for one device.
|
|
func parseSmartctl(b []byte) (Disk, error) {
|
|
var r struct {
|
|
Device struct {
|
|
Name string `json:"name"`
|
|
} `json:"device"`
|
|
ModelName string `json:"model_name"`
|
|
SerialNumber string `json:"serial_number"`
|
|
UserCapacity struct {
|
|
Bytes int64 `json:"bytes"`
|
|
} `json:"user_capacity"`
|
|
SmartStatus struct {
|
|
Passed *bool `json:"passed"`
|
|
} `json:"smart_status"`
|
|
Temperature struct {
|
|
Current float64 `json:"current"`
|
|
} `json:"temperature"`
|
|
}
|
|
if err := json.Unmarshal(b, &r); err != nil {
|
|
return Disk{}, err
|
|
}
|
|
d := Disk{Name: r.Device.Name, Model: r.ModelName, Serial: r.SerialNumber, Size: r.UserCapacity.Bytes, TempC: r.Temperature.Current, Source: "smartctl"}
|
|
if r.SmartStatus.Passed != nil {
|
|
d.Health = "FAILED"
|
|
if *r.SmartStatus.Passed {
|
|
d.Health = "PASSED"
|
|
}
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// runner lets tests replace the smartctl binary.
|
|
type runner func(ctx context.Context, args ...string) ([]byte, error)
|
|
|
|
func execSmartctl(ctx context.Context, args ...string) ([]byte, error) {
|
|
// smartctl uses non-zero exit bits to report problems; the JSON is still valid
|
|
out, err := exec.CommandContext(ctx, "smartctl", args...).Output()
|
|
if len(out) > 0 {
|
|
return out, nil
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
// LocalSmart scans for devices and reads each one.
|
|
func LocalSmart(ctx context.Context, run runner) ([]Disk, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
|
defer cancel()
|
|
b, err := run(ctx, "--scan", "-j")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var scan struct {
|
|
Devices []struct {
|
|
Name string `json:"name"`
|
|
} `json:"devices"`
|
|
}
|
|
if err := json.Unmarshal(b, &scan); err != nil {
|
|
return nil, err
|
|
}
|
|
out := []Disk{}
|
|
for _, dev := range scan.Devices {
|
|
b, err := run(ctx, "-j", "-H", "-i", "-A", dev.Name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if d, err := parseSmartctl(b); err == nil {
|
|
if d.Name == "" {
|
|
d.Name = dev.Name
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|