3ef095fb71
gates / gates (push) Successful in 14s
PVE answers 403 permission denied, not "does not exist", for a vmid outside the felhom pool; v0.136.0 turned that into a lookup failure and the local tier read UNKNOWN every evaluation (measured on demo-hp). Such an archive is skipped. Red-proofed; verified read-only on demo-hp with the pre-release binary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
113 lines
6.3 KiB
Go
113 lines
6.3 KiB
Go
package backup
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
|
)
|
|
|
|
// R-689 (v0.135.0) — demo-hp keeps its golden template in `local:backup/`. It is content "backup",
|
|
// 654 MB and so "plausibly complete", and it was the newest SETTLED entry: the restore test picked it
|
|
// every 6 h and failed extractconfig with a 403 (measured 2026-09-24 10:36, 09-25 04:57 and 10:57),
|
|
// while the guest's own archive — younger than the 24 h settle — went untested and nothing was proven.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): drop the guestBackupArchive call from PickSettledRestoreCandidateOn —
|
|
// this test then picks `local:backup/felhom-golden-0.236.0.tar.zst`.
|
|
func TestR689_TheRestoreTestNeverPicksTheGolden(t *testing.T) {
|
|
const day = int64(86400)
|
|
now := int64(1790370000) // 2026-09-25 ~19:00Z
|
|
api := &fakeBackupAPI{content: []proxmox.StorageContent{
|
|
// the guest's real archive, settled (older than the cutoff below)
|
|
{VolID: "local:backup/vzdump-lxc-9201-2026_09_22-21_59_25.tar.zst", Content: "backup", VMID: 9201, Size: 8 << 30, CTime: now - 3*day},
|
|
// the golden: newer, settled, big, and NOT a backup of a guest
|
|
{VolID: "local:backup/felhom-golden-0.236.0.tar.zst", Content: "backup", Size: 654115664, CTime: now - 2*day},
|
|
// a hand-copied tarball that PVE happens to attribute to a vmid — the name is not a vzdump's
|
|
{VolID: "local:backup/copy-of-9201.tar.zst", Content: "backup", VMID: 9201, Size: 8 << 30, CTime: now - 2*day},
|
|
}}
|
|
r := NewBackupRunner(api, "local", proxmox.ModeSnapshot, "", "keep-last=1", quiet())
|
|
got, _, err := r.PickSettledRestoreCandidateOn(context.Background(), "local", time.Unix(now-day, 0).UTC())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "local:backup/vzdump-lxc-9201-2026_09_22-21_59_25.tar.zst" {
|
|
t.Fatalf("picked %q — the restore test must prove a backup OF A GUEST", got)
|
|
}
|
|
}
|
|
|
|
func TestR689_GuestBackupArchiveShapes(t *testing.T) {
|
|
for _, c := range []struct {
|
|
e proxmox.StorageContent
|
|
ok bool
|
|
}{
|
|
{proxmox.StorageContent{VolID: "local:backup/vzdump-lxc-9201-2026_09_24-21_59_25.tar.zst", VMID: 9201}, true},
|
|
{proxmox.StorageContent{VolID: "local:backup/vzdump-qemu-300-2026_09_24-21_59_25.vma.zst", VMID: 300}, true},
|
|
{proxmox.StorageContent{VolID: "felhom-pbs:backup/ct/9201/2026-07-28T05:31:14Z", VMID: 9201}, true},
|
|
{proxmox.StorageContent{VolID: "felhom-pbs:backup/vm/300/2026-07-28T05:31:14Z", VMID: 300}, true},
|
|
{proxmox.StorageContent{VolID: "local:backup/felhom-golden-0.236.0.tar.zst"}, false},
|
|
{proxmox.StorageContent{VolID: "local:backup/vzdump-lxc-9201-x.tar.zst", VMID: 9202}, false}, // vmid disagrees with the name
|
|
{proxmox.StorageContent{VolID: "felhom-pbs:backup/ct/9201/2026-07-28T05:31:14Z"}, false}, // no vmid reported
|
|
} {
|
|
if ok, why := guestBackupArchive(c.e); ok != c.ok {
|
|
t.Errorf("%s vmid=%d: ok=%v (%s), want %v", c.e.VolID, c.e.VMID, ok, why, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// R-689 (v0.136.0) — the measured demo-hp shape right after v0.135.0: the golden (skipped), a leftover archive
|
|
// of guest 9100 deleted in August (settled), and today's archive of 9201 (not settled yet). The pick must be
|
|
// NOTHING — never the deleted guest's archive. With 9201's archive settled, that one.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): drop the known-guest check — the pick is the 9100 leftover.
|
|
func TestR689_AnArchiveOfADeletedGuestIsNeverPicked(t *testing.T) {
|
|
const day = int64(86400)
|
|
now := int64(1790476000)
|
|
api := &fakeBackupAPI{goneGuests: map[int]bool{9100: true}, content: []proxmox.StorageContent{
|
|
{VolID: "local:backup/felhom-golden-0.236.0.tar.zst", Content: "backup", Size: 654115664, CTime: now - 14*day},
|
|
{VolID: "local:backup/vzdump-lxc-9100-2026_08_21-17_59_15.tar.zst", Content: "backup", VMID: 9100, Size: 656970239, CTime: now - 37*day},
|
|
{VolID: "local:backup/vzdump-lxc-9201-2026_09_27-04_35_47.tar.zst", Content: "backup", VMID: 9201, Size: 8 << 30, CTime: now - 7*3600},
|
|
}}
|
|
r := NewBackupRunner(api, "local", proxmox.ModeSnapshot, "", "keep-last=1", quiet())
|
|
got, _, err := r.PickSettledRestoreCandidateOn(context.Background(), "local", time.Unix(now-day, 0).UTC())
|
|
if err != nil || got != "" {
|
|
t.Fatalf("picked %q err=%v — a deleted guest's archive proves nothing about this box", got, err)
|
|
}
|
|
got, _, _ = r.PickSettledRestoreCandidateOn(context.Background(), "local", time.Unix(now, 0).UTC())
|
|
if got != "local:backup/vzdump-lxc-9201-2026_09_27-04_35_47.tar.zst" {
|
|
t.Fatalf("with 9201's archive settled the pick is %q", got)
|
|
}
|
|
}
|
|
|
|
// Any OTHER lookup failure is not "the guest is gone": the tier must read UNKNOWN (an error), never
|
|
// "nothing to prove".
|
|
func TestR689_AGuestLookupFailureIsUnknownNotEmpty(t *testing.T) {
|
|
api := &fakeBackupAPI{cfgErr: fmt.Errorf("proxmox: connection refused"), content: []proxmox.StorageContent{
|
|
{VolID: "local:backup/vzdump-lxc-9201-x.tar.zst", Content: "backup", VMID: 9201, Size: 8 << 30, CTime: 10},
|
|
}}
|
|
r := NewBackupRunner(api, "local", proxmox.ModeSnapshot, "", "keep-last=1", quiet())
|
|
if _, _, err := r.PickSettledRestoreCandidateOn(context.Background(), "local", time.Time{}); err == nil {
|
|
t.Fatal("a failed guest lookup read as a clean answer")
|
|
}
|
|
}
|
|
|
|
// v0.137.0 — THE MEASURED ANSWER: the agent's token sees only its pool, so for the deleted guest PVE says 403
|
|
// "permission denied at /vms/9100", not "does not exist" (demo-hp, right after v0.136.0 — the local tier read
|
|
// UNKNOWN). Such a guest is not one this agent manages: its archive is skipped, the tier is not an error.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): drop the "permission denied" case — the pick errors.
|
|
func TestR689_AGuestOutsideTheAgentsACLIsNotAKnownGuest(t *testing.T) {
|
|
const day = int64(86400)
|
|
now := int64(1790476000)
|
|
api := &fakeBackupAPI{aclGuests: map[int]bool{9100: true}, content: []proxmox.StorageContent{
|
|
{VolID: "local:backup/vzdump-lxc-9100-2026_08_21-17_59_15.tar.zst", Content: "backup", VMID: 9100, Size: 656970239, CTime: now - 37*day},
|
|
{VolID: "local:backup/vzdump-lxc-9201-2026_09_27-04_35_47.tar.zst", Content: "backup", VMID: 9201, Size: 8 << 30, CTime: now - 7*3600},
|
|
}}
|
|
r := NewBackupRunner(api, "local", proxmox.ModeSnapshot, "", "keep-last=1", quiet())
|
|
got, _, err := r.PickSettledRestoreCandidateOn(context.Background(), "local", time.Unix(now-day, 0).UTC())
|
|
if err != nil || got != "" {
|
|
t.Fatalf("picked %q err=%v — want nothing and no error (the only settled archive is not ours)", got, err)
|
|
}
|
|
}
|