Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Fable 5 904072237c feat(svc-bkp-local-2-device): scheduled device sync when mounted
Adds a nightly timer (02:00, after the secrets snapshot) next to the
existing plug trigger. ConditionPathIsMountPoint on the unit makes an
unmounted tick a silent condition-skip instead of a failure, so the
schedule only ever acts when the device is currently plugged in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:47:52 +02:00
kevinveenbirkenbachandClaude Fable 5 ed23a19a46 feat(svc-bkp): run pre-state backups in the service-loader preload
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>
2026-07-13 01:47:52 +02:00
kevinveenbirkenbachandClaude Fable 5 cd50dc0544 feat(cli): role order tools under meta/roles/order
callorder moves to 'order run' unchanged in behaviour (group-file call
order of the main pass); the new 'order preload' prints the
sys-service-loader preload order from the service registry, run_after
edges included, so both scheduling layers have their own inspector
instead of one tool mixing them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:47:52 +02:00
kevinveenbirkenbachandClaude Fable 5 feccc1d916 refactor(registry): make run_after ordering bucket-agnostic
The per-bucket topological sort forbade run_after edges across loader
buckets (later bucket = hard error, earlier = silently ignored), a
leftover from before the service loader owned the preload order. The
sort is now global: an edge orders across any bucket, the name-prefix
bucket only tie-breaks among simultaneously ready roles, and cycle
detection covers the whole graph. The sort lives in the shared
run_after_topological_order helper, and the never-declared preload
flag is gone (run_after is the single ordering source).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:47:52 +02:00
kevinveenbirkenbachandClaude Fable 5 c3d6657311 fix(suitecrm): seed the app tree from the image instead of volume copy-up
Baking the multi-GB tree into /var/www/html made dockerd's volume
copy-on-first-use flood the shared NFS export from every swarm node in
parallel, so the stack never converged. The Dockerfile becomes a
multi-stage build (shared base with PHP/Apache setup, builder producing
the tree without node_modules, final stage with one COPY --chown layer
under /usr/src/suitecrm) and the entrypoint seeds the empty webroot
volume once, under the existing cross-replica boot lock; installed
volumes are only stamped, never overwritten. Ownership and the
writable-dir modes are baked at build, dropping the runtime chown pass
that crashed on the read-only ldap.yaml config mount. start_period
grows to 20m with the lock ceilings recoupled (live lab NFS measures
the seed at roughly a minute). App dir, seed dir and web user/group are
single-point role vars threaded through build args, entrypoint and
php.ini; the FROM lint now accepts file-internal stage references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:44:17 +02:00
kevinveenbirkenbachandClaude Fable 5 cc4945aaa5 fix(svc-bkp-nfs-2-local): stop snapshotting the shared backup root
The nfs backup rsyncs the whole export, and the shared backup root
lives inside it (every node's DIR_BACKUPS is the mounted state dir), so
each generation embedded a copy of all previous backups and grew
quadratically; the giant listings also flushed real errors out of
captured journals. The unit now passes the backup root relative to the
export as an anchored rsync exclude, and recover.py protects the live
root from --delete while never replaying embedded stale copies from old
generations (DirectoryRecovery gains rsync_extra_args for that).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:44:17 +02:00
kevinveenbirkenbachandClaude Fable 5 0d13895616 fix(matomo): treat a stub config.ini.php as not installed
The container healthcheck's first page views make the installer write a
stub config before the install check runs, so the check reported
'installed', the retry clean-slate step (gated on the missing-config rc)
never fired, and every bootstrap retry inherited the previous attempt's
partial state until Matomo wedged in the CoreUpdater redirect (3x420s,
whole lab red). Installed now means the config carries a [database]
section, and the retry move-aside fires on any non-zero pre-check rc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:42:13 +02:00
kevinveenbirkenbachandClaude Fable 5 f7102b40d8 fix(user-backup): whitelist backup repos from the BACKUP_REPOSITORIES SPOT
The provider ssh-wrapper enumerated only the docker and nfs repo types,
so the first secrets generation made every remote pull fail: the puller
discovers types dynamically, hit backup-secrets-to-local, and the ls
fell through to 'This command is not supported' (exit 1), killing the
DR drill at step 4/9. BACKUP_REPOSITORIES in group_vars now drives both
the wrapper whitelist and its type discovery, so only whitelisted repos
are returned and only whitelisted repos are pullable. A lint pins the
SPOT against the svc-bkp-* repo names in both directions, so a fourth
repo type cannot bypass the wrapper again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:42:13 +02:00
kevinveenbirkenbachandClaude Fable 5 329897f335 refactor(tests): move the skip list into meta/tests.yml
meta/tests.yml becomes the SPOT for per-role test metadata: the
test-deploy discovery skip list moves there (top-level skip:) from the
primary service entity in meta/services.yml, next to the existing
cli.timeout. get_role_skip reads the new location; the drv lint,
--skip-mode help and unit fixtures follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:40 +02:00
kevinveenbirkenbachandClaude Fable 5 808efad765 fix(test-e2e-cli): per-role CLI timeout via meta/tests.yml
svc-runner's CLI test runs a full nested reinstall deploy and needs
75+ minutes, so the uniform 3600s timeout killed it deterministically
in both modes. A new role_tests lookup reads roles/<id>/meta/tests.yml
(cli.timeout, scaled through the timeout lookup) with the existing
default as fallback, keeping the short hang-leash for every other CLI
test. svc-runner gets 4h and pins the nested deploy to variant 0; the
second matrix round only redeployed cdn+dashboard and added no
assertion value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:40 +02:00
kevinveenbirkenbachandClaude Fable 5 bae7d27510 fix(nextcloud): tolerate occ stalls in the swarm healthcheck
The occ-status probes (1m interval, 10s timeout, 3 retries) flipped
unhealthy during the recognize plugin install (575MB onto the NFS html
volume saturates the lab node), so swarm reaped the fpm and cron tasks
mid-install and wedged the DinD dockerd the retries kept exec-ing into.
Raise both probes to 30s timeout / 10 retries so a load storm outlasts
the healthcheck instead of the deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:40 +02:00
kevinveenbirkenbachandClaude Fable 5 f07b671069 fix(opentalk): gate kcadm provisioning on a healthy keycloak
The single-shot master-realm login raced a keycloak container that had
just been recreated and answered its warm-up window with
unauthorized_client. Adopt the same guard web-app-keycloak's own login
uses: resolve the cid with --filter health=healthy, wait for the
container to report healthy, and retry the kcadm login.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:39 +02:00
kevinveenbirkenbachandClaude Fable 5 3a2ef3da01 fix(nfs): soft client mounts for the github CI runtime
The swarm labs run NFS server and clients on one shared kernel; with
hard mounts a writeback stall wedges containers in D-state and takes
the whole DinD node with it (the bbb/matomo and nextcloud wedges).
dev/act already mount soft for exactly this reason; github gets the
same lab profile. Production (host) keeps hard mounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:39 +02:00
kevinveenbirkenbachandClaude Fable 5 42ff8deb20 fix(svc-storage-nfs-server): default to the CI-tested ganesha flavor
kernel nfsd loopback-deadlocks in the shared-kernel DinD labs (reclaim
-> writeback -> the same nfsd), wedging the heaviest labs per run, so
CI can only ever cover ganesha. Make ganesha the single default in
meta/services.yml and drop the per-variant RUNTIME switching; kernel
remains available as a deliberate, untested per-inventory override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:39 +02:00
Alejandro Roman fbb4e13265 fix issues with Gitlab 2026-07-12 21:12:50 +02:00
kevinveenbirkenbachandClaude Fable 5 bf0f79b0e6 chore(claude): session permission updates
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:54 +02:00
kevinveenbirkenbachandClaude Fable 5 e2704ffd71 refactor(svc-bkp-volume-2-local)!: resolve baudolo args via backup_* lookups
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>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 4a23e0cb09 feat(services): declare locally-built images via services.<key>.custom
The custom flag moves from 33 hardcoded template kwargs into the merged
service config: 'custom: true' names the shared <entity>_custom build,
a string names a second distinct '<string>_custom' image inside one app
(nextcloud whiteboard). The image lookup reads the declaration as a
fallback, so config consumers (e.g. backup image matching) resolve the
exact deployed reference from one declaration point. nextcloud cron now
runs the nextcloud_custom build like the app service instead of the
upstream image.

A new services lint couples the declaration to reality in both
directions: custom requires a role Dockerfile, and a role Dockerfile
requires a custom declaration. The eight legacy roles deploying local
builds under upstream tags or ad-hoc image= overrides are ratcheted via
'# nocheck: dockerfile-custom'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 e917f0860d refactor(lookups): split bare-ref image resolver out of container_image
New 'image' lookup is the SPOT for resolving <registry>/<image>:<version>
(swarm registry prefix in swarm mode, bare in compose; custom= accepts a
string base for a second distinct custom image inside one app).
container_image becomes a thin wrapper that forwards terms+kwargs and adds
the compose image: "<ref>" line, so all existing template call sites keep
their output byte-identical. The lookup-usage integration test now counts
lookup_loader.get() delegation as production usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 14844ccf8f chore(claude): allow WebFetch docs.rs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 214016fc9b fix(swarm-tests): bound the Playwright collect so cleanup always runs
A wedged manager (act-swarm containerd stall) made the docker exec tar
block up to 900s; on a cancelled job that ate the runner's grace window
and the always() Cleanup step never ran, leaking the DiD cluster. Probe
the manager first (10s) and cap the copy at 120s with --kill-after so a
hung exec is force-reaped; teardown then always follows. Reports stay
collected before teardown, since they live inside the manager node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 781e787550 fix(opentalk): grant the embedded seaweedfs sidecar its consumer identity
The OpenTalk controller v0.34.1 verifies its bucket via S3 ListBuckets
membership, but SeaweedFS hides every bucket from an unconfigured
identity (confirmed identical on 4.38 and 4.39), and the embedded sidecar
started with no s3.config, so the controller's signed request got an
empty bucket list and crash-looped on 'bucket missing' although the
bucket existed. The sidecar now runs s3.configure to register the
consumer's access key with Read/Write/List/Tagging on its bucket, the
same grant the central provider gets from its mounted s3.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 9c859709b4 refactor(swarm-tests): deploy the backup host from its own inventory
The backup node no longer rides in the cluster inventory: extend_inventory
writes a sibling backup.yml (same host_vars dir) holding only the
svc-bkp-remote-2-local and svc-bkp-local-2-device groups, and the matrix
deploys it as a second play after the cluster converges, right before the
DR drill. Without a svc-swarm-node group that play resolves
DEPLOYMENT_MODE=compose and IS_STACK_HOST=true on the backup host, so the
backup roles and their alarm/cleanup units (cln-faild-bkps, alm-compose)
deploy without cluster-membership special cases; the interim group-gate
on sys-ctl-cln-faild-bkps is gone again. A stack host that should also
pull backups joins the bkp groups of its own cluster inventory instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:31:39 +02:00
kevinveenbirkenbachandClaude Fable 5 4666f59afc fix(swarm-tests): mint a unique machine-id per lab node
The node image baked one machine-id into every container, so the pull
target directory backup/<machine-id>/ was shared by all providers and
the per-provider rsyncs raced each other with --delete (DR drill step
4/9, run 29186471592 job 86634020663). Emptying /etc/machine-id makes
systemd generate a distinct id on first boot; real hosts always had
their own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:31:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0302a6fe1f fix(mail,moodle): distro-independent SMTP CA trust + TLS-aware SSLPROXY
msmtprc's tls_trust_file is now overridable per consumer: the moodle and
wordpress containers are Debian-based, so the host-distro default CA path
does not exist inside them; both pin /etc/ssl/certs/ca-certificates.crt.

MOODLE_SSLPROXY was hardcoded true, which 500s behind the http-only onion
vhost ("Must use https address in wwwroot when ssl proxy enabled"); derive
it from the central TLS lookup like the other roles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:14 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0039602379 feat(seaweedfs): extract browser frontend into a gated service
SeaweedFS is a server-side S3 backend for its consumers; only the
filer/master admin UIs are browser-facing. Split them into a `frontend`
service that stays off on onion nodes ('svc-net-tor' in group_names):

- frontend disabled: no nginx proxy container, no filer/master vhosts,
  no oauth2 SSO sidecar; the S3 API vhost is unaffected
- stale filer/master vhosts from earlier frontend-enabled deploys are
  removed so the site-wide CSP health probe stops hitting dead domains
- csp_skip_domains additionally skips canonicals a disabled service
  declares via services.<name>.domains (defense in depth)
- UI personas (admin/biber/guest/storage-buckets) gate on the
  FRONTEND_SERVICE_ENABLED flag; tor variants dropped (backend role)

Verified on the onion node: 4/4 UI specs skip, S3 API reachable, CSP
health check green, matrix S3 media consumer unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0a581f6622 feat(matrix): full onion support — OIDC backchannel, MXIDs, Tor-stable specs
Synapse on an onion node could never complete SSO: it has no Tor, so the
http://auth.<onion> issuer was unresolvable server-side (discovery/jwks/
token), and Synapse hard-rejects non-https issuers on top.

