agent v0.37.0: host-reboot remount re-resolves enrolled drives by fs-UUID

TASK A — close out the reboot story (agent half). Root cause (pinned live on
felhom-pve): an enrolled .mount unit left `disabled` by a prior detach never
auto-mounts at boot, and kernel re-enumeration can move a drive's node
(/dev/sdb->sdc). Fix re-asserts every enrolled mount by filesystem UUID at
startup + on the periodic tick.

- ResolveStorageDevice: resolve uuid:<fs-uuid> -> current /dev node via
  /dev/disk/by-uuid (never a cached node); errors if absent.
- parseFelhomMountUnit: pure inverse of renderMountUnit (marker-gated).
- (*SudoHostOps).ReassertEnrolledMounts: re-run EnsureMount (enable --now) for
  any enrolled unit not in /proc/mounts; idempotent, skips mounted/absent.
- main.go: runs before ReassertGuestBinds at startup + on the 20s tick.
- tests (Linux, seam=device resolution): letter-move tolerated (sdb->sdc) +
  red-proof companion, absent/scheme rejection, render->parse round-trip.

TASK A2 verdict: enrolling a NEW drive does NOT need an LXC restart — the path
lands on the live AttachDrive (shared parent, named live slots), not RebootGuest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 17:49:00 +02:00
parent 437f096d9d
commit a621f4c5a0
8 changed files with 305 additions and 40 deletions
+15 -1
View File
@@ -44,7 +44,7 @@ import (
// version is the agent version. Overridable at build time with
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
var version = "0.36.7"
var version = "0.37.0"
// 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
@@ -506,6 +506,15 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
go func() { errc <- pbsLoop.Run(ctx) }()
if localSrv != nil {
localServers = 1
// Host-reboot remount fix: BEFORE binding into the guest, re-assert enrolled drive MOUNTS on the
// host. A `disabled` mount unit (left so by a prior detach) doesn't auto-mount at boot, and kernel
// re-enumeration can move the device (/dev/sdb→sdc); ReassertEnrolledMounts re-resolves each by
// filesystem UUID and re-mounts (idempotent `enable --now`) so a letter reshuffle is a no-op.
// Type-asserted (the concrete op exposes it; the interface stays lean).
mountReasserter, _ := hostOps.(*storage.SudoHostOps)
if mountReasserter != nil {
mountReasserter.ReassertEnrolledMounts(ctx)
}
// F9: on startup (the host's bring-up/reconcile trigger), re-assert any enrolled guest data-drive
// bind that a re-provision dropped — before serving, so the drive is back in the guest config
// (activates on the guest's next reboot). On-durable-id-match; absent/swapped drives are skipped.
@@ -523,6 +532,11 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
case <-ctx.Done():
return
case <-t.C:
// Re-assert mounts first (handles a USB that enumerated late after a host reboot, or a
// unit re-disabled at runtime), then re-assert the guest binds against the now-live mounts.
if mountReasserter != nil {
mountReasserter.ReassertEnrolledMounts(ctx)
}
localSrv.ReassertGuestBinds(ctx)
}
}