R-85 Phase 4: docs — the UNATTENDED path is documented as unproven

- ROADMAP: R-85 row. Code SHIPPED; rotation NOT YET OBSERVED LIVE, stated as
  such rather than written as done.
- Capability map: a new row for UNATTENDED restore-proof, IMPLEMENTED not
  PROVEN-LIVE, kept distinct from the R-82 row that a MANUAL selftest earned.
  That distinction is the same one the activation-vs-arrival split made.
- 03-host-agent §8: the scheduler covers every tier, oldest-proven first; the
  spec is per-run; a restore-test joins the one-heavy-op gate. The safety
  properties that must not be re-derived are listed.
- 07: restore-proof recorded as a per-tier property. Doc still NOT ratified.
- 06: corrects S4.1's 'the offsite restore-test now runs unattended' — it
  silently stopped being true when local_backup_target was retargeted to 'local',
  the SECOND time in that doc that a correct mechanism was broken by its input
  changing underneath it.
- CONTEXT + REUSE.

Hub gate green (17 packages, rc=0).
This commit is contained in:
Claude Code
2026-07-27 07:33:40 +02:00
parent b802a9e7de
commit 57ba3c7c8c
7 changed files with 67 additions and 1 deletions
@@ -228,6 +228,33 @@ Tiers double as backup *and* restore-source priority (fastest surviving source f
per Part 1: **snapshot** (LVM-thin, transient, whole-guest rollback — not a backup) →
**local second storage** (vzdump to dir/NFS/CIFS) → **PBS offsite** (the DR substrate).
> **Self-restore-test: the scheduler covers EVERY tier, oldest-proven first (R-85, agent v0.104.0).**
> It used to build ONE runner on `cfg.Backup.BackupTarget()`, so **the offsite tier's archives were
> never candidates** — the DR tier could not be restore-tested unattended at all, which is how
> demo-hp's tier sat `applied` with zero snapshots for five days unnoticed. Three things changed:
>
> - **The spec is built PER RUN** (`backup.SpecBuilder`), not frozen at daemon start. It previously
> came from an immediately-invoked function, so `storageTier()`/`restoreTaskTimeout()` were
> evaluated once and reused forever — nothing tier-varying was expressible, and any storage-type or
> config change stayed stale until the daemon restarted. `SourceTier` comes from **the archive**
> (`restoreTierForArchive`), never the configured target: config-derived is what classified a PBS
> archive as `local` and killed a 14.46 GB WAN restore at the 10-minute local bound.
> - **Rotation is oldest-first** (operator ruling 2026-07-26), persisted at
> `<state_dir>/restore-test-state.json` so it survives a restart — an in-memory map would reset
> every tier to "never proven" on each deploy and could starve one indefinitely. **Credit only on
> SUCCESS**, or a permanently failing tier would look freshly proven and stop being retried. A tier
> with no archive **no-ops and the next tier is tried** — a brand-new offsite tier legitimately has
> nothing to restore, and that is not an error.
> - **A restore-test joins the host-wide one-heavy-operation gate** (`backup.InFlight`), shared with
> the local-API backup path. Not a lock concern — the scratch VMID never touches the live guest's
> vzdump lock — but a **LINK** one: an offsite restore PULLS multi-GB over the same tunnel an
> offsite backup PUSHES one, and at the measured ~33 MB/min upstream running both drives each
> toward its timeout, which is how a healthy tier ends up recorded as failed. Callers **defer**;
> they never cancel what is already running.
>
> Unchanged and load-bearing: `onboot=0` on the scratch at restore time, **every NIC link-down before
> boot**, journal-before-mutate, guaranteed teardown, and the per-tier restore-task timeout.
- **Quiescing (controller-driven for app-consistency) — implemented (slice 8B):** an LXC has no
fsfreeze (`proxmox-platform.md` §4.2), so app-consistency is the controller's job: it learns a
backup is due (`GET /backup/due`, §6) → **quiesces** (stops its app stacks) → `POST /backup`