Commit Graph
9 Commits
Author SHA1 Message Date
kevinveenbirkenbach f27d8369d7 Merge branch 'main' of github.com:infinito-nexus/core into integration/store-i18n-merge
# Conflicts:
#	roles/web-app-akaunting/TODO.md
#	roles/web-app-funkwhale/meta/services.yml
#	roles/web-app-openproject/tasks/01_manager_ops.yml
2026-10-30 19:37:15 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b894db0a75 fix(svc-bkp-volume-2-local): restore into the resolved volume mountpoint
rsync 3.5.0 (Debian security update 3.5.0+ds1-0+deb13u1) refuses a
destination symlink owned by neither root nor the calling user.
svc-opt-ssd-hdd turns a volume's `_data` into a symlink to /mnt/hdd or
/mnt/ssd. The restore drill empties the volume and runs `compose create`;
docker repopulates it and lchowns the `_data` symlink to the image
directory's owner (e.g. www-data). The restore then failed with
"refusing to follow a symlink owned by an untrusted user" and
"change_dir#1 .../_data/ failed: Too many levels of symbolic links",
the drill left its subjects down, and the dependent checks timed out
(CI run 40979252521, ssd-hdd#0, on feature/store-i18n; recover.py is
identical on main).

VolumeRecovery now resolves the mountpoint before handing it to rsync:
os.path.realpath locally, `ssh <host> realpath -e -- <mountpoint>` for
--docker-host, so rsync writes into the real directory and never meets
the symlink. `--insecure-links` was not used because older rsync builds
on other distributions lack it. Two new unit tests (local symlinked
mountpoint, remote resolution); both fail against the previous code.

Root cause and fix by the i18n session (feature/store-i18n triage of
ssd-hdd#0), ported to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:51:44 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4fcf097e5b fix(svc-bkp-nfs-2-local): leave backup-disabled volumes out of the export snapshot
The NFS backup rsynced the whole export and honoured only one exclude, the
shared backup root. Volumes declared `backup: false` were copied anyway and
then pulled a second time by remote-2-local; in the swarm DR drill of
Magento#0 (run 40625679270) docs_sites alone (a full repo clone plus its
prepared and work trees) took about 15.5 GiB per copy and filled the
runner. The volume backup already skipped these through the backup_volume
lookup; only the NFS path ignored the declaration.

- utils/storage/nfs.py: volume_dir() names the state directory a volume
  binds to (pinned name or key), shared with compose_volumes;
  backup_disabled_dirs() returns every NFS-backed `backup: false` volume
  dir that no backed-up volume also binds to.
- The role-local lookup backup_nfs_excludes feeds them into
  BKP_NFS_2_LOCAL_EXCLUDES next to the backup root; script.sh takes any
  number of excludes. Compose installs still pass only the backup root.
- recover.py protects every live state directory the snapshot does not
  hold, so `rsync --delete` keeps the excluded volumes (model weights,
  the docs tree, caches) and volumes created after the snapshot instead
  of wiping them.
- The swarm DR drill restores BACKUP_NFS_VOLUME, the first NFS volume
  not declared `backup: false`; PRIMARY_NFS_VOLUME stays for the drain
  tests. BookWyrm now drills bookwyrm_media; decidim, fusiondirectory,
  meta, coturn, svc-registry-cache and svc-registry-docker have nothing
  backed up on NFS and skip the drill.

Measured: compose_volumes renders identically before and after for 291
roles across compose/swarm x local/nfs (1940 of 1940); the 15 excluded
directories are exactly the backup-disabled device dirs; the systemd unit
renders shell-safe on ansible-core 2.20 and 2.21 and stays byte-identical
in compose and swarm-local. A new unit test runs the real script.sh over
two generations and the real recover.py back into the export, and fails
without the restore protection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:43:31 +01:00
kevinveenbirkenbach 396c937033 Merge branch 'main' into feature/store-i18n
# Conflicts:
#	cli/build/docs/integration_matrix/__init__.py
#	tests/lint/ansible/roles/meta/__init__.py
#	utils/github/variant/axes.py
2026-10-21 00:09:53 +02:00
kevinveenbirkenbachandClaude Opus 5.5 6e776a5780 refactor(python): clear the open CodeQL code-quality alerts
- Declare `__all__` in every package `__init__.py` whose `PROJECT_ROOT`
  is imported by its modules (163 packages), so the constant counts as
  exported; list every public name where the package defines more.
