Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd81866b16 | |||
| 3ef095fb71 | |||
| 7c986915ca |
@@ -1,3 +1,17 @@
|
|||||||
|
## v0.136.0 — … of a guest that still EXISTS (2026-09-27, R-689 second half)
|
||||||
|
|
||||||
|
> **RELEASED 2026-09-27** by `scripts/release-agent.sh` — tag `v0.136.0` (`16dbc83`), sha256 `2eb0b5ebe253defd68b322312bbac12418c051b0a7a0d2d1831310d97fa6d755`, verified by download. **NOT vouched** (the operator's act).
|
||||||
|
|
||||||
|
**MinAgent impact:** none required by any controller.
|
||||||
|
|
||||||
|
- **R-689, second half.** Read on demo-hp right after v0.135.0 with the read-only `-selftest=restore-test-due`: the
|
||||||
|
golden was skipped, and the pick fell to `vzdump-lxc-9100-2026_08_21…` — a leftover of a guest deleted in August. A
|
||||||
|
candidate's guest must now exist on this node (`GuestConfig`): "does not exist" skips the archive (INFO once per
|
||||||
|
volid); any other lookup failure is returned, so the tier reads UNKNOWN, never "nothing to prove". Tests
|
||||||
|
`TestR689_AnArchiveOfADeletedGuestIsNeverPicked` (red-proofed: without the check it picks the 9100 leftover),
|
||||||
|
`TestR689_AGuestLookupFailureIsUnknownNotEmpty`. A second agent release in one session — the first half was found
|
||||||
|
incomplete on the box.
|
||||||
|
|
||||||
## v0.135.0 — the restore test proves only backups OF A GUEST (2026-09-27, R-689)
|
## v0.135.0 — the restore test proves only backups OF A GUEST (2026-09-27, R-689)
|
||||||
|
|
||||||
> **RELEASED 2026-09-27** by `scripts/release-agent.sh` — tag `v0.135.0` (`d4be12c`), sha256 `ad4e75f16d338552f4588d3fe64c51cbf9651220b9d223b5386b85b6c37fd4c3`, verified by download. **NOT vouched** (the operator's act).
|
> **RELEASED 2026-09-27** by `scripts/release-agent.sh` — tag `v0.135.0` (`d4be12c`), sha256 `ad4e75f16d338552f4588d3fe64c51cbf9651220b9d223b5386b85b6c37fd4c3`, verified by download. **NOT vouched** (the operator's act).
|
||||||
|
|||||||
@@ -11,4 +11,14 @@ picker fixtures moved to real archive names.
|
|||||||
**Delivered** by signed `agent_update` (felhom-opsign, key `felhom-op-1`): demo-felhom committed 12:22:53 CEST, demo-hp
|
**Delivered** by signed `agent_update` (felhom-opsign, key `felhom-op-1`): demo-felhom committed 12:22:53 CEST, demo-hp
|
||||||
12:29:02 CEST (`felhom.eu/documentation/audits/version-travel-2026-09-26/D1/`).
|
12:29:02 CEST (`felhom.eu/documentation/audits/version-travel-2026-09-26/D1/`).
|
||||||
|
|
||||||
**NOT yet validated:** one scheduled restore-test cycle on demo-hp (due ~18:28 CEST, read into `D1/D1-cycle-demo-hp.txt`).
|
**Then v0.136.0 (`16dbc83`, sha256 `2eb0b5eb…fa6d755`, NOT vouched):** the scheduler's read-only verdict on demo-hp
|
||||||
|
(`-selftest=restore-test-due`, `D1/D1-selftest-due-demo-hp.txt`) skipped the golden but picked a leftover archive of
|
||||||
|
guest 9100 (deleted in August). The guest must now exist; red-proof `D1/RP-r689-deleted-guest.txt`. Signed-delivered to
|
||||||
|
both hosts. The verdict after 0.136.0: `D1/D1-selftest-due-after-0136.txt`.
|
||||||
|
|
||||||
|
**Then v0.137.0 (`3ef095f`):** 0.136.0's lookup met PVE's 403 "permission denied" (the token sees only its pool), not
|
||||||
|
"does not exist", so the local tier read UNKNOWN every evaluation (`D1/D1-selftest-due-after-0136.txt`) — a regression of
|
||||||
|
0.136.0, fixed: a guest the agent cannot read is not one it manages. Red-proof `D1/RP-r689-acl.txt`; verified read-only on
|
||||||
|
demo-hp with the pre-release binary before the release (`D1/D1-selftest-due-0137-pre.txt`): both leftovers skipped, the
|
||||||
|
off-site tier due on 9201, the local tier waiting for today's 9201 archive to settle. Three agent releases in one session
|
||||||
|
— each the smallest fix of what the box showed.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type fakeBackupAPI struct {
|
|||||||
waitErr error
|
waitErr error
|
||||||
cfg proxmox.GuestConfig
|
cfg proxmox.GuestConfig
|
||||||
goneGuests map[int]bool // R-689: vmids whose config lookup answers "does not exist"
|
goneGuests map[int]bool // R-689: vmids whose config lookup answers "does not exist"
|
||||||
|
aclGuests map[int]bool // R-689: vmids outside the token's ACL — PVE answers 403 "permission denied"
|
||||||
cfgErr error
|
cfgErr error
|
||||||
content []proxmox.StorageContent
|
content []proxmox.StorageContent
|
||||||
contentErr error
|
contentErr error
|
||||||
@@ -46,6 +47,9 @@ func (f *fakeBackupAPI) WaitTask(_ context.Context, _ string, _ proxmox.WaitOpti
|
|||||||
return proxmox.TaskStatus{Status: "stopped", ExitStatus: "OK"}, f.waitErr
|
return proxmox.TaskStatus{Status: "stopped", ExitStatus: "OK"}, f.waitErr
|
||||||
}
|
}
|
||||||
func (f *fakeBackupAPI) GuestConfig(_ context.Context, vmid int) (proxmox.GuestConfig, error) {
|
func (f *fakeBackupAPI) GuestConfig(_ context.Context, vmid int) (proxmox.GuestConfig, error) {
|
||||||
|
if f.aclGuests[vmid] {
|
||||||
|
return proxmox.GuestConfig{}, fmt.Errorf("proxmox: GET /nodes/n/lxc/%d/config -> HTTP 403: permission denied at /vms/%d (missing privilege VM.Audit)", vmid, vmid)
|
||||||
|
}
|
||||||
if f.goneGuests[vmid] { // R-689: PVE's answer for a deleted guest
|
if f.goneGuests[vmid] { // R-689: PVE's answer for a deleted guest
|
||||||
return proxmox.GuestConfig{}, fmt.Errorf("proxmox: GET /nodes/n/lxc/%d/config -> HTTP 500: Configuration file 'nodes/n/lxc/%d.conf' does not exist", vmid, vmid)
|
return proxmox.GuestConfig{}, fmt.Errorf("proxmox: GET /nodes/n/lxc/%d/config -> HTTP 500: Configuration file 'nodes/n/lxc/%d.conf' does not exist", vmid, vmid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,3 +91,22 @@ func TestR689_AGuestLookupFailureIsUnknownNotEmpty(t *testing.T) {
|
|||||||
t.Fatal("a failed guest lookup read as a clean answer")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -381,14 +381,18 @@ func (r *BackupRunner) PickSettledRestoreCandidateOn(ctx context.Context, target
|
|||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
known[e.VMID] = true
|
known[e.VMID] = true
|
||||||
case strings.Contains(err.Error(), "does not exist"):
|
case strings.Contains(err.Error(), "does not exist"), strings.Contains(err.Error(), "permission denied"):
|
||||||
|
// v0.137.0: PVE answers 403 "permission denied at /vms/<id>" — not "does not exist" — for a guest
|
||||||
|
// outside the agent's ACL (the `felhom` pool). Measured on demo-hp after v0.136.0: the deleted
|
||||||
|
// guest 9100's archive made the local tier UNKNOWN every evaluation. A guest the agent cannot
|
||||||
|
// read is not one it manages; its archive is not a candidate.
|
||||||
known[e.VMID] = false
|
known[e.VMID] = false
|
||||||
default:
|
default:
|
||||||
return "", time.Time{}, fmt.Errorf("checking whether guest %d still exists: %w", e.VMID, err)
|
return "", time.Time{}, fmt.Errorf("checking whether guest %d still exists: %w", e.VMID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !known[e.VMID] {
|
if !known[e.VMID] {
|
||||||
r.noteNotAGuestBackupOnce(e, fmt.Sprintf("guest %d no longer exists on this node", e.VMID))
|
r.noteNotAGuestBackupOnce(e, fmt.Sprintf("guest %d does not exist on this node or is not one this agent manages", e.VMID))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !notAfter.IsZero() && e.CTime > notAfter.Unix() {
|
if !notAfter.IsZero() && e.CTime > notAfter.Unix() {
|
||||||
|
|||||||
Reference in New Issue
Block a user