feat(backups): assert restic CONTENT, and add the host that was never checked
Operator ruling 2026-09-22 (relayed via svos-dev): have the freshness check
assert snapshot content rather than REST-server reachability, so the green
light is a statement about DATA rather than about a daemon.
⚠ FIRST, A CORRECTION TO MY OWN REPORT. I ran `grep -ic restic` against
backup-freshness-alert.sh -- the WRAPPER -- got 0, and told the operator that
nothing on the fleet verified restic. Wrong. check-backup-freshness.sh has
always checked restic snapshot ages, for seven hosts. I grepped one file of a
two-file tool and generalised, which is the identical error a peer had just
made with dev-backup, made while correcting them.
THE REAL GAP was narrower and still real: the NH3 host list read
`irv-ml1 nh3-docker` and omitted nh3-dev -- the repo holding every Claude Code
session transcript, althing routes, hermes run history and Miranda's
conversation. /volume1/Backup/restic/nh3-dev/ has always existed and always
been written nightly; it was simply never enumerated, so a total failure of the
one repo nobody could reconstruct would have gone unreported indefinitely.
THE CONTENT ASSERTION, built on svos-dev's ladder (list proves the repo
answers; ls proves the index; check proves structure; RESTORE proves the bytes
come back):
- CONJUNCTIVE: latest snapshot newer than MAX_AGE_H AND the probe path
present AND a restore returns non-zero bytes. "A snapshot exists containing
X" is satisfied by a three-month-old one; "the latest is recent" is
satisfied by an empty one. Age alone was the old problem; content alone is
the same problem rotated.
- IT RESTORES. Metadata is what survives the failures worth fearing -- a
pruned or partially-written repo can list a path whose blobs are gone.
~493KB from ~/.local/state/svos, sub-second. Large paths are not restored:
a repo that returns one file will return others.
- IDENTITY: the timer runs as lkraven, which has no NOPASSWD sudo on nh3-dev,
so the probe hops through infra-ops@localhost. /etc/restic is root-only by
design.
⚠ restic ls ALWAYS PRINTS A HEADER LINE, matched or not. A path absent from the
repo returns 1 line; a real one returned 6. Counting with `grep -c .` read the
header as a hit, so the ABSENT case fell through and reported "blobs gone" --
telling an operator the repository was corrupt when the truth was a mistyped
path. Now `grep -c '^/'`.
TESTED BY MAKING IT FAIL, because a check only ever seen passing is untested:
bogus probe path -> "absent from snapshot"; BACKUP_MAX_AGE_HOURS=1 -> "12h old
(>1h)"; healthy -> snapshot id, age, entries, bytes restored.
This commit is contained in:
+36
-18
@@ -177,30 +177,48 @@ the stop→umount→rm-ghost→remount→start variant.
|
||||
|
||||
## Known gaps / TODO
|
||||
|
||||
### ⚠ NOTHING VERIFIES THAT RESTIC EVER RUNS (found 2026-09-22, open)
|
||||
### ✅ restic content assertion (2026-09-22, CLOSED)
|
||||
|
||||
`scripts/backup-freshness-alert.sh` contains **zero** references to restic:
|
||||
⚠ **I reported this gap wrongly first.** I ran `grep -ic restic` against
|
||||
`backup-freshness-alert.sh` — the **wrapper** — got 0, and told the operator
|
||||
nothing on the fleet verified restic. The actual checking lives in
|
||||
`check-backup-freshness.sh`, which had always checked restic snapshot ages for
|
||||
seven hosts. Grepping one file of a two-file tool and generalising is the same
|
||||
error a peer had just made with `dev-backup`, made while correcting them.
|
||||
|
||||
```sh
|
||||
grep -ic restic scripts/backup-freshness-alert.sh # -> 0
|
||||
```
|
||||
**The real gap was narrower and still real:** the host list read
|
||||
`irv-ml1 nh3-docker` and **omitted nh3-dev** — the repo holding every Claude
|
||||
Code transcript, althing routes, hermes history and Miranda's conversation.
|
||||
`/volume1/Backup/restic/nh3-dev/` had always existed and always been written;
|
||||
it was simply never enumerated, so a total failure of the one repo nobody could
|
||||
reconstruct would have gone unreported indefinitely.
|
||||
|
||||
It checks PBS guest snapshot ages, and it pings the rest-servers for liveness
|
||||
(`rest-server-nh3: up (401)`). **That confirms the server answers. It says
|
||||
nothing about whether a snapshot was ever written to it.**
|
||||
Both are fixed. nh3-dev is in the list, and a **content assertion** now runs per
|
||||
the operator ruling of 2026-09-22 (assert snapshot content, not REST-server
|
||||
reachability):
|
||||
|
||||
So if resticprofile stopped, the password file broke, or every run failed, the
|
||||
checker would go on printing `RESULT: all backups fresh` — correctly by its own
|
||||
definition — indefinitely. Restic holds the entire home directory on nh3-dev;
|
||||
PBS holds the VM images. The layer with the granular data is the unwatched one.
|
||||
- **Conjunctive**: latest snapshot newer than `MAX_AGE_H` **AND** the probe path
|
||||
present **AND** a restore returns non-zero bytes. Age alone was the old
|
||||
check's problem; content alone is the same problem rotated.
|
||||
- **It restores.** Listing proves the *index*; only a restore proves the bytes
|
||||
come back, and metadata is exactly what survives a pruned or partially-written
|
||||
repo. ~493 KB from `~/.local/state/svos`, sub-second. The large paths are not
|
||||
restored: a repo that returns one file will return others, one that cannot is
|
||||
broken for everything.
|
||||
- **Identity**: the timer runs as `lkraven` (no NOPASSWD sudo here), so the probe
|
||||
hops through `infra-ops@localhost`, which has it. The credentials under
|
||||
`/etc/restic` are deliberately root-only.
|
||||
|
||||
This is the same shape as two other instruments found the same day: a tool that
|
||||
enumerates what is fine has selected against its own subject. Here the light is
|
||||
not merely blind — it is **actively reassuring** about a system it cannot see.
|
||||
⚠ **`restic ls` always prints a header line**, matched or not — a path absent
|
||||
from the repo returns 1 line, a real one returned 6. Counting with `grep -c .`
|
||||
read the header as a hit and made the ABSENT case fall through to *"blobs
|
||||
gone"*, telling an operator the repository was corrupt when the truth was a
|
||||
mistyped path. Count `grep -c '^/'`.
|
||||
|
||||
Fix when taken: check restic snapshot **age per host**, not endpoint liveness,
|
||||
and carry an expected-paths list so "covered by a job that has not run in a
|
||||
month" stops rendering identically to "covered".
|
||||
**Tested by making it fail**, because a check only ever seen passing is
|
||||
untested: a bogus probe path reports *absent*; `BACKUP_MAX_AGE_HOURS=1` reports
|
||||
*too old*; the healthy path reports the snapshot id, age, entry count and bytes
|
||||
restored.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user