Initial Beach: homelab control panel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
6938da4d7a
18 files changed
+2071
No files matched your search
@@ -0,0 +1,85 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user