91f6a26490
GET /disks/candidates enumerates host disks the Impl-1 unclaimed filter proves free (init/attach split). RegistryKnownTargets sources the watchdog's known-drive set from the intent registry + Felhom .mount units (not Observe/PVE storages) — decouples drive health from PVE storage (closes the registry-only false-detach class); Observe kept for real PVE storages + a deduped /disks union. Idempotent existing-drive migration at start. Tests + red-proof (Observe misses a registry-only drive; registry provider tracks it). go build/vet/test clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
4.3 KiB
Go
99 lines
4.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// fakeIntent is a minimal IntentReader for the registry tests.
|
|
type fakeIntent struct{ m map[string]DriveIntent }
|
|
|
|
func (f *fakeIntent) Get(durableID string) DriveIntent { return f.m[durableID] }
|
|
|
|
func writeUnit(t *testing.T, dir, name, content string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write unit %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func TestRegistryKnownTargets(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeUnit(t, dir, "mnt-a.mount", renderMountUnit(MountSpec{Name: "felhom-a", UUID: "UUID-A", Where: "/mnt/felhom-a", FSType: "ext4"}))
|
|
writeUnit(t, dir, "mnt-b.mount", renderMountUnit(MountSpec{Name: "felhom-b", UUID: "UUID-B", Where: "/mnt/felhom-b", FSType: "ext4"}))
|
|
writeUnit(t, dir, "other.mount", "[Mount]\nWhat=/dev/x\nWhere=/mnt/x\n") // not one of ours → ignored
|
|
|
|
// felhom-a enrolled, felhom-b has NO intent (new).
|
|
intent := &fakeIntent{m: map[string]DriveIntent{"uuid:UUID-A": IntentEnrolled}}
|
|
r := NewRegistryKnownTargets(dir, intent, quietLogger())
|
|
|
|
got, err := r.Known(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Known: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].Name != "felhom-a" {
|
|
t.Fatalf("want only felhom-a (enrolled); new felhom-b excluded, non-felhom ignored — got %+v", got)
|
|
}
|
|
kt := got[0]
|
|
if kt.DurableID != "uuid:UUID-A" || kt.UUID != "UUID-A" || !kt.MountBacked || kt.MountPath != "/mnt/felhom-a" {
|
|
t.Fatalf("KnownTarget fields wrong: %+v", kt)
|
|
}
|
|
|
|
// An EJECTED drive is still tracked (known) — the watchdog's IntentReader gate prevents remount.
|
|
intent.m["uuid:UUID-B"] = IntentEjected
|
|
got2, _ := r.Known(context.Background())
|
|
if len(got2) != 2 {
|
|
t.Fatalf("ejected drive must still be a Known target: %+v", got2)
|
|
}
|
|
}
|
|
|
|
// The decoupling red-proof: with NO PVE storage for a drive, the OLD Observe-based Known() misses it,
|
|
// while the registry+units provider tracks it. This is exactly the 3b-fix class the decoupling closes.
|
|
func TestRegistryVsObserve_RedProof(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeUnit(t, dir, "mnt-a.mount", renderMountUnit(MountSpec{Name: "felhom-a", UUID: "UUID-A", Where: "/mnt/felhom-a", FSType: "ext4"}))
|
|
intent := &fakeIntent{m: map[string]DriveIntent{"uuid:UUID-A": IntentEnrolled}}
|
|
|
|
// OLD path: Observer with an API that has NO storages → Known() is empty (drive invisible).
|
|
obs := NewObserver(&fakeStorageAPI{node: "n"}, &fakeHostReader{}, nil, quietLogger())
|
|
oldKnown, _ := obs.Known(context.Background())
|
|
if len(oldKnown) != 0 {
|
|
t.Fatalf("precondition: Observe-based Known should be empty with no PVE storage, got %+v", oldKnown)
|
|
}
|
|
|
|
// NEW path: the registry provider tracks the drive from unit + intent, no PVE storage needed.
|
|
newKnown, _ := NewRegistryKnownTargets(dir, intent, quietLogger()).Known(context.Background())
|
|
if len(newKnown) != 1 || newKnown[0].Name != "felhom-a" {
|
|
t.Fatalf("registry provider must track the registry-only drive: %+v", newKnown)
|
|
}
|
|
}
|
|
|
|
func TestReconcileExistingDrives_Idempotent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeUnit(t, dir, "mnt-a.mount", renderMountUnit(MountSpec{Name: "felhom-a", UUID: "UUID-A", Where: "/mnt/felhom-a", FSType: "ext4"}))
|
|
store, err := OpenIntentStore(filepath.Join(t.TempDir(), "intents.json"))
|
|
if err != nil {
|
|
t.Fatalf("intent store: %v", err)
|
|
}
|
|
mounts := []Mount{{Device: "/dev/disk/by-uuid/UUID-A", MountPoint: "/mnt/felhom-a"}}
|
|
|
|
// First reconcile: an un-recorded mounted drive → becomes enrolled.
|
|
ReconcileExistingDrives(dir, mounts, store, quietLogger())
|
|
if store.Get("uuid:UUID-A") != IntentEnrolled {
|
|
t.Fatalf("reconcile must record the mounted drive enrolled, got %q", store.Get("uuid:UUID-A"))
|
|
}
|
|
// Idempotent: a re-run doesn't change/downgrade it.
|
|
ReconcileExistingDrives(dir, mounts, store, quietLogger())
|
|
if store.Get("uuid:UUID-A") != IntentEnrolled {
|
|
t.Fatalf("re-run must be a no-op, got %q", store.Get("uuid:UUID-A"))
|
|
}
|
|
// An UNMOUNTED drive is NOT auto-enrolled by the migration.
|
|
writeUnit(t, dir, "mnt-c.mount", renderMountUnit(MountSpec{Name: "felhom-c", UUID: "UUID-C", Where: "/mnt/felhom-c", FSType: "ext4"}))
|
|
ReconcileExistingDrives(dir, mounts, store, quietLogger())
|
|
if store.Get("uuid:UUID-C") != IntentNew {
|
|
t.Fatalf("unmounted drive must NOT be migrated, got %q", store.Get("uuid:UUID-C"))
|
|
}
|
|
}
|