Files
felhom.eu/documentation/audits/CAMPAIGN-10-closeout-2026-08-02.md
T
admin 482af37b7d Campaign 10 closeout Part 2: teardown complete — five layers, each verified gone
Evidence-survival check FIRST: HEAD == origin/main == 7efb7a5, and every artefact
confirmed present in origin/main with git cat-file, including all 27 evidence
files. Nothing died with the rig.

1. VM 311 destroyed by exact VMID. qm list now shows only drill-r50; 311.conf does
   not exist; images/311 gone. 126 GB returned - nvme 133G used / 758G avail ->
   6.7G / 884G. A first attempt stopped the VM but its destroy never ran, killed by
   an unescaped paren in my own echo; caught by checking qm list rather than the
   exit code.

2. c10-scratch removed by name. storage.cfg 5 -> 4 entries. felhom-backup shares
   the same path and is intact, config unchanged.

3. PBS on DooPlex (Tier 2). Identity confirmed first - the target carried the
   comment this campaign set at creation. Datastore felhom-c10, user c10@pbs,
   token !box, both ACLs and the on-disk chunkstore all gone; felhom-spike,
   felhom@pbs, felhom@pbs!n100 and /mnt/5_hdd/backup/demo-felhom all intact. Only
   the campaign's entries disappeared. "user delete" is not a PBS subcommand - it
   printed usage and the user survived rc=0; caught by re-reading the user list.

4. Restic subaccount 281530 deleted after asserting home==felhom-campaign10 and
   the description names Campaign 10 - the script aborts otherwise. 4 -> 3
   subaccounts; demo-felhom, peti-felhom and demo-hp all present.

5. Hub customer c10-soak. The ONLINE gate REFUSED with HTTP 409 for 18 consecutive
   attempts, exactly as anticipated (customer_delete.go:98-102, :146). Predicted
   deletable at last-report 09:38:19 + 30 min = 10:08:19; measured 303 at 10:08:23.
   Verified positively: 0 customer rows, 0 host/appliance rows, GET
   /customers/c10-soak -> 404, and the hub logged the DELETE cascade COMPLETE with
   residue purged. No /appliances/discard was needed - the appliance record was
   bound and went with the cascade. drill-r50 verified INTACT (200, VM 300 present).

All 12 credential files shredded and the directory removed.

Register rows NOT written: app-catalog-felhom.eu has an uncommitted working tree
including scripts/check-volume-persistence.py, so the parallel catalog session is
still open and owns the register. R-156/R-157/R-158 rows and rankings remain owed,
itemised in the doc - and that session's volume-persistence checker looks like
R-156's fix shape, so the two should be reconciled before filing.
2026-08-02 10:11:21 +02:00