- runner/hostpin.{sh,service}: pin the onion auth host to the runner's real
  default gateway (node OpenResty) from /proc/net/route, Before=docker.service
  so nested containers see it from their first request; host-gateway is
  unusable in nested DinD (resolves to the inner docker0)
- Synapse bind-mounts the runner's /etc/hosts; token iss stays the onion
  issuer the browser uses
- bootstrap.service: PassEnvironment (systemd units do not inherit container
  env; MATRIX_MDAD_PLAYBOOK_TAGS only worked via its fallback default)
- vars.yml.j2: skip_verification on onion (Synapse's escape hatch for
  issuers without https; Tor provides transport security per RFC 7686)
- 02_render_inventory: drop the MDAD bootstrap marker + bounce the runner on
  inventory changes — mode=update reconfigures the nested stack at all now
- MATRIX_SERVER_NAME follows matrix_domain onto the onion so MXIDs built from
  it (DM spec, bridge configs) reference users that exist
- specs: shared Firefox onion fixture (16 files), gotoElementApp reload-retry
  against Element's Tor-triggered init race, seaweedfs spec gates on the
  filer frontend, env uses canonical_url consumer=

Verified in-container: SSO 302 to onion Keycloak, admin+biber OIDC login,
E2EE DM admin<->biber, CSP — all green over Tor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5abfcfc6d6 feat(e2e): shared Tor-aware Playwright harness
Promote the Firefox onion fixture (secure-context .onion prefs, SOCKS
remote-DNS) from web-app-dashboard to test-e2e-playwright as the single
source of truth; the dashboard copy now re-exports it. Stage the shared
fixture on all three paths (deploy runner, lint-playwright, rerun-spec).

