mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
5.6 KiB
Markdown
100 lines
5.6 KiB
Markdown
# Backup and Recover Schema (svc-bkp-* role family)
|
|
|
|
End-to-end flow of the backup chain and its recover counterparts. Solid
|
|
arrows run on schedule (systemd units + pull); dashed arrows are the
|
|
recover direction (`files/python/recover.py` of each role, enforced by
|
|
`tests/lint/ansible/roles/test_bkp_roles_have_recover.py`). The swarm
|
|
test pipeline exercises the full chain in
|
|
`scripts/tests/deploy/swarm/routine/backup/base.sh`.
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph source["Source host (e.g. arch-server1 / swarm manager + NFS server)"]
|
|
direction LR
|
|
volumes["Docker volumes + databases"]
|
|
export["NFS export<br/><export-base>/state/<volume>"]
|
|
secrets["Host secrets + CA + ACME + node identity<br/>/var/lib/infinito/secrets, /etc/<domain>/ca,<br/>/etc/letsencrypt, /etc/certbot, ssh host keys, machine-id"]
|
|
unit_vol["svc-bkp-volume-2-local.service<br/>(baudolo, nightly)"]
|
|
unit_nfs["svc-bkp-nfs-2-local.service<br/>(rsync --link-dest, nightly)"]
|
|
unit_sec["svc-bkp-secrets-2-local.service<br/>(rsync --link-dest per subtree, nightly)"]
|
|
src_backups["/var/lib/infinito/backup<br/><hash>/backup-docker-to-local/<gen>/<volume>/files<br/><hash>/backup-nfs-to-local/<gen>/files<br/><hash>/backup-secrets-to-local/<gen>/files/<subtree>"]
|
|
|
|
volumes -->|"stop-aware dump + rsync"| unit_vol --> src_backups
|
|
export -->|"differential snapshot"| unit_nfs --> src_backups
|
|
secrets -->|"differential snapshot"| unit_sec --> src_backups
|
|
end
|
|
|
|
subgraph backuphost["Backup host (e.g. arch-server2)"]
|
|
direction LR
|
|
unit_pull["svc-bkp-remote-2-local.service<br/>(pull_specific_host.py, backup@source,<br/>forced-command ssh wrapper)"]
|
|
bkp_backups["/var/lib/infinito/backup<br/>(pulled generations = recovery store)"]
|
|
unit_dev["svc-bkp-local-2-device.service<br/>(script.py, plug-triggered via .mount)"]
|
|
|
|
unit_pull --> bkp_backups
|
|
bkp_backups --> unit_dev
|
|
end
|
|
|
|
subgraph device["Encrypted device (LUKS)"]
|
|
stick["<mount>/<hash>/svc-bkp-local-2-device/<ts>/backup"]
|
|
end
|
|
|
|
source --> backuphost --> device
|
|
|
|
src_backups -->|"rsync over ssh (pull only)"| unit_pull
|
|
unit_dev -->|"rsync --link-dest"| stick
|
|
|
|
stick -.->|"local-2-device recover.py<br/>luksOpen + mount + newest snapshot<br/>rsync --delete"| bkp_backups
|
|
bkp_backups -.->|"operator rsync<br/>(backup user is pull-only)"| src_backups
|
|
src_backups -.->|"nfs-2-local recover.py<br/>backup unit first, then rsync --delete"| export
|
|
src_backups -.->|"volume-2-local recover.py<br/>backup unit first, then rsync --delete<br/>into the volume mountpoint"| volumes
|
|
src_backups -.->|"volume-2-local recover.py --databases<br/>baudolo-restore <engine> --empty per dump,<br/>consumers must be down"| volumes
|
|
src_backups -.->|"secrets-2-local recover.py<br/>backup unit first, then rsync --delete<br/>per subtree to its fixed system path"| secrets
|
|
```
|
|
|
|
Databases travel as sql dumps, not as a file tree (`--dump-only-sql`), so
|
|
they have their own recover leg: `recover database <generation>`, which
|
|
`recover full` plans right after the volume steps. That leg is the one part
|
|
of the chain that cannot run on a bare host: a dump is replayed through
|
|
`docker exec`, so the database service has to be deployed and up, with its
|
|
consumers stopped. The engine of a dump is
|
|
resolved from the consuming role's `meta/services.yml` service key in
|
|
`utils/recovery/databases.py` — the same value the backup seed used — and a
|
|
replay refuses to run while a consumer of that database is up, because
|
|
`--empty` pre-cleans the schema and a booting consumer would recreate it
|
|
underneath the replay.
|
|
|
|
Per-role recover procedures live in each role's `## Recover` README
|
|
section; `svc-bkp-remote-2-local` documents its one-way opt-out in
|
|
`files/python/recover.py.nocheck`.
|
|
|
|
## Deploy-time trigger flow
|
|
|
|
When the backup units actually run during a deploy, keyed on `MODE_BACKUP`
|
|
and whether this is a first install or an update. The nightly systemd
|
|
timers fire independently of every path below.
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
start["Deploy starts (tasks/stages/01_constructor.yml)"] --> boot["Pre-preload bootstrap:<br/>docker + NFS export + token store ready"]
|
|
boot --> mode{"MODE_BACKUP?"}
|
|
|
|
mode -->|true| pre["Preload pass instant-starts the pre-state snapshot<br/>(force_flush_instant + state started):<br/>svc-bkp-secrets, svc-bkp-volume (pulls sys-svc-container for a<br/>live dockerd + ensures /opt/compose first), and svc-bkp-nfs<br/>after svc-storage-nfs-server (export must exist)"]
|
|
pre --> kind{"install or update?"}
|
|
kind -->|"1st install"| baseline["Source still empty (no volumes/export yet)<br/>-> near-empty baseline generation"]
|
|
kind -->|update| anchor["Captures the live pre-update state<br/>-> rollback anchor before any mutation"]
|
|
baseline --> apps
|
|
anchor --> apps
|
|
apps["App pass deploys / updates the stacks"] --> term["Terminator (end of play): svc-bkp-remote-2-local<br/>forced pull (force_flush_final)"]
|
|
term --> done["Deploy done"]
|
|
|
|
mode -->|false| install_only["Units installed + enabled, never started here"]
|
|
install_only --> apps_nb["App pass deploys / updates the stacks"] --> done
|
|
|
|
done --> timers["Nightly timers keep running on schedule<br/>(both MODE_BACKUP paths)"]
|
|
```
|
|
|
|
A failing pre-state snapshot aborts the deploy (no `suppress_flush`):
|
|
no update proceeds without a successful backup of the state it is about
|
|
to change. `svc-bkp-local-2-device` is never started at deploy time (no
|
|
device present); it stays plug-triggered via its `.mount` unit.
|