agent v0.49.0: reboot-during-backup stale-lock recovery (F2-b) + shared-parent script redeploy fix (F2-a)

F2-b: at startup, recover a guest left with a stale vzdump lock by a
reboot-during-backup — pct unlock -> delete dangling vzdump snapshot ->
start iff onboot, guarded by a no-vzdump-running invariant (fail-safe).
New internal/localapi/stalelock.go; proxmox GuestConfig.Lock()/OnBoot(),
ListSnapshots, ListRunningTasks, Snapshot type. New narrow sudoers grant
FELHOM_STALELOCK (pct unlock) + Critical capability stalelock-unlock.

F2-a: EnsureSharedParent only redeployed the boot script when the UNIT
differed, so the v0.36.6 make-private fix never reached hosts whose unit
was current -> /mnt/felhom-drives stayed in root's shared:1 and doubled
every drive bind. New sharedParentInstallStale compares BOTH script and
unit. Boot-time-only; never churns the live mount.

Both root causes confirmed live on felhom-pve before fixing. Green gate
(build/vet/test) all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BnMpUXscPsUB1cU8Tr6K
This commit is contained in:
2026-06-30 09:00:32 +02:00
parent 81954c3d2a
commit 6e38e2f921
12 changed files with 743 additions and 32 deletions
+8 -1
View File
@@ -45,7 +45,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.48.0"
var version = "0.49.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
@@ -581,6 +581,10 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
}()
// F20-BUG3: complete a disk format that an agent restart interrupted (durable-id-bound, re-resolved).
localSrv.RecoverFormatJob(ctx)
// F2-b: recover any guest left with a stale vzdump lock by a reboot-during-backup (unlock → delete
// the dangling snapshot → start iff onboot). Runs BEFORE the backup loop starts, so a present
// backup lock is stale by definition (guarded by a no-vzdump-running check; fail-safe otherwise).
localSrv.RecoverStaleLockedGuests(ctx)
go func() { errc <- localSrv.Run(ctx) }()
}
if lanLoop != nil {
@@ -748,6 +752,9 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
Guests2: px,
GuestAttach: guestBinder, // slice 10 P2: bind enrolled data drives into the guest
ControllerSwap: guestBinder, // Phase 1: agentic controller update — in-guest image swap
// F2-b: recover a guest left with a stale vzdump lock by a reboot-during-backup. Reads + start
// go through the API client; the `pct unlock` is the one fenced root-CLI op (no API equivalent).
StaleLock: localapi.NewStaleLockController(px, &proxmox.ExecRunner{Mode: gaMode, SudoPath: cfg.Privileged.SudoPath}),
Intent: intent, // slice 10 P3: record enroll/eject intent for self-heal
GuestBinds: guestBinds, // F9: per-guest bind record for the startup re-assert
FormatJobs: formatJobs, // F20-BUG3: detached-format job record + restart recovery