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:
@@ -114,17 +114,35 @@ func (b *GuestBinder) EnsureSharedParent(ctx context.Context) error {
|
||||
return fmt.Errorf("shared-parent: make-shared: %w", err)
|
||||
}
|
||||
}
|
||||
// Install the boot-persistence unit only when missing OR its content differs from what we ship (so a
|
||||
// unit-template fix deploys) — EnsureSharedParent runs on a periodic reconcile, and re-writing files +
|
||||
// daemon-reload every tick would be wasteful, so the common case (unchanged) is a cheap read.
|
||||
if cur, err := os.ReadFile(sharedParentUnitPath); err != nil || string(cur) != sharedParentUnit {
|
||||
// (Re)install the boot-persistence files only when the on-disk SCRIPT or UNIT differs from what we
|
||||
// ship (or is missing) — EnsureSharedParent runs on a periodic reconcile, so re-writing files +
|
||||
// daemon-reload every tick would be wasteful; the common case (both current) is two cheap reads.
|
||||
//
|
||||
// F2-a: this MUST compare the SCRIPT too, not just the unit. The v0.36.6 make-private fix changed
|
||||
// only the script (the unit was unchanged), so the earlier unit-only gate never redeployed it —
|
||||
// leaving hosts running the pre-fix script (no make-private), whose self-bind stays in root's shared
|
||||
// peer group and DOUBLES every drive bind. Comparing both files closes that deploy gap.
|
||||
if sharedParentInstallStale(sharedParentUnitPath, sharedParentScriptPath) {
|
||||
if ierr := b.installSharedParentUnit(ctx); ierr != nil {
|
||||
b.logger.Warn("shared-parent: boot-persistence unit install failed (live setup OK; survives until host reboot)", "err", ierr)
|
||||
b.logger.Warn("shared-parent: boot-persistence (re)install failed (live setup OK; survives until host reboot)", "err", ierr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sharedParentInstallStale reports whether the on-disk boot script OR unit is missing or differs from
|
||||
// what this build ships — the trigger to (re)install both. Comparing BOTH (not the unit alone) is the
|
||||
// F2-a fix: a script-only change must still redeploy. Pure (path args) so it is unit-testable.
|
||||
func sharedParentInstallStale(unitPath, scriptPath string) bool {
|
||||
if cur, err := os.ReadFile(unitPath); err != nil || string(cur) != sharedParentUnit {
|
||||
return true
|
||||
}
|
||||
if cur, err := os.ReadFile(scriptPath); err != nil || string(cur) != sharedParentScript {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// installSharedParentUnit writes the script + unit (from agent-written temps) and enables the unit so the
|
||||
// shared parent is re-established on every host boot before pve-guests. Idempotent.
|
||||
func (b *GuestBinder) installSharedParentUnit(ctx context.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user