F-REBOOT + F-LEAK: the agent's authority over guest lifecycle (v0.107.0)

F-REBOOT — a guest rebooted mid-backup never came back (fault 11: 9m47s of total
appliance outage, no lock, nothing retrying). The existing stale-lock recovery is
correct but missed it two ways: its predicate needs a stale vzdump lock and that
guest was unlocked, and it runs only at agent startup. New periodic guest-power
watchdog acts on 'should be running, is not, is not locked'.

onboot is the should-be-running signal, not invented here: stalelock.go already
uses it for this same decision, it is 0 on scratch/golden, and pve-guests uses it
at host boot. Guards: onboot:0 never touched (Scenario B), a locked guest is left
to the stale-lock path, a guest with a vzdump in flight is left stopped,
unprovable ownership acts on nothing, unconfirmable backup state fails safe.
Bounded retry 3x at 1/2/4m then ERROR (Scenario C) — a healthy start takes ~25s.

F-LEAK — a failed restore-test could not destroy its scratch (403 VM.Allocate).
It is pool membership, not privsep: VM.Allocate is granted at /pool/felhom only,
and a failed restore never completes the --pool association. Fix needs NO new
grant — Pool.Allocate is already held, so the teardown adopts the stranded
scratch into the pool and retries the destroy. Guarded by scratchAdoptAllowed:
scratch provenance AND the numeric band, both required (Scenario E).

Six red-proofs across both fixes, all observed failing.
This commit is contained in:
2026-07-28 10:27:07 +02:00
parent a18b18e5de
commit 367a503a0f
7 changed files with 691 additions and 5 deletions
+6
View File
@@ -1057,6 +1057,12 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
// 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)
// F-REBOOT: the startup recovery above only covers a guest left LOCKED by an interrupted
// backup. A guest that simply ends up stopped-and-unlocked (a `pct reboot` whose shutdown
// half completed and whose start half never fired — Campaign 8 fault 11, 9m47s of total
// appliance outage with nothing retrying) needs a PERIODIC check. onboot is the "should be
// running" signal, so a deliberately stopped guest is never touched.
go localSrv.WatchGuestPower(ctx)
go func() { errc <- localSrv.Run(ctx) }()
}
if lanLoop != nil {