The extras looked up the 'swarm' and 'data' meshes by name and fell back to
the underlay when they were absent, so any other declarations ran swarm and
NFS outside the tunnel without failing.
user-administrator creates the account from users.administrator.password,
which the development inventory states outright. The value ansible sends for
become is generated per host and never reaches the account, so every sudo
over the mesh was refused. Nothing noticed before because the container
connection escalates to root without a password.
The extras already carry that inventory's users, so the same value now rides
the deploy-facing twin as ansible_become_password.
The cluster mounts the export over the mesh; the act runner is not a member
and mounts it over the lab network. Both addresses now travel in the extras
rather than one serving two topologies.
The extras file pinned swarm.manager.advertise_addr and storage.nfs.server to
the lab addresses, and extras override the group_vars default. Every tunnel
came up and carried nothing: swarm kept advertising 192.168.244.10 and the
clients kept mounting 192.168.244.13, so the mesh existed beside the traffic it
was built for rather than under it.
Read from the host_vars the mesh writer produced earlier in the round rather
than recomputed, so the address allocation stays in one place, with the
underlay kept as the fallback for a round that has no mesh.
Caught in a drill whose gate had just gone green: tunnels healthy, handshakes
current, and swarm still on the underlay.
The extras writer carved `applications` out of the deploy-facing twin and said
why in its own docstring: extra-vars replace rather than merge, and would strip
every generated credential. `users` carries the same hazard and stayed in, so
every swarm deploy ran with `-e @swarm-nfs-extras.deploy.yml` replacing a
nine-entry users map with the three from inventories/development/default.yml.
Everything not in those three then fell to its role default. For zammad-api-bot
that default is `{{ 42 | strong_password }}`, and utils/manager/value_generator.py
opens by warning what that means: it is a template, not a value, re-rendered at
every use site, so the task that creates an account and the task that later
authenticates as it read two different secrets. Playwright sent a 42-character
password that no deploy had ever written, and Zammad answered
Seed POST /api/v1/tickets failed: 401 Invalid BasicAuth credentials
on both swarm jobs of Zammad#1 while both compose jobs passed. Compose never
sees an `-e @file`; its driver emits only scalar pairs.
`users` now leaves the twin the way `applications` already does. The two deltas
the block actually carried, the swarm-test pubkeys for administrator and backup,
travel the merging channel instead: baked into base_overrides, handed to the
provisioner through --vars, and deep-merged into host_vars. ensure_swarm_keypairs
is split out so the keys exist before provisioning rather than at extras time.
Two things this restores beyond the 401. The rotation gate rotates
users.<name>.password, and swarm was overwriting the result with the defaults
straight afterwards, so user rotation was never actually exercised there. And
user-administrator/meta/users.yml ties the administrator account password to
ansible_become_password; the extras had been pinning it to default.yml's sample
literal instead, which sudo on these nodes does need to match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drill configured svc-bkp-remote-2-local with a fixed manager plus NFS provider list for every app, while the manager only runs a backup repository when the app's include closure pulls svc-bkp-volume-2-local or svc-bkp-secrets-2-local in. For an app whose container_backup consumer is statically off, such as svc-dns-unbound, user-backup never ran on the manager, so the pull was rejected with 'Permission denied (publickey,password)' and the fail-if-any wrapper took the whole unit down at drill step 4/9. Four roles reach that step today: svc-dns-unbound, svc-db-redis, svc-registry-cache and svc-registry-docker.
backup_repos.py expresses the placement once and matrix derives the provider list from it per round. derive_includes accepts the round's variant map because INFINITO_APP_VARIANTS only reaches the subprocess callers. The extras file keeps the device paths the drill reads back and loses the provider list nothing read from it, and the drill banners stop claiming backup legs that were never installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move files that share a delimiter-bounded name prefix within a folder (threshold 2) into a subfolder named after the prefix, dropping the prefix from the filename. Covers plugins/filter (docker, generate, has, merge, get/all), env handler subpackages, utils/roles/entity, utils/tests/swarm/write, role SQL/scripts/templates, keycloak/mailu task-includes, shared tasks/utils/load, and CI scripts. Update every import, query_file/script/include_tasks/src/lookup target, compose bind mount, and python -m path to the new locations.
Extend the prefix-grouped-files lint to threshold 2 with dir-level .nocheck and file-head nocheck opt-outs, and skip groups whose shortest member's stem equals the shared prefix. Keep flat only where a loader forbids nesting: WordPress mu-plugins and the docs/requirements namespace. Ansible filter discovery is recursive, so nested filters stay Jinja-resolvable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>