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:
@@ -54,6 +54,24 @@ func (c *Client) GuestConfig(ctx context.Context, vmid int) (GuestConfig, error)
|
||||
return cfg, c.get(ctx, path, &cfg)
|
||||
}
|
||||
|
||||
// ListSnapshots returns GET /nodes/{node}/lxc/{vmid}/snapshot (the guest's snapshots, including the
|
||||
// synthetic "current"). The startup stale-lock recovery uses it to find a dangling "vzdump" snapshot
|
||||
// left by an interrupted snapshot-mode backup.
|
||||
func (c *Client) ListSnapshots(ctx context.Context, vmid int) ([]Snapshot, error) {
|
||||
var ss []Snapshot
|
||||
path := fmt.Sprintf("/nodes/%s/lxc/%d/snapshot", c.node, vmid)
|
||||
return ss, c.get(ctx, path, &ss)
|
||||
}
|
||||
|
||||
// ListRunningTasks returns the node's currently-running tasks (GET /nodes/{node}/tasks?running=1).
|
||||
// The startup stale-lock recovery uses it as the load-bearing safety guard: a backup lock is cleared
|
||||
// ONLY when no vzdump task is genuinely in-flight for the guest (an agent restart while a real backup
|
||||
// runs must never clear the live lock).
|
||||
func (c *Client) ListRunningTasks(ctx context.Context) ([]TaskStatus, error) {
|
||||
var ts []TaskStatus
|
||||
return ts, c.get(ctx, "/nodes/"+c.node+"/tasks?running=1", &ts)
|
||||
}
|
||||
|
||||
// ListStorage returns GET /storage (cluster-wide storage definitions).
|
||||
func (c *Client) ListStorage(ctx context.Context) ([]Storage, error) {
|
||||
var ss []Storage
|
||||
|
||||
@@ -118,6 +118,35 @@ func (g *GuestConfig) Nets() map[string]string {
|
||||
return g.prefixed("net")
|
||||
}
|
||||
|
||||
// Lock returns the guest's current lock ("backup", "snapshot-delete", "migrate", …) from the config,
|
||||
// or "" when unlocked. An interrupted vzdump leaves a "backup" or "snapshot-delete" lock — the signal
|
||||
// the startup stale-lock recovery (F2-b) keys on.
|
||||
func (g *GuestConfig) Lock() string {
|
||||
raw, ok := g.Extra["lock"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if json.Unmarshal(raw, &s) != nil {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// OnBoot reports whether the guest is configured to auto-start at host boot (onboot:1). PVE omits the
|
||||
// key when 0, so an absent key reads as false.
|
||||
func (g *GuestConfig) OnBoot() bool {
|
||||
raw, ok := g.Extra["onboot"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
var n int
|
||||
if json.Unmarshal(raw, &n) != nil {
|
||||
return false
|
||||
}
|
||||
return n == 1
|
||||
}
|
||||
|
||||
func (g *GuestConfig) prefixed(prefix string) map[string]string {
|
||||
out := map[string]string{}
|
||||
for k, raw := range g.Extra {
|
||||
@@ -136,6 +165,16 @@ func (g *GuestConfig) prefixed(prefix string) map[string]string {
|
||||
return out
|
||||
}
|
||||
|
||||
// Snapshot is one entry of GET /nodes/{node}/lxc/{vmid}/snapshot. The list always includes the
|
||||
// synthetic "current" pseudo-snapshot; named entries are real snapshots. An interrupted vzdump
|
||||
// snapshot-mode backup leaves a dangling snapshot named exactly "vzdump".
|
||||
type Snapshot struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SnapTime int64 `json:"snaptime,omitempty"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
}
|
||||
|
||||
// Storage is one entry of GET /storage (cluster) and GET /nodes/{node}/storage
|
||||
// (the latter adds usage fields). Unused fields stay zero.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user