- Empty the 15 package `__init__.py` files whose `PROJECT_ROOT` no module
  imports.
- Declare the public API of `utils.github.variant.axes` in `__all__`,
  re-exports included, instead of silencing them with `noqa: F401`; the
  list matches the one `feature/store-i18n` already carries.
- Use `import unittest.mock as mock` in `test_applications.py`, as the
  rest of the suite does.
- Spell `_NON_PRINTABLE` in `utils/yaml_bootstrap.py` as the equivalent
  BMP-only positive class; it matches exactly the same 2111 code points
  as the old negated class across U+0000..U+10FFFF.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 21:40:58 +02:00
kevinveenbirkenbach d0b45f8b2d Merge branch 'main' of github.com:infinito-nexus/core into feature/store-i18n
# Conflicts:
#	.github/workflows/call-images-build-ci.yml
#	.github/workflows/call-test-deploy.yml
#	.github/workflows/call-test-install-pkgmgr.yml
#	.github/workflows/call-test-workspace.yml
#	roles/svc-bkp-local-2-device/README.md
#	roles/svc-storage-nfs-client/README.md
#	roles/web-app-pixelfed/meta/services.yml
#	scripts/tests/deploy/act/workflow.sh
#	tests/unit/python/utils/recovery/test_base.py
2026-10-20 20:05:15 +02:00
kevinveenbirkenbachandClaude Opus 5.5 21eba1e88f fix(backup): carry hard links, ACLs and xattrs on every backup and recover hop
baudolo 7.1.x writes volume generations with `rsync -aHAX`, so the
producing host keeps `user.*` extended attributes (OpenCloud keeps its
storage metadata in `user.oc.*`). Every later copy dropped them again,
because none of the other rsync calls passed `-X` (nor `-A`/`-H`):

- NFS export snapshots (svc-bkp-nfs-2-local `script.sh`),
- the backup host's pull (svc-bkp-remote-2-local `pull_specific_host.py`),
- the device sync (svc-bkp-local-2-device `script.py`),
- every recover into a live target (`utils/recovery/base.py`, used by the
  volume, NFS and device recover scripts),
- the `recover full` and `recover remote` tree pulls.

A disaster recovery through the documented chain therefore restored
OpenCloud without `user.oc.*`, silently: the copies succeeded and the
dirval stamp does not cover attributes.

All of these now run `rsync -aHAX` (`-aHAXP` for the device sync). A
target filesystem without xattr support makes rsync exit non-zero as soon
as a source file carries attributes, so the hop fails loudly instead of
dropping them; there is no per-device opt-out. The single-image pull in
`recover remote` keeps `-a`. The provider's ssh-wrapper needs no change:
its glob accepts any server flags and its character guard allows them.

Two tests write a `user.oc.id` attribute and assert it survives the NFS
snapshot and the recover; with the old `-a` both fail with
`OSError: [Errno 61] No data available`.

Closes #1021

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 18:20:46 +02:00
kevinveenbirkenbachandClaude Opus 5 b033aab42b fix(backup): carry extended attributes through the restore
A recover mirrored a snapshot with rsync -a, which transfers no extended attributes, so a restored volume came back byte-identical while every application that keeps identity in xattrs read the tree as unknown. OpenCloud posixfs is the case that surfaced it: after the DR drill its storage log reports node.Xattrs on the platform administrator space as no data available, the showcase files are assimilated as new objects, and the graph call that lists drives returns no personal drive, so the design spec dereferences undefined. Measured here, source tagged with user.oc.name, on tmpfs and on ext4 alike: rsync -a returns 0 and drops the attribute, rsync -aHX and rsync -aHAX return 0 and carry it. The shared restore flow and the NFS half of this repo now use -aHAX, the form ensure_subject_subvolume.sh already uses, continuing what 69321d4135 started for numeric ownership.

The backup half still has the same gap: backup-docker-to-local 7.0.2 builds its command as rsync -aP --no-D --delete --delete-excluded with no flag passthrough, so until it carries -X and the pin moves, a snapshot holds no attributes for this restore to put back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-17 14:16:29 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
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>
2026-08-22 01:31:09 +02:00