v0.56.0: resolve a raw enrolled drive's durable-id from the mount table
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:<fs-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) <noreply@anthropic.com>
This commit is contained in:
@@ -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:<fs-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)
|
## 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)
|
Agent backend for drive enrollment (SPIKE-drive-enrollment §SQ1/SQ4/SQ5). Makes raw (non-PVE-storage)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import (
|
|||||||
|
|
||||||
// version is the agent version. Overridable at build time with
|
// version is the agent version. Overridable at build time with
|
||||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
// -ldflags "-X main.version=<v>"; 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 <vmid> <phase>`). On the
|
// runGuestHook is the PVE pre-start hook body (`felhom-agent guest-hook <vmid> <phase>`). On the
|
||||||
// pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots
|
// pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// storage view) — the key the intent store records enroll/eject against. "" if not resolvable.
|
||||||
func (s *Server) durableIDForMount(ctx context.Context, where string) string {
|
func (s *Server) durableIDForMount(ctx context.Context, where string) string {
|
||||||
targets, err := s.storage.Observe(ctx)
|
// Preferred: the storage view (PVE storages) already carries the derived durable-id.
|
||||||
if err != nil {
|
if targets, err := s.storage.Observe(ctx); err == nil {
|
||||||
return ""
|
for _, t := range targets {
|
||||||
|
if t.MountPath == where {
|
||||||
|
return t.DurableID
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, t := range targets {
|
// Fallback (Impl-2b): a RAW enrolled drive is NOT a PVE storage, so it never appears in Observe —
|
||||||
if t.MountPath == where {
|
// which left its enroll/eject intent + guest-bind unrecorded ("durable-id unresolved"). Resolve the
|
||||||
return t.DurableID
|
// fs-UUID directly from the mount table: the device mounted at `where` → its by-uuid identity. This
|
||||||
|
// is the SAME scheme Observe derives ("uuid:<fs-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 ""
|
return ""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user