Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
3.5 KiB
REPORT — v0.74.0: re-assert pool membership after restore-over-existing (campaign-2 R2)
Date: 2026-07-07 · Class: implementation (agent). Baseline: main @ e04b75e (v0.73.0)
→ ca0b169. Part B of the campaign-2 R1/R2 fix bundle (C = controller F-C2-1 v0.103.0; A = operator
pool one-liner; D = DR-source spike; E = report correction).
Root cause (the campaign report's R1 was a symptom)
Pool membership is what lets the pool-scoped FelhomAgentGuest token reach a guest — the grant at
/pool/<pool> applies only to pool MEMBERS. pct restore --pool sets membership at CREATE, but a
restore OVER AN EXISTING VMID (the host-loss/finale path) never re-applies it, and no code re-added a
guest to the pool. So every destroy-restore silently dropped membership → the guest lost VM.Audit
→ the NEXT --selftest=restore-test/DR couldn't read the source config → its existing, correct
bindMountOverrides never ran → raw bind mounts hit pct restore → "mp8 … only possible for root"
(the campaign's mislabelled "R1"). The role + ACL were correct all along; membership was the fault.
The fix
Client.PoolAddVMID(ctx, pool, vmid)(internal/proxmox/mutate.go):PUT /pools/{pool}vms={vmid}— PVE-additive (merge, not replace;delete=1removes), idempotent (already-member swallowed), needsPool.Allocate(the token has it). Sync (no UPID).- bring-up re-asserts (
internal/reconcile/bringup.go): after liveness is proven, ifspec.Pool != "", callPoolAddVMID. A pool-add hiccup is surfaced LOUD + intores.StartWarningsbut must NOT flip a healthy running guest's verdict (membership matters for the NEXT op). - B3 (scratch teardown 403) — diagnosed, no code:
restoretest.goalready passesPool: DefaultPoolfor scratch restores → the campaign'sVM.Allocateteardown 403 was a CASCADE of the failed bind-mount restore (a half-built guest outside any pool), not an independent gap. - Role/ACL untouched (correct);
bindMountOverridesuntouched (correct — it just needed to run).
Tests + red-proofs
pool_test.go: PoolAddVMID PUT shape + idempotent-on-already-member + real-error-surfaces +
validation. bringup_test.go: re-asserts when Pool!="" (red-proof: pre-fix no-call →
poolAdds=[] FAIL, demonstrated + reverted), no-pool→no-call, pool-add-failure warns-but-passes
(liveness wins, guest kept). Full gate go build/vet/test ./... = PASS.
Deploy + live acceptance (the headline)
Built -X main.version=0.74.0, deployed to felhom-pve (.bak-0.73.0 kept); --version 0.74.0,
active, capabilities 56/56 degraded=0. Part A run (pveum pool modify felhom --vms 9201, per
operator go): 9201 now a pool member; VM.Audit+VM.Allocate+VM.Backup present on /vms/9201.
Live --selftest=restore-test then PASSED for the first time: neutralizing source bind-mount mountpoints … bind_mounts=2 → scratch 990000 restored + verified boot+running + torn down
clean (no VM.Allocate 403) in 4m35s. B3 confirmed as a cascade; restore-test needs no bind code.
Observations
- DR
bring-up -mode drhas a SEPARATE real gap (no overrides, source guest gone) — scoped infelhom.eu/documentation/audits/SPIKE-dr-bindmount-source-2026-07-07.md; recommended fix is a small known-constant override reusingbindMountOverrides(mp8/mp9 are structural constants). - The empty pool predated the campaign (07:41 403 before the 14:09 finale) — an earlier drill's restore-over-existing; v0.74.0 stops the recurrence going forward.