279 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Campaign 10 — closeout: two last measurements, then teardown (2026-08-02)
**Companion doc** (my call, per the task) to `CAMPAIGN-10-two-storage-soak-2026-07-31.md` and
`SPIKE-recovery-unit-space-2026-08-02.md`, rather than appending — those two are already long and this
session answers two specific questions and then dismantles the rig.
**Part 1 headline:** Q1 **lowers** R-158's rank — the failure *is* customer-visible, on one page, and it
persists and clears correctly. Q2 **clears** `ValidateDump` of shipping bad dumps, and simultaneously
**kills C2's proposed gate** on its current predicate.
---
## Q1 — when a backup refuses for lack of space, what does the customer see?
Method: ballast `/mnt/sys_drive` to 699 MB free (homebox's unit needs 2.3 GB), run the real backup
through `POST /api/backup/run`, and read every customer-facing surface through the real UI endpoints.
### The five points
| # | Surface | Result |
|---|---|---|
| 1 | **`/backups` overview** | **SHOWS IT**`✗ Adatmentés sikertelen` |
| 2 | **`/backups/apps`** (per-app) | **shows nothing** — Homebox tier-1 reads `Utolsó: 3 perce` |
| 3 | `/stacks/homebox` (app detail) | **shows nothing** |
| 4 | `/` dashboard · `/launcher` | **show nothing** — no banner |
| 5 | `GET /api/backup/status` | `db_dump.success: **false**` |
| — | hub event / email | **none** (R-158) |
Exact copy, as rendered on `/backups`:
✗ Adatmentés sikertelen
### The point-5 question — retry, stay failed, or quietly go green?
**It retries, stays failed, and does not lie.** Measured across three runs:
| run | condition | `status.success` | `/backups` shows failure | unit mtime |
|---|---|---|---|---|
| 1 | constrained | false | **true** | 07:30:50 |
| 2 | still constrained | false | **true (persists)** | 07:30:50 (unchanged) |
| 3 | space freed | **true** | **false (cleared)** | **07:37:38 (fresh)** |
**`/backups/apps` is honest, not deceptive.** Its `Utolsó: 3 perce` tracks the *unit's real mtime*
(07:30:50), not the failed run's timestamp — so it correctly reports the age of the last good unit. It
simply never says the latest attempt failed. **This is NOT the R-156 family**: no surface claims a
fresh backup over a stale unit.
### R-158's rank — LOWERED, with the reason
> **R-158 is a notification gap, not a silent-failure defect. It ranks BELOW R-157.**
Because the customer *can* find out by looking, and the indicator behaves correctly — it persists while
failing and clears on recovery. What is missing is that **nothing pushes it**: no hub event, no email,
no dashboard banner. And the discovery path is poor in a specific way worth fixing: **the per-app
backup page — the natural place to ask "is homebox backed up?" — shows no failure at all**, while the
answer lives one page away on the overview.
R-157 outranks it because R-157's mechanism B leaves a `deployed: true` app **not running**, with the
deadapp check reporting `0 currently down` — silent on *every* channel, including the one the customer
would look at.
---
## Q2 — was `ValidateDump`'s warning correct?
The warning, seen twice:
[WARN] [backup] ValidateDump: …/rallly-postgres.sql is structurally valid (32 tables) but its
accounts table has NO rows — the dump may predate the customer's data
### It was factually correct, and the dump was NOT deficient
The live database genuinely had no accounts. Every non-empty table, from `pg_stat_user_tables`:
_prisma_migrations | 129
cc_proof | 82 <-- the campaign's own canary
instance_settings | 1
accounts | 0
**An empty table proves nothing either way**, so — as the task required — an account was **seeded** and
the backup re-run:
INSERT users (id='c10user', …) -> 1 row
INSERT accounts (id='c10acct', …) -> 1 row
Result:
[INFO] [backup] DB dump: rallly-postgres → rallly-postgres.sql (100.6 KB, 396ms, 32 tables)
(no ValidateDump line at all — the warning STOPPED)
grep -c c10acct <dump> = 1 grep -c c10user <dump> = 2
dump size 102 766 B -> 103 029 B
**Verdict: the validator is sound and no bad dumps are shipping.** It correctly detected an empty
`accounts` table, and the moment real data existed the dump captured it and the warning cleared.
*(Seeded data disclosed: one `users` row and one `accounts` row, both `c10*`-prefixed, on a scratch
customer that this session then deletes.)*
### What it does to C2's ordering — the proposed gate is DEAD on this predicate
C2 proposed dropping the DB volume tar (halving DB-app units, and closing the D5/R-127(b) initdb-skip
password trap) **once `ValidateDump` is promoted from a warning to a gate**. That ordering does not
survive Q2:
> **A fresh appliance legitimately has zero accounts.** Gating on "the `accounts` table has rows" would
> **block the backups of every new customer** until someone registers a user — refusing good backups
> for the entire period a box is most fragile.
The validator's *fact* is right and its *inference* ("may predate the customer's data") is wrong: there
was no data to predate. So the dependency chain is longer than C2 assumed:
1. a **sound gate predicate** first — a comparison against the **live** DB (per-table row counts in the
dump vs `pg_stat_user_tables`) rather than an absolute expectation about one table;
2. **then** the warn→gate promotion;
3. **only then** the tar-drop.
**Until (1) exists, the DB volume tar stays load-bearing** — not because dumps are bad, but because
nothing can yet *prove* a given dump is good.
---
## Findings
**No new `R-n`.** Register grepped (`documentation/backlog/*.md`) for `ValidateDump`, `accounts table`,
`Adatmentés sikertelen`, and the per-app backup page: no existing rows, and neither Q1 nor Q2 produced
a defect that is not already covered.
- **Q1 does not mint an R** — it *re-ranks* R-158 (filed in the spike) and adds the specific
improvement worth making: surface the per-app failure on `/backups/apps`, where a customer looks for
a specific app.
- **Q2 does not mint an R** — it *removes* a proposed change (the gate) and lengthens C2's dependency
chain. Recording that a design is wrong is the spike rule working.
Nothing fixed, per the fences.
---
# Part 2 — teardown: five layers, each verified gone
## 0. Evidence-survival check — done BEFORE anything was deleted
`HEAD == origin/main == 7efb7a5`, tree clean, and each artefact confirmed present **in `origin/main`**
(not merely locally) with `git cat-file -e origin/main:<path>`:
CAMPAIGN-10-two-storage-soak-2026-07-31.md IN origin/main
CAMPAIGN-10-closeout-2026-08-02.md IN origin/main
SPIKE-recovery-unit-space-2026-08-02.md IN origin/main
REPORT-campaign10.md IN origin/main
documentation/tests/campaign10-evidence-2026-07-31/ — 27 files in origin/main
Everything the campaign and both spikes produced survives the rig.
## 1. VM 311 on demo-hp — GONE
| | before | after |
|---|---|---|
| `qm list` | 300 `drill-r50`, **311 `c10-appliance` running** | **300 `drill-r50` only** |
| `311.conf` | present | `Configuration file 'nodes/felhom-host/qemu-server/311.conf' does not exist` |
| `/mnt/nvme-1tb/images/311` | **127 G** | `No such file or directory` |
| `/mnt/nvme-1tb` | 133 G used / 758 G avail | **6.7 G used / 884 G avail** |
**Space returned: 126 GB.** Deleted by exact VMID with `qm destroy 311 --purge`. `drill-r50` untouched.
*(A first attempt stopped the VM but its `qm destroy` never ran — an unescaped paren in my own echo
made the remote shell exit before it. Caught by checking `qm list` rather than the exit code, and
re-run.)*
## 2. `c10-scratch` dir storage — GONE
before: dir: local | lvmthin: local-lvm | pbs: felhom-pbs | dir: felhom-backup | dir: c10-scratch (count=5)
after : dir: local | lvmthin: local-lvm | pbs: felhom-pbs | dir: felhom-backup (count=4)
Removed by name (`pvesm remove c10-scratch`). **`felhom-backup` shares the same path
(`/mnt/nvme-1tb`) and is intact** — `path /mnt/nvme-1tb, content backup, is_mountpoint 1`, unchanged.
## 3. PBS datastore + token on DooPlex (Tier 2) — GONE, production untouched
**Identity confirmed before deleting.** The target carried the comment this campaign set at creation:
felhom-c10 /mnt/5_hdd/backup/campaign10 "Campaign 10 scratch - DELETE at teardown" <-- target
felhom-spike /mnt/5_hdd/backup/demo-felhom (no comment) <-- PRODUCTION
| | before | after |
|---|---|---|
| datastores | `felhom-c10`, `felhom-spike` | **`felhom-spike` only** |
| users | `c10@pbs`, `felhom@pbs`, `root@pam` | **`felhom@pbs`, `root@pam`** |
| tokens | `c10@pbs!box`, `felhom@pbs!n100` | **`felhom@pbs!n100`** |
| ACLs | 2 × `/datastore/felhom-c10` + 2 × `/datastore/felhom-spike` | **2 × `/datastore/felhom-spike` only** |
| on disk | `/mnt/5_hdd/backup/campaign10` | removed; `/mnt/5_hdd/backup/demo-felhom` **INTACT** |
**Only the campaign's entries disappeared.** *(`user delete` is not a PBS subcommand — it printed
usage and the user survived. Caught by re-reading the user list rather than trusting `rc=0`; the
correct pair is `user delete-token` then `user remove`.)*
## 4. Restic subaccount on the Storage Box — GONE, production untouched
Identity asserted programmatically before the DELETE — the script aborts unless both hold:
id=281530 username=u629488-sub4 home=felhom-campaign10 desc="Campaign 10 scratch - DELETE at teardown"
assert home == "felhom-campaign10" PASSED
assert "Campaign 10" in description PASSED
Its inability to reach any production path was already proven in `isolation/restic-denial.txt`.
| | before | after |
|---|---|---|
| subaccounts | 4 | **3** |
| `281530` (campaign) | present | **absent** |
| `273581`/`269130`/`275124` (demo-felhom, **peti-felhom**, demo-hp) | present | **all present** |
## 5. Hub customer `c10-soak` — GONE, after the ONLINE gate refused
**The gate refused, exactly as anticipated.** `POST /configs/c10-soak/delete` returned **HTTP 409**
for 18 consecutive attempts. Source: `internal/web/customer_delete.go:98-102` sets
`onlineBlocked = true` while `hostStatus(LastReportAt) == "ok"`, and `:146` comments
*"leg 1 refuses; decommission the agent first"*.
**When it became deletable — predicted and confirmed.** Last host-report **09:38:19**; `ok` lapses at
30 minutes ⇒ deletable from **10:08:19**. Measured:
09:50:17 attempt 1 -> HTTP 409
10:07:20 attempt 18 -> HTTP 409
10:08:23 attempt 19 -> HTTP 303 <-- 4 s after the predicted threshold
**Verified positively, not by trusting the response:**
customers list — c10-soak / "Campaign 10" rows: 0
hosts+appliances — c10-soak / 55ab3776 rows: 0
GET /customers/c10-soak -> HTTP 404
hub: delete c10-soak: residue purged (reports=198 app_telemetry=1099 notif_prefs=1
selfbind_tokens=1 appliance_registrations=1)
hub: customer DELETE cascade COMPLETE for c10-soak (journal #9) — full teardown
**No separate `/appliances/<id>/discard` was needed**: the appliance record was bound, so it left the
unclaimed list at bind time and was purged by the cascade (`appliance_registrations=1`). The task
anticipated `/discard`; on this path the cascade covered it.
**Remaining customers, unchanged:** Demo Ügyfél, Demo HP, Peti Proxmox, and
**`R-120 golden 0.186.0 proof` (= `sess-f`) — the pre-existing R-131 orphan, not this campaign's and
deliberately not touched.**
**`drill-r50` (R-93 fixture) verified intact:** `GET /customers/drill-r50` → **HTTP 200**, and VM 300
still present and stopped on demo-hp. (It is absent from the dashboard listing because it is DOWN, not
because it is gone — checked explicitly rather than inferred from the listing.)
## 6. Credentials
All 12 files under `~/.config/campaign10/` **shredded** (`shred -u`) and the directory removed —
host/dashboard passwords, API key, app secrets, PBS token, restic password, claim code. The guest-side
copies (`/root/c10/`, `/root/c10api.sh`) died with the VM. No campaign processes remain.
---
# Register work still owed — NOT done this session, and why
**The parallel catalog session is OPEN.** `app-catalog-felhom.eu` has an uncommitted working tree:
`CLAUDE.md` and `REUSE.md` modified, plus untracked `audits/`, `scripts/check-volume-persistence.py`
and `scripts/test_check_volume_persistence.py`. One session owns the register at a time, so **no rows
were written**.
Still owed, for whoever takes the register next:
| finding | needs |
|---|---|
| **R-156** | row + rank. papra's data neither persisted nor backed up, reports healthy. *Note: the parallel session's `check-volume-persistence.py` looks like exactly this fix's shape — reconcile before filing.* |
| **R-157** | row + rank. bootrecon's start-once sweep, **two mechanisms** (A: Exited/race ~50%; B: zero-container, **silent on every channel**). A settle-condition fix closes A only. |
| **R-158** | row + rank — **ranked BELOW R-157 by Q1** (notification gap, not silent failure; visible on `/backups`, absent from `/backups/apps`). |
| spike C2 | not a finding: a **dependency chain** (sound predicate → warn→gate → tar-drop), lengthened by Q2. |
# What did not happen, and why
- **No register rows written** — parallel session owns it (above).
- **Nothing fixed** — R-156/R-157/R-158 and everything Q1/Q2 surfaced are findings only, per the fences.
- **No `/appliances/<id>/discard` call** — there was no appliance row left to discard (§5).
- **`sess-f` / R-131 orphan not deleted** — not this campaign's record; deliberately out of scope.
- **The ~5 GB linearity point** — the earlier spike's brief said skip it; still skipped.