gotoOnion: onion navigations default to waitUntil=domcontentloaded —
heavy chunked SPAs (Element) serialise 30+ bundle requests over Tor, so
the load event regularly exceeds the navigation cap; callers' explicit
onion-scaled selector waits cover app boot instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 be1a22661a feat(lookup): central onion-aware canonical_url resolver + enforcement
lookup('canonical_url', app_id[, key][, consumer=app]) resolves the web base
URL for an app or one of its named canonicals (element/synapse/filer),
following the onion-injected merged-domain map: .onion targets resolve to
http (Tor provides transport security), everything else follows TLS_ENABLED.
The consumer= kwarg returns '' unless the consuming app has the target's
service binding enabled, replacing the recurring
"(lookup(...) if lookup('config', app, 'services.X.enabled') else '')"
template branching.

- utils/cache/domains.py: inject the node onion into dict-form
  (named-canonical) domain declarations, not just list/str forms
- keycloak redirect_uris: onion redirect URIs are http
- 13 playwright.env.j2: manual scheme branching -> canonical_url(consumer=)
- lint: forbid manual '://' concatenation and to_onion_url in role templates
- unit tests: 10 cases (primary/named x clearnet/onion, consumer gating)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bbb1220d7d feat(web-svc-mirror): first-party privacy proxy for third-party assets, coupled to tor
New web-svc-mirror role: an nginx vhost that proxies and caches exactly
the external asset hosts aggregated from all deployed applications' CSP
whitelists (external_asset_origins filter) under first-party paths
(/<origin-host>/...), refusing every other upstream. svc-net-tor
consumes the service, so any tor deployment automatically serves the
mirror on its onion.

