06e0bc9c25
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
100 lines
4.3 KiB
Go
100 lines
4.3 KiB
Go
package backup
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
|
)
|
|
|
|
// localTarget is a non-PBS dir storage; pbsTarget is a PBS storage — for the scope gate.
|
|
var (
|
|
localTargetStores = []proxmox.Storage{{Storage: "local", Type: "dir", Content: "backup"}}
|
|
pbsTargetStores = []proxmox.Storage{{Storage: "felhom-pbs", Type: "pbs", Content: "backup"}}
|
|
)
|
|
|
|
func okAPI(stores []proxmox.Storage) *fakeBackupAPI {
|
|
return &fakeBackupAPI{vzdumpUPID: "UPID:vzdump:1", storages: stores}
|
|
}
|
|
|
|
// TestPrune_LocalCarriesKeepLast: a LOCAL-target backup with retention carries `--prune-backups
|
|
// keep-last=3` on the vzdump. Companion: the SAME runner built with no retention ("") emits NO prune
|
|
// option — proving the flag only rides when retention is set (a no-prune build accumulates).
|
|
func TestPrune_LocalCarriesKeepLast(t *testing.T) {
|
|
api := okAPI(localTargetStores)
|
|
r := NewBackupRunner(api, "local", proxmox.ModeStop, "", "keep-last=3", quiet())
|
|
_, _ = r.Backup(context.Background(), 9201) // archive-resolution may fail in the fake; we assert the captured vzdump opts
|
|
if len(api.vzdumps) != 1 || api.vzdumps[0].PruneBackups != "keep-last=3" {
|
|
t.Fatalf("local backup must carry prune-backups keep-last=3, got %q", api.vzdumps[0].PruneBackups)
|
|
}
|
|
|
|
// COMPANION: no retention → no prune option (dumps would accumulate).
|
|
api2 := okAPI(localTargetStores)
|
|
r2 := NewBackupRunner(api2, "local", proxmox.ModeStop, "", "", quiet())
|
|
_, _ = r2.Backup(context.Background(), 9201)
|
|
if api2.vzdumps[0].PruneBackups != "" {
|
|
t.Fatalf("a no-retention runner must NOT prune, got %q", api2.vzdumps[0].PruneBackups)
|
|
}
|
|
}
|
|
|
|
// TestPrune_NeverPrunesPBS is the scope rule (§9): retention is NOT applied when the target is a PBS
|
|
// storage (offsite retention is a separate lifecycle). Companion: the identical retention on a LOCAL
|
|
// target IS applied — proving the gate keys on storage type, not luck.
|
|
func TestPrune_NeverPrunesPBS(t *testing.T) {
|
|
api := okAPI(pbsTargetStores)
|
|
r := NewBackupRunner(api, "felhom-pbs", proxmox.ModeStop, "", "keep-last=3", quiet())
|
|
_, _ = r.Backup(context.Background(), 9201)
|
|
if api.vzdumps[0].PruneBackups != "" {
|
|
t.Fatalf("a PBS target must NEVER be pruned by the per-run flag, got %q", api.vzdumps[0].PruneBackups)
|
|
}
|
|
|
|
// COMPANION: same retention, local target → applied.
|
|
api2 := okAPI(localTargetStores)
|
|
r2 := NewBackupRunner(api2, "local", proxmox.ModeStop, "", "keep-last=3", quiet())
|
|
_, _ = r2.Backup(context.Background(), 9201)
|
|
if api2.vzdumps[0].PruneBackups != "keep-last=3" {
|
|
t.Fatalf("control: a local target with the same retention MUST be pruned, got %q", api2.vzdumps[0].PruneBackups)
|
|
}
|
|
}
|
|
|
|
// TestPrune_FailSafeOnUnknownTarget: if the target's type can't be confirmed (lookup error / not in the
|
|
// list), the run SKIPS pruning rather than risk pruning a PBS/unknown storage.
|
|
func TestPrune_FailSafeOnUnknownTarget(t *testing.T) {
|
|
// target not present in the list → skip.
|
|
api := &fakeBackupAPI{vzdumpUPID: "UPID:vzdump:1", storages: localTargetStores}
|
|
r := NewBackupRunner(api, "some-other-store", proxmox.ModeStop, "", "keep-last=3", quiet())
|
|
_, _ = r.Backup(context.Background(), 9201)
|
|
if api.vzdumps[0].PruneBackups != "" {
|
|
t.Fatalf("an unknown target must skip pruning (fail-safe), got %q", api.vzdumps[0].PruneBackups)
|
|
}
|
|
}
|
|
|
|
// TestPrune_KeepLastClamp (§7-B): a 0/negative/unset LocalBackupRetention clamps to ≥1 (default 3) so the
|
|
// vzdump NEVER prunes the archive it just made. Companion: a no-clamp impl that returns 0 would emit
|
|
// keep-last=0 → PVE prunes everything → FAILS the "≥1" assertion.
|
|
func TestPrune_KeepLastClamp(t *testing.T) {
|
|
cases := []struct {
|
|
set int
|
|
want int
|
|
}{
|
|
{0, 3}, // unset → default
|
|
{-5, 3}, // negative → default
|
|
{1, 1}, // honored
|
|
{3, 3}, // honored
|
|
{10, 10}, // honored
|
|
}
|
|
for _, c := range cases {
|
|
b := config.BackupConfig{LocalBackupRetention: c.set}
|
|
if got := b.KeepLast(); got != c.want {
|
|
t.Errorf("KeepLast(%d) = %d, want %d", c.set, got, c.want)
|
|
}
|
|
if b.KeepLast() < 1 {
|
|
t.Fatalf("keep-last must NEVER be < 1 (would prune the fresh backup), got %d for %d", b.KeepLast(), c.set)
|
|
}
|
|
}
|
|
if spec := (config.BackupConfig{}).PruneBackupsSpec(); spec != "keep-last=3" {
|
|
t.Fatalf("default PruneBackupsSpec = %q, want keep-last=3", spec)
|
|
}
|
|
}
|