From e593fb277a4467bfcb7c3cc36bdcbd5ee84ccb44 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 1 Jul 2026 18:29:22 +0200 Subject: [PATCH] v0.56.0: resolve a raw enrolled drive's durable-id from the mount table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit durableIDForMount was Observe-only, so a raw (non-PVE-storage) drive's enroll intent + guest-bind went unrecorded ("durable-id unresolved") — it mounted+bound but wasn't intent-tracked (RegistryKnownTargets skips intent==new). Fall back to resolving the mount's device fs-UUID (HostReader.Mounts + ResolveUUID) → uuid:, same scheme as Observe. Test + red-proof. Residual: ReassertGuestBinds still Observe-based (raw guest-bind not re-asserted post-reboot). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 +++++++++ cmd/felhom-agent/main.go | 2 +- internal/localapi/disks.go | 27 +++++++++--- internal/localapi/durableid_rawmount_test.go | 43 ++++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 internal/localapi/durableid_rawmount_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index fa535be..9d1f978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## v0.56.0 — record intent/guest-bind for a RAW enrolled drive (durableIDForMount fallback) (2026-07-01) + +Surfaced by the first live raw enrollment (Impl-2b): a raw drive is not a PVE storage, so +`durableIDForMount` (Observe-based) returned "" for it → the enroll's intent + guest-bind recording +logged "durable-id unresolved" and silently skipped. Result: the drive mounted + bound + usable, but was +NOT intent-tracked (so `RegistryKnownTargets` — which gates on intent ≠ new — didn't health-track it) and +its guest-bind wasn't persisted. + +- **`internal/localapi/disks.go` `durableIDForMount`:** after the Observe lookup, fall back to resolving + the fs-UUID directly from the mount table — the device mounted at `where` (via `HostReader.Mounts`) → + its by-uuid identity (`HostReader.ResolveUUID`) → `uuid:` (the SAME scheme Observe derives, so + intent keys stay consistent). Fixes intent recording (enroll/eject) AND guest-bind recording for raw + drives; the PVE-storage path is unchanged. +- Test `TestDurableIDForMount_RawFallback` (+ red-proof: Observe-only → ""). `go build/vet/test ./...` clean. +- **KNOWN residual (follow-up):** `ReassertGuestBinds` still builds its durable-id→mount map from Observe, + so a raw drive's guest-BIND is not re-asserted after a full host reboot (the drive still auto-mounts via + its `.mount` unit + is watchdog-tracked; only the in-guest bind re-assert is missed). Same Observe- + dependency class as the Impl-2a watchdog decoupling — to be extended to the guest-bind reconcile. + ## v0.55.0 — raw-device discovery + registry-sourced drive tracking (Impl-2a) (2026-07-01) Agent backend for drive enrollment (SPIKE-drive-enrollment §SQ1/SQ4/SQ5). Makes raw (non-PVE-storage) diff --git a/cmd/felhom-agent/main.go b/cmd/felhom-agent/main.go index 0f6b845..819ee3b 100644 --- a/cmd/felhom-agent/main.go +++ b/cmd/felhom-agent/main.go @@ -45,7 +45,7 @@ import ( // version is the agent version. Overridable at build time with // -ldflags "-X main.version="; defaults to the in-repo CHANGELOG version. -var version = "0.55.0" +var version = "0.56.0" // runGuestHook is the PVE pre-start hook body (`felhom-agent guest-hook `). On the // pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots diff --git a/internal/localapi/disks.go b/internal/localapi/disks.go index 10ad8d9..e9f6ca2 100644 --- a/internal/localapi/disks.go +++ b/internal/localapi/disks.go @@ -883,13 +883,28 @@ func (s *Server) ReassertGuestBinds(ctx context.Context) { // durableIDForMount resolves the durable-id of the storage mounted at `where` (from the agent's own // storage view) — the key the intent store records enroll/eject against. "" if not resolvable. func (s *Server) durableIDForMount(ctx context.Context, where string) string { - targets, err := s.storage.Observe(ctx) - if err != nil { - return "" + // Preferred: the storage view (PVE storages) already carries the derived durable-id. + if targets, err := s.storage.Observe(ctx); err == nil { + for _, t := range targets { + if t.MountPath == where { + return t.DurableID + } + } } - for _, t := range targets { - if t.MountPath == where { - return t.DurableID + // Fallback (Impl-2b): a RAW enrolled drive is NOT a PVE storage, so it never appears in Observe — + // which left its enroll/eject intent + guest-bind unrecorded ("durable-id unresolved"). Resolve the + // fs-UUID directly from the mount table: the device mounted at `where` → its by-uuid identity. This + // is the SAME scheme Observe derives ("uuid:"), so intent keys stay consistent. + if s.host != nil { + if mounts, err := s.host.Mounts(); err == nil { + for _, m := range mounts { + if m.MountPoint == where { + if uuid, ok := s.host.ResolveUUID(m.Device); ok && uuid != "" { + return "uuid:" + uuid + } + break + } + } } } return "" diff --git a/internal/localapi/durableid_rawmount_test.go b/internal/localapi/durableid_rawmount_test.go new file mode 100644 index 0000000..34d35b1 --- /dev/null +++ b/internal/localapi/durableid_rawmount_test.go @@ -0,0 +1,43 @@ +package localapi + +import ( + "context" + "testing" + + "gitea.dooplex.hu/admin/felhom-agent/internal/storage" +) + +// uuidHostReader is a HostReader that resolves specific device fs-UUIDs (fakeHostReader always fails). +type uuidHostReader struct { + mounts []storage.Mount + uuids map[string]string +} + +func (u uuidHostReader) Mounts() ([]storage.Mount, error) { return u.mounts, nil } +func (u uuidHostReader) ResolveUUID(dev string) (string, bool) { + v, ok := u.uuids[dev] + return v, ok +} +func (u uuidHostReader) DeviceExists(string) bool { return true } +func (u uuidHostReader) Rotational(string) (bool, bool) { return false, false } +func (u uuidHostReader) Removable(string) (bool, bool) { return false, false } + +// Impl-2b: a RAW enrolled drive is not a PVE storage (absent from Observe), so its durable-id must be +// resolved from the mount table's device fs-UUID. RED-PROOF: with the Observe-only resolution (before +// the host fallback) this returns "" → intent + guest-bind go unrecorded. +func TestDurableIDForMount_RawFallback(t *testing.T) { + s := &Server{ + storage: fakeStorage{}, // Observe empty — the raw drive isn't a PVE storage + host: uuidHostReader{ + mounts: []storage.Mount{{Device: "/dev/sdd", MountPoint: "/mnt/teszt"}}, + uuids: map[string]string{"/dev/sdd": "RAW-UUID"}, + }, + } + if got := s.durableIDForMount(context.Background(), "/mnt/teszt"); got != "uuid:RAW-UUID" { + t.Fatalf("raw mount durable-id: got %q want uuid:RAW-UUID", got) + } + // An unknown mount still resolves to "" (no false positive). + if got := s.durableIDForMount(context.Background(), "/mnt/nope"); got != "" { + t.Fatalf("unknown mount: got %q want empty", got) + } +}