For apps with services.tor.enabled the shared body filter rewrites
external asset URLs in HTML to the onion mirror (onion sessions only;
clearnet untouched) and the CSP builder allows the mirror origin in the
asset directives. The dashboard's keycloak-js vendoring is reverted:
onion sessions now load the adapter through the mirror, clearnet loads
jsdelivr directly as before.

Verified: variant=0 deploy green (failed=0), dashboard 14/14 over onion
with keycloak-js served through the mirror, mirror spec green, health
check accepts the 404 vhost root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f14b1ab3f6 fix(lint-playwright): stage role spec subdirs and addon-gating like the deploy does
The lint stager only copied top-level *.js, so role-local subdirs
(fixtures/, addons/) never reached the staged tree: the dashboard's new
fixtures/onion-test require failed, and addons/*.spec.js were silently
never linted. Mirror the test-e2e-playwright staging (copy subdirs plus
the shared addon-gating.js) so --list covers the full spec tree; addon
specs of 11 roles are now actually linted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5c6f656413 style(meta): normalize inline-comment spacing via autoformat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:12 +02:00
kevinveenbirkenbachandClaude Fable 5 f05b3f1c4f fix(swarm-tests): clear the nologin gate on the lab nodes
The DiD nodes boot degraded, so systemd never reaches the point where
systemd-user-sessions removes /run/nologin and pam blocked every
non-root ssh login with 'System is booting up'. The bootstrap play now
starts the unit on every node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 10:47:57 +02:00
kevinveenbirkenbachandClaude Fable 5 3c1a004647 fix(backup): make the remote pull chain deployable
On Debian the system user backup (uid 34) already exists with
/var/backups and nologin; without explicit home/shell the user module
left it untouched, the role wrote authorized_keys to /home/backup/.ssh
while sshd searched /var/backups/.ssh, and every remote pull died with
'Permission denied (publickey)'. Arch hosts never showed it because the
user did not pre-exist there.

svc-bkp-remote-2-local also no longer ExecStarts during the deploy
flush: the pull can only work once the operator has paired the ssh
identity on the providers, so the debug-mode smoke start failed every
swarm deploy carrying a backup host. Same system_service_suppress_flush
idiom as the volume, secrets and nfs sibling roles; the timer stays
enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 10:47:57 +02:00
kevinveenbirkenbach ec48ce2a7a Merge branch 'main' into feature/swarm-nfs 2026-07-12 09:44:02 +02:00
kevinveenbirkenbachandClaude Fable 5 a46f928986 feat(config): name the present keys when a schema entry is unset
A ConfigEntryNotSetError now lists the keys the merged application
actually carries, which distinguishes a defaults-only merge (inventory
overrides never arrived) from a genuinely missing entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:42:33 +02:00
kevinveenbirkenbachandClaude Fable 5 8ef6af928c fix(swarm-tests): give nfs and backup nodes worker-grade inventory and lab DNS
The provisioner copied the manager host_vars only to the workers, so the
service-loader preload on the nfs and backup nodes merged a defaults-only
applications dict (credentials lookups failed), and the bootstrap play
seeded /etc/hosts only on manager and nfs, so the backup node's msmtp
healthcheck died with EX_NOHOST. Both nodes now get the host_vars copy
and the app-domain hosts entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:42:22 +02:00
kevinveenbirkenbachandClaude Fable 5 4d7c288c30 fix(swarm-tests): route backup app overrides through the provisioner merge
The DR-drill applications block in swarm-nfs-extras.yml reached the
deploy as extra-vars; -e replaces the whole inventory applications dict,
so every generated credential vanished and the first schema-checked
lookup (svc-db-mariadb credentials) killed all swarm deploys. The block
now enters through the provisioner's host_vars merge (_bake_overrides
base_overrides), the deploy loads the applications-free
swarm-nfs-extras.deploy.yml twin, and the full extras file stays for the
drill, which reads the device paths from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:41:46 +02:00
kevinveenbirkenbachandClaude Fable 5 d523749373 fix(msmtp): require the nfs-client only on swarm members
The cluster-shared DIR_VAR_LIB assertion also fired on hosts outside the
swarm (the DR drill's backup node), which keep a local state dir by
design. Hosts not in svc-swarm-node pass without the nfs-client group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:41:32 +02:00
kevinveenbirkenbachandClaude Fable 5 d130f55596 fix(swarm): gate the post-deploy converge wait on the effective role mode
cdf544c35 switched the gate to compose_mode, but post_deploy.yml runs in
roleless include_tasks contexts where the sys-svc-compose defaults are
not in scope, so every compose deploy died with 'compose_mode is
undefined'. Use the compose_mode_force-or-DEPLOYMENT_MODE resolution the
lookup plugins and deploy.yml.j2 already apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:41:19 +02:00
kevinveenbirkenbachandClaude Fable 5 577185035c fix(sshd): stop trim_blocks glueing LogLevel to the next line
Ansible renders templates with trim_blocks, so the newline after the
inline {% endif %} was swallowed and the config shipped
'LogLevel INFO# Authentication:'; sshd -t rejects that line and every
restart left the host without ssh (the swarm test caught it because the
backup node pulls over ssh right after the rollout). Inline expression
instead of block tags, and the commented upstream example defaults are
gone; the rendered directives are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 09:41:06 +02:00
kevinveenbirkenbachandClaude Fable 5 edca61152e fix(swarm-tests): bound every bootstrap play task with a timeout
Each task carries a timeout sized to its I/O class (probes 10-30s,
service restart 60s, repo tar/unarchive 300s, .deb install 600s), so a
wedged node fails the play loudly instead of hanging the bootstrap step;
the systemd wait keeps its rescue diagnostics for exactly that case.
Bare integers instead of the timeout lookup because the standalone
-c docker run does not load repo plugins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:40:18 +02:00
kevinveenbirkenbachandClaude Fable 5 e634033ef2 refactor(ci): order debug collectors coarse to specific
On failure the docker-level rescue snapshot runs first and preserves the
runtime state before later steps touch it, then the lab diagnostics,
then inventory, then playwright; cleanup stays last. When the coarse
layer is already broken the finer collectors cannot add anything, so
they come after it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:40:18 +02:00
kevinveenbirkenbachandClaude Fable 5 5f51b1e4b3 fix(swarm-tests): write /etc/hosts and resolv.conf in place
Both files are bind mounts inside the node containers, so ansible's
atomic tempfile+rename fails with EBUSY and every node dropped out of
the bootstrap play. unsafe_writes lets the two tasks write in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:06:18 +02:00
kevinveenbirkenbachandClaude Fable 5 27ce239295 fix(swarm): never read a failed stack query as converged
swarm_stack_ready.sh piped 'docker stack services' through a process
substitution, so the timeout wrapper added in 8516e3167 turned a slow or
wedged manager into an empty listing, an empty not_running set and exit
0: the convergence gate would have gone green with zero running replicas.
Capture the listing, fail on a non-zero rc.

That exposed the second half: post_deploy gated the swarm wait on the
cluster-wide DEPLOYMENT_MODE, so forced-compose roles (matrix ansible
flavor, bigbluebutton) ran it against a stack that does not exist and
only passed because the old code swallowed the error. Gate it on
compose_mode, like the handlers already do.

teardown.sh: the NFS mountpoint and umount calls, the likeliest hang
sites, were the only ones left unwrapped, and the timeout around docker
inspect inverted the D-state warning (a hung inspect returned 124, the
if went false and the script reported a clean teardown). Warn on rc 0
and 124, and shrink the per-node budget so the script fits inside its
step timeout.

Swarm step budget: the bounded steps alone summed to 365 over a 360-min
cap, so cleanup was still starved. Deploy 240, diagnostics 15, playwright
15, rescue 25, cleanup 15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:06:18 +02:00
kevinveenbirkenbachandClaude Fable 5 d7e38514c9 fix(ci): bound every hang path that drove jobs into the 6h cancel
27 jobs of run 29147179243 burned their full 360-min budget. Three
systemic layers, all addressed here:

- The CLI e2e task ran the staged test.sh unbounded, and test.sh blocks
  in 'systemctl start' on a oneshot backup unit with no JobTimeout; on a
  wedged dockerd or a stalled NFS mount both freeze silently for hours.
  The task now runs the script under 'timeout TEST_E2E_CLI_TIMEOUT'
  (60 min via the timeout lookup, so TIMEOUT_FACTOR scales it) and the
  unit starts are bounded by BKP_TEST_HEALTH_TIMEOUT.
- The converge probe re-ran the full per-service diagnostics (service ps,
  service logs, inspect, journalctl) on EVERY retry, so 600 nominal
  2-second attempts cost 25-33s each and silently outlived the step
  timeout. The block is gone (the workflow-level container.py rescue
  snapshot already collects it recursively on failure) and every docker
  call is timeout-bounded. Same removal in the swarm collect script,
  which keeps only the NFS and render evidence container.py does not see.
- Playwright-collect, rescue diagnostics and Cleanup had no
  timeout-minutes, and teardown.sh calls docker against D-state nodes,
  so a failed job hung there for hours until the job cap killed it
  without artifacts. Every step is bounded now, every teardown call is
  timeout-wrapped, and the deploy steps (swarm 270, compose 300) leave
  room for diagnostics and cleanup below the 360-min cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 04:06:17 +02:00
kevinveenbirkenbachandClaude Fable 5 aaa50ab02d fix(espocrm): satisfy shellcheck in the entrypoint log tail
Replace the ls -t parse with a find/sort pipeline (SC2012) and the
A && B || C construct with a real if-else (SC2015): a failing tail no
longer prints the misleading '(no app log found)', and '|| true' keeps
set -eu from swallowing the intended exit code in the failure path.
Verified the pipeline in the espocrm:10.0.2 image including the
empty-directory case; blocked the pre-push shellcheck gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 e525afd47c chore(backup): bump backup-docker-to-local to 1.8.1
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>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 c54e1a1ff4 chore(gitignore): ignore .claude-* scratch files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 5af4b081df refactor(swarm-tests): move teardown and topology under utils, drop shell rescue collector
routine/08_cleanup.sh becomes utils/clean/teardown.sh and topology/
becomes utils/topology/ so routine/ holds only the sequenced flow; all
consumers (workflow steps, compose/swarm.yml env guards, relative
sources, README) follow. The shell rescue collector and its lint are
gone: utils/diagnostics/container.py covers compose and swarm
identically. extend_inventory gains the backup-host placements the DR
drill deploys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00