agent v0.51.0: local vzdump retention default (--prune-backups keep-last=3)
The preventive counterpart to host_disk + storage_fill detectors: the periodic local whole-guest vzdump now prunes its own old archives (keep-last=3, clamped >=1) so a box can't refill its own root via its own backups. Local target only — PBS never pruned (resolved via ListStorage; fail-safe skip on unknown). Seeded in host-install. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
@@ -19,6 +19,8 @@ type BackupAPI interface {
|
||||
WaitTask(ctx context.Context, upid string, opts proxmox.WaitOptions) (proxmox.TaskStatus, error)
|
||||
GuestConfig(ctx context.Context, vmid int) (proxmox.GuestConfig, error)
|
||||
StorageContent(ctx context.Context, store string) ([]proxmox.StorageContent, error)
|
||||
// ListStorage enumerates storages (name+type) — used to scope local-only retention (never prune PBS).
|
||||
ListStorage(ctx context.Context) ([]proxmox.Storage, error)
|
||||
// TaskLogTail reads trailing task-log lines — used to read the ACTUAL vzdump mode
|
||||
// (PVE may downgrade a requested snapshot to stop for a stopped guest — spike B1).
|
||||
TaskLogTail(ctx context.Context, upid string, limit int) ([]string, error)
|
||||
@@ -32,20 +34,51 @@ type BackupRunner struct {
|
||||
target string // backup storage (content=backup)
|
||||
mode proxmox.BackupMode // default ModeSnapshot
|
||||
notes string // optional notes-template
|
||||
logger *slog.Logger
|
||||
now func() time.Time
|
||||
// retention is the per-run `--prune-backups` spec (e.g. "keep-last=3") applied to a LOCAL target after
|
||||
// each successful backup, so the agent's own backups can't pile up and refill root. Empty → no prune
|
||||
// (the legacy behaviour; restore-test/selftest runners pass ""). NEVER applied to a PBS target.
|
||||
retention string
|
||||
logger *slog.Logger
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewBackupRunner builds a runner. mode defaults to snapshot (works for a stopped guest and
|
||||
// for lvm-thin); the caller may pass ModeStop for storages without snapshot support.
|
||||
func NewBackupRunner(api BackupAPI, target string, mode proxmox.BackupMode, notes string, logger *slog.Logger) *BackupRunner {
|
||||
// for lvm-thin); the caller may pass ModeStop for storages without snapshot support. retention is the
|
||||
// per-run prune spec ("keep-last=N", or "" to never prune) — only the periodic local backup sets it.
|
||||
func NewBackupRunner(api BackupAPI, target string, mode proxmox.BackupMode, notes, retention string, logger *slog.Logger) *BackupRunner {
|
||||
if mode == "" {
|
||||
mode = proxmox.ModeSnapshot
|
||||
}
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &BackupRunner{api: api, target: target, mode: mode, notes: notes, logger: logger, now: func() time.Time { return time.Now().UTC() }}
|
||||
return &BackupRunner{api: api, target: target, mode: mode, notes: notes, retention: retention, logger: logger, now: func() time.Time { return time.Now().UTC() }}
|
||||
}
|
||||
|
||||
// localPruneSpec returns the `--prune-backups` spec to apply to THIS backup, or "" to skip pruning. It
|
||||
// applies the configured retention ONLY when the target is a non-PBS storage — PBS offsite retention is a
|
||||
// separate lifecycle and must never be pruned by the per-run flag (§9). Fail-safe: if the target's type
|
||||
// can't be confirmed (lookup error / not found), it SKIPS pruning rather than risk pruning PBS — the
|
||||
// host_disk + storage_fill detectors remain the safety net.
|
||||
func (r *BackupRunner) localPruneSpec(ctx context.Context) string {
|
||||
if r.retention == "" {
|
||||
return ""
|
||||
}
|
||||
stores, err := r.api.ListStorage(ctx)
|
||||
if err != nil {
|
||||
r.logger.Warn("backup: could not resolve target storage type — skipping local prune this run", "target", r.target, "err", err)
|
||||
return ""
|
||||
}
|
||||
for _, s := range stores {
|
||||
if s.Storage == r.target {
|
||||
if s.Type == "pbs" {
|
||||
return "" // PBS retention is out of scope — never prune the offsite DR
|
||||
}
|
||||
return r.retention
|
||||
}
|
||||
}
|
||||
r.logger.Warn("backup: target storage not found in list — skipping local prune this run (fail-safe)", "target", r.target)
|
||||
return ""
|
||||
}
|
||||
|
||||
// snapshotMarker is the vzdump task-log line that signals the storage snapshot has been created
|
||||
@@ -99,6 +132,7 @@ func (r *BackupRunner) backup(ctx context.Context, vmid int, onSnapshot func())
|
||||
|
||||
upid, err := r.api.Vzdump(ctx, proxmox.VzdumpOptions{
|
||||
VMID: vmid, Storage: r.target, Mode: r.mode, Notes: r.notes,
|
||||
PruneBackups: r.localPruneSpec(ctx), // local target → keep-last=N; PBS/unknown → "" (no prune)
|
||||
})
|
||||
if err != nil {
|
||||
rec.Error = err.Error()
|
||||
|
||||
Reference in New Issue
Block a user