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

14 KiB
Raw Blame History

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 intactpath /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-r50HTTP 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.