Two bots bumped the one baudolo version independently. The pip pin
updater rewrites the literal package_name pin in
roles/svc-bkp-volume-2-local/tasks/01_install.yml and opens a PR scoped
to roles/<entry>/, and dependabot bumps the same package in
pyproject.toml. Each PR alone splits the pins that
tests/lint/repository/dependencies/test_baudolo_pin.py holds equal; #995
merged such a split and turned main's test-lint red until #996, and the
next upstream release (7.1.2 is tagged) would repeat it.
The version lookup now takes an optional distribution name and returns
its exact == pin from [project.dependencies]. The install task asks it
for backup-docker-to-local, so pyproject.toml is the only place the
version lives, dependabot is the only bot that moves it, and the pip
updater no longer finds a literal to bump. The lint now forbids a
literal pin in the task and requires the lookup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7.0.1 stops the --empty pre-clean from listing an extension's own objects for
a one-by-one DROP that postgres refuses. Without it a discourse generation is
unreplayable: the role declares the vector extension, its functions belong to
the application role rather than to a superuser, and the pre-clean's owner
filter therefore selected them.
That aborted the restore under ON_ERROR_STOP, which is where the backup drill
stood after the dump itself started working again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.0.0 resolves a databases.csv instance from one engine-name set instead of
an inline regex, which closes two gaps 6.0.0 left. A container named exactly
after its engine resolves again without a declaration, and the central
MariaDB is dumped under swarm for the first time: its task reads
mariadb_mariadb.1.<id>, which matched neither the static name passed through
--database-containers nor any token the suffix match knew.
It also stops recording an application container as a database. Refusing its
dump alone would have written the volume to the manifest as
database: true, dumped: false, which is the shape cab5e2523 makes the restore
drill fail on - the fix for one defect would have tripped the check for
another.
Both pins move together because the version that writes a generation and the
version that reads it must agree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.0.0 forwards PGPASSWORD into the container, so an engine whose pg_hba
demands a password on TCP loopback is dumped instead of refusing every
attempt, and it stops resolving an application container onto its own
dedicated engine's databases.csv row, where a dump used to start and write a
file that looked like a backup and held none of the data.
Both pins move together because the version that writes a generation and the
version that reads it must agree: pyproject.toml covers the control host
running utils/recovery, the role covers the target host that writes.
6.0.0 also requires a database container whose name carries no database
token to be declared through --database-containers. Every container this
repository produces is already covered: the central engines are declared via
backup.database_routine, dedicated engines are named <entity>-database, and
bigbluebutton names its own bigbluebutton-postgres-1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A recent refactor imported baudolo into utils.recovery for three layout strings, and cli.administration.recover.__main__ imports full unconditionally - so the whole recovery CLI, --help included, died at import on every host without baudolo. That is most of them: recover device|nfs|secrets runs on swarm nodes, the NFS server and bare rescue hosts, while only the backup node carries svc-bkp-volume-2-local. utils/recovery had been deliberately stdlib-only for exactly that reason.
baudolo 5.0.0 writes a manifest into every generation stating the layout it used, so the names are now asked of the generation being read rather than of the installed package. A tree written by any version is walked with the names that version used, and utils.recovery.layout answers only for generations older than the manifest, which cannot describe themselves. That also retires two of core's guesses: the '<entity>_database' naming convention now yields to the engine the run actually recorded.
A repo whose manifest cannot be read is skipped rather than guessed at, but no longer takes the other repos with it - recovering two of three beats recovering none because one manifest is corrupt. The exit code reports the gap.
The pin lint compared two declared version strings and was therefore green while a different distribution was installed. It now checks the installed distribution and the layout literals against baudolo's own BackupPaths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
utils/recovery restated three things baudolo owns: the generation layout (suffixes, sql/ and files/ directories), the \connect parser for cluster dumps, and the databases.csv reader. Each was a second implementation of a contract whose author is one pip install away - the connect parser had even received the same quoted-identifier fix twice, once per repo.
The layout constants are now derived from BackupPaths, the inventory comes from dump_inventory, and the CSV goes through read_rows. dump_inventory is stricter than the parser it replaces: it also reads CREATE DATABASE lines, so a database a stream creates but never reconnects to is found - a new test pins that. recover/full.py and the drill's token.py take their globs from the same constants instead of spelling them out.
The dependency this creates is declared: backup-docker-to-local==4.0.0 in the base dependencies, because the deploy CLI must import from the base set alone. The role installs the same version, and a lint holds the two pins together - they drifted apart is exactly the failure this whole commit removes, so it must not come back one level up.
The generation-layout tests now write their fixtures through BackupPaths itself; before, the fixture hand-built the paths with the same assumption the test claimed to verify.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A databases.csv row with database = '*' makes the backup dump a whole instance with pg_dumpall. The recover chain had no path for that file: it sorted cluster dumps out and printed SKIP, so a host whose engine is dumped that way could not be restored at all, and the drill refused to certify it.
Cluster dumps now go back through 'baudolo-restore cluster' with the instance's superuser. Three details decide the shape. A cluster row states '*' where a single-database row names a database, so credentials_of would file every instance under one key and keep the first, hence a lookup keyed by instance. Which applications must be down is stated by the dump's own connect lines and nowhere else, and reading them needs a real identifier parser: a quoted name may hold spaces and psql options precede the target. And the consumers of a cluster sit in the compose project of its engine, not in projects named after each database, so both are checked before anything is dropped.
The version gate moves out. baudolo compares the dump's header against the running server before its own pre-clean, so the copy here was a second implementation of the same rule, free to drift from the one that actually gates the replay. The test asserts the delegation negatively: the replay must not query the engine version itself. The pin follows, and baudolo-restore joins the command assertion.
The docker probing splits into utils/recovery/docker.py, which keeps the replay module under the size limit and gives the two halves one job each. Calls go through the module rather than through imported names, or a test patching the new module would not reach them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ASYNC pass fires a second backup run when it finds only one generation.
Generation directories are named by the second, so on a host with little to
copy both runs landed in the same one: baudolo reused the finished generation,
rsync --delete overwrote it, and the stamp guard failed the unit afterwards.
CI job 95119330633 died exactly there.
Wait out the second before the extra run, and take baudolo 3.4.3, which claims
the generation directory exclusively and aborts before the first write instead
of reporting the damage after it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.4.2 stops the registry host and the tag from deciding which dump tool runs. On a swarm node that hosts the local registry and is named after the app under test, svc-db-mariadb-swarm-mgr-01:5000/postgres_custom read as MariaDB, mariadb-dump ran inside a Postgres container and exited 127, and the BackupException took svc-bkp-volume-2-local down with it -- which is what failed the MariaDB 0 job of run 31864448027 through its CLI test.
Verified on PyPI before the bump: pip index versions reports 3.4.2 as LATEST, so the pinned install resolves. Repo-wide this is the only pin of the package.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.4.1 drops -b from the volume rsync, so a generation no longer keeps a file
the source replaced or deleted between the hot and the cold pass as name~.
That twin is what made the restore drill's elasticsearch unrecoverable: Lucene
parses every file starting with segments as a radix-36 generation, so a restored
segments_3~ leaves the shard store unreadable and .security-7 unallocatable,
and the reserved elastic user then has no password hash to authenticate against.
Only new generations are clean. A restore from one written before this bump
still carries the twins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
svc-bkp-volume-2-local passes --volumes-no-backup-required, which 3.3.0 carries, but the generation is only free of unix sockets and fifos from 3.4.0, where the volume copy runs with --no-D. Without the bump the deploy keeps installing a baudolo that writes socket inodes onto the ganesha export and the remote pull keeps exiting 23.
This is the only pin in the repository. It must not reach a deploy before 3.4.0 is on PyPI: sys-pip-install would fail to resolve the version and abort the role.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exclusion was a property of the image: --images-no-backup-required resolves through volume_is_fully_ignored, which skips a volume only when every container using it is ignored. A container holding a derived tree beside state that must be kept could not express the exclusion at all.
matrix_mdad_docker is that case - the data root of a nested docker daemon, sharing one container with matrix_mdad_matrix and matrix_mdad_state, which carry the actual Matrix data. Copying it is not merely wasteful: overlay2 encodes a deletion both as a character-device whiteout and as a trusted.overlay.* xattr, and no rsync in the chain carries -X, so the copy cannot be restored faithfully. The whiteouts also cannot be written onto an overlayfs destination at all, which is what kept the web-app-matrix DR drill red for 58 minutes a round.
meta/volumes.yml gains a 'backup:' key, validated like 'nfs:'. The backup_volume lookup resolves every 'backup: false' entry to its pinned docker name and feeds baudolo's --volumes-no-backup-required. A lint requires the decision to be written down for any volume mounted at /var/lib/docker or /var/lib/containers, so the next docker-in-docker role cannot repeat this silently.
Requires backup-docker-to-local 3.3.0, which carries the flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The launcher declined on every btrfs host for a reason no operator could
guess: `/var/lib/docker` is created by mkdir, so it is an ordinary
directory inside the enclosing subvolume, and `btrfs subvolume snapshot`
needs a subvolume root as its source. Documenting that as a manual
prerequisite would have left the feature switched on and never firing.
ensure_subject_subvolume.sh converts the data root on the deploy that
introduces the mode. The copy uses `cp --reflink=always`: both trees live
on the same btrfs, so extents are shared instead of duplicated and the
conversion costs seconds and no extra space no matter how large the data
root is - where an rsync copy would have meant hours of downtime and a
free-space requirement of 110%. The original tree is kept until the daemon
has answered on the converted root, so a failed copy, a mismatched
verification pass or a daemon that will not start all put it back and
start docker on it again.
The mode itself moves from group_vars into meta/services.yml as
`snapshot_mode`, read through `lookup('config', ...)`, so a single host can
override it from the inventory. Falsey spellings normalise to `never` and
anything else fails a deploy-time assert, because YAML reads a bare
off/no/false as a boolean and `--mode False` would kill the unit nightly.
btrfs-progs is installed wherever a btrfs mount exists, with centos opted
out: centos stream ships neither the kernel module nor the package, and
demanding it there would fail the package-availability test for the whole
RedHat family.
The pin moves to 3.2.2, which is not cosmetic: that release carves the
snapshot inside its subject, and the reaper here looks for leftovers in
exactly that place. Against 3.2.1 it would search beside the data root and
never find one.
Two drills run before anything else in test.sh: subvolume_probe.sh proves
the conversion carries every path, payload and symlink across and that the
result can actually be snapshotted; snapshot_probe.sh pins the launcher's
decision, including the refusals that keep a host on the live copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baudolo 3.2.1 can capture every volume from one atomic filesystem
snapshot, which removes both the container stops and the torn copy a
live rsync of a database produces. It states the snapshot kind rather
than probing it, so naming a kind the host cannot deliver costs a whole
backup run.
Deciding that at deploy time is not possible: the set of docker volumes
changes between deploys, and a volume created afterwards would sit in the
snapshot as an empty directory, so the copy would succeed while holding
nothing. The decision therefore moves into the run itself. ExecStart now
points at baudolo-snapshot, which probes the host, appends
--snapshot/--snapshot-subject when a snapshot will be faithful, and
becomes the baudolo process either way.
STORAGE_BACKUP_SNAPSHOT_MODE ships as `never`, so no host changes
behaviour until an operator opts in - baudolo's snapshot path has never
been driven end to end on a real deployment. `auto` probes and degrades
to today's live two-pass copy with the reason in the journal; a stated
`btrfs`/`zfs` skips only the fstype lookup and fails the unit when the
host cannot deliver, which is the point of stating it. The token is
`never` rather than `off` because YAML 1.1 reads a bare off/no/false as a
boolean, which would render `--mode False` and kill the unit on every
timer fire; falsey spellings are normalised and a deploy-time assert
rejects anything the launcher's choices would not accept.
The probe refuses unless the data root is a btrfs subvolume root whose
parent is writable and on the same filesystem with no nested subvolume
below it, and unless every docker volume uses the local driver, declares
no Options of its own, sits under the data root and has nothing mounted
inside. The volume rules read the declaration rather than the mount
table, because docker mounts an NFS-backed volume lazily and unmounts it
when the last container stops - such a volume is a separate filesystem
that a snapshot of the data root captures as an empty directory. `auto`
accepts btrfs only; zfs needs to be stated, since none of its premises
could be exercised here.
Before taking a snapshot the launcher removes the `.baudolo-*` leftovers
a killed run leaves beside the data root: baudolo removes its snapshot in
a finally, which a SIGKILL or a reboot skips, and a read-only snapshot of
the data root pins every block the live root later frees.
--hard-restart-projects is stripped when the snapshot flags are appended:
it exists for stacks whose database cannot be backed up hot, which is
what a snapshot removes.
The pin moves 3.1.3 -> 3.2.1. That version is not on PyPI yet; publish it
before deploying, or the pip task fails on every host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.
Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.1.3 wraps the postgres dump replay in a single transaction, preventing the concurrent-writer duplicate-key abort that failed the swarm restore drill for the discourse-backed nextcloud/wordpress variants.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the postgres --empty pre-clean covering text search dictionaries and configurations, which unblocks taiga's restore drill. CI needs the 3.1.2 release on PyPI before this pin can deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the restore fix that drops overloaded postgres functions by identity signature, unblocking the svc-bkp-remote-2-local restore drill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the --empty pre-clean fix (per-statement \gexec drops and
non-public schema removal) needed by the discourse and gitlab restore
drills.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the constructor's pre_deploy_snapshot.sh loop with the loader
itself: volume/nfs/secrets instant-start their oneshot in the preload
pass under MODE_BACKUP (force_flush_instant + state started), so the
live pre-update state is snapshotted before any app mutation; nfs keeps
run_after svc-storage-nfs-server, volume pulls sys-svc-container for a
live dockerd and pre-creates /opt/compose. remote pulls at end of play
via the terminator (force_flush_final) once every provider snapshot is
done. Dropping suppress_flush arms the result assert: a failed
pre-state backup aborts the deploy. Role READMEs carry the new trigger
schema as mermaid diagrams.
Known residual: on a host whose pull identity is not yet provisioned
the terminator-forced remote pull fails and turns the deploy red; the
swarm lab installs the trust only in the DR drill after round 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the find_dock_val_by_bkp_entr filter with three role-local
lookups that read the live merged applications config:
backup_database_containers (backup.database_routine -> service names),
backup_image (backup.no_stop_required/disabled -> exact deployed image
refs via the image lookup, swarm registry prefix and custom builds
included), and backup_hard_restart_projects
(backup.project_hard_restart -> compose project names, declared on
mailu instead of hardcoded). Pin baudolo to 3.0.0, whose --images-*
flags match .Config.Image exactly and whose hard-restart flag is
--hard-restart-projects.
The restore drill now starts all compose projects in parallel per
retry round; the previous serial alphabetical loop deadlocked when a
project's migrate service waited on a datastore project that sorted
later (mastodon -> postgres), killing the CLI test at the 3600s
timeout. Volume restore rsyncs into the mountpoint directly, dropping
the rsync helper image remnants from the test env.
BREAKING CHANGE: deploys require backup-docker-to-local 3.0.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.8.1 drops user-owned collations in the postgres --empty pre-clean, so
dumps with CREATE COLLATION (OpenProject) restore cleanly. Includes the
regenerated .dockerignore picking up the .claude-* ignore pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.8.0 ships the restore fixes the svc-bkp e2e drill needs: the mariadb
empty mode drops FK-linked tables in one session, the postgres replay
skips superuser-only dump lines without touching COPY data, dumps are
produced app-user-replayable, and swarm task containers are never
stopped around volume backups.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the role, its test tree, the primary entity key
(volume-2-local), the derived vars (BKP_VOLUME_2_LOCAL_*,
SYS_SERVICE_BACKUP_VOLUME_2_LOCAL), the run-once fact, and every
group_names consumer flag, include path, doc, and lint reference. The
container_backup service key and the on-disk backup-docker-to-local
tree keep their names: both are contracts with existing consumer
entries, snapshot trees, and the baudolo pull tooling.
Operators must rename the svc-bkp-container-2-local inventory group and
disable the previous versioned units on already-provisioned hosts; the
pre-deploy snapshot glob derives its base from the new role name and
will not find them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>