v0.95.0: SMART coverage — union-path drives + LVM/dm root + device model

Implements SPIKE-smart-coverage-2026-07-25 fixes B+A (additive; MinAgent unchanged).
Fix B: storage.SmartReader.SMARTForBacking wired into the /disks union path (localapi
Smart seam) so registry/USB drives get a real SMART read (watchdog Known stays
enrich-free). Fix A: smartDeviceFor resolves dm/LVM to the whole disk via
/sys/block/<dm>/slaves (recursive; skips >1-disk); the builtin local dir on the LVM
root gets a SMART-only device from its containing filesystem (never touches
backing/durable_id). SmartSummary.ModelName captured from smartctl. Fix C (-d sat)
stays rejected. Tests + red-proofs (dm multi-disk skip, enrich smartHint, union
routing); Known-path-never-SMARTs asserted.
This commit is contained in:
2026-07-25 08:21:45 +02:00
parent 643899c191
commit ed97232598
13 changed files with 579 additions and 74 deletions
+75
View File
@@ -1,6 +1,9 @@
package localapi
import (
"context"
"io"
"log/slog"
"net/http"
"testing"
@@ -62,3 +65,75 @@ func TestDisks_SmartSerialized(t *testing.T) {
t.Errorf("nosmart disk: smart must be omitted when Health is empty, got %+v", byName["nosmart"].Smart)
}
}
// ---- Fix B (v0.95.0): the /disks union path reads SMART for registry/USB drives ----
type fakeKnownTargets struct{ drives []storage.KnownTarget }
func (f fakeKnownTargets) Known(context.Context) ([]storage.KnownTarget, error) { return f.drives, nil }
type fakeSmartReader struct {
byDev map[string]hub.SmartSummary
calls []string
}
func (f *fakeSmartReader) SMARTForBacking(_ context.Context, dev string) hub.SmartSummary {
f.calls = append(f.calls, dev)
if s, ok := f.byDev[dev]; ok {
return s
}
return hub.SmartSummary{}
}
func sp(s string) *string { return &s }
// A union-path (registry/USB) drive now gets a real SMART read + model, via the Smart seam — it used
// to ride the enrich-free union path and show "Nincs adat".
// Red-proof: delete the Fix-B block in handleDisks (the s.smart.SMARTForBacking call) → the union
// drive carries no smart and this fails.
func TestDisks_UnionPathReadsSMART(t *testing.T) {
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}}
sm := &fakeSmartReader{byDev: map[string]hub.SmartSummary{
"/dev/sdb1": {Health: hub.SmartPassed, ModelName: sp("TOSHIBA MQ04ABF100")},
}}
srv, err := NewServer(Options{
ListenAddr: "127.0.0.1:0",
Guests: &fakeGuests{},
Backups: &fakeBackups{},
Store: &fakeStore{},
Storage: fakeStorage{}, // no Observe targets → the union drive is not deduped away
DriveTargets: fakeKnownTargets{drives: []storage.KnownTarget{
{Name: "data-usb", Type: hub.StorageTypeUSB, MountPath: "/mnt/hdd_1", DurableID: "uuid:47a3", UUID: "47a3"},
}},
Smart: sm,
Tokens: staticTokens{"A": 8200},
Disks: d,
HostReader: sysOnSDA(),
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
})
if err != nil {
t.Fatalf("new server: %v", err)
}
srv.baseCtx = context.Background()
srv.resolveStorageDevice = func(string) (string, error) { return "/dev/sdb1", nil }
disks := decodeDisks(t, do(t, srv.Handler(), "GET", "/disks", "A", "").Body.Bytes())
var usb *DiskInfo
for i := range disks {
if disks[i].Name == "data-usb" {
usb = &disks[i]
}
}
if usb == nil {
t.Fatalf("union drive not in /disks: %+v", disks)
}
if usb.Smart == nil || usb.Smart.Health != hub.SmartPassed {
t.Fatalf("union drive SMART not read: %+v", usb.Smart)
}
if usb.Smart.ModelName == nil || *usb.Smart.ModelName != "TOSHIBA MQ04ABF100" {
t.Errorf("union drive model not carried: %v", usb.Smart)
}
if len(sm.calls) != 1 || sm.calls[0] != "/dev/sdb1" {
t.Errorf("SMART should be read once on /dev/sdb1, got %v", sm.calls)
}
}