Merge remote-tracking branch 'upstream/main' into agent/i8-release-article

This commit is contained in:
evangelostsak committed 2026-10-21 11:15:15 +02:00
commit c71f5bc196
278 files changed
+1286 -171

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
self-hosted-runner:
labels:
- ubuntu-26.04
+1 -1
View File
@@ -43,7 +43,7 @@ concurrency:
jobs:
build:
name: 💿 Image
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: write
@@ -78,7 +78,7 @@ concurrency:
jobs:
sync:
name: 🪞 Mirror
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: write
+1 -1
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
lint:
name: 🧹 Lint
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 45
env:
INFINITO_LINT_RUNNER: host
+2 -2
View File
@@ -343,7 +343,7 @@ jobs:
- test-deploy-chunk-2
- test-deploy-chunk-3
if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
issues: write
actions: read
@@ -374,7 +374,7 @@ jobs:
- lint
- security-codeql
if: ${{ always() }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: 🛑 Fail if any upstream job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
+2 -2
View File
@@ -30,7 +30,7 @@ env:
jobs:
check-ci-images:
name: 🔎 Check CI images
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
missing: ${{ steps.check.outputs.missing }}
ci_tag: ${{ steps.citag.outputs.ci_tag }}
@@ -123,7 +123,7 @@ jobs:
needs.build-ci-images-if-missing.result == 'success' ||
needs.build-ci-images-if-missing.result == 'skipped'
)
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: write
+7 -2
View File
@@ -163,7 +163,7 @@ env:
jobs:
discover:
name: 🔍 Chunk ${{ inputs.index }}${{ inputs.marker }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 60
outputs:
apps: ${{ steps.discover.outputs.apps }}
@@ -251,7 +251,7 @@ jobs:
deploy:
name: "${{ matrix.label }}"
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 355
needs: discover
@@ -308,6 +308,11 @@ jobs:
shell: bash
run: ./scripts/github/runner/relocate_docker_dataroot.sh
- name: 🛡️ Tear down host AppArmor profiles (dev-host parity)
if: ${{ !env.ACT }}
shell: bash
run: make security-apparmor-teardown
- name: 🔍 Resolve repository owner
shell: bash
run: |
+6 -1
View File
@@ -23,7 +23,7 @@ permissions:
jobs:
dns:
name: 🌐 DNS
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 30
steps:
- name: 🧹 Free disk space
@@ -41,6 +41,11 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🛡️ Tear down host AppArmor profiles (dev-host parity)
if: ${{ !env.ACT }}
shell: bash
run: make security-apparmor-teardown
- name: 🔑 Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
+1 -1
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
test-install-make:
name: 🛠️ Makefile
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 30
env:
NIX_CONFIG: |
@@ -10,7 +10,7 @@ permissions:
jobs:
test-install-pkgmgr:
name: 📦 PKGMGR
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 30
env:
NIX_CONFIG: |
+7 -2
View File
@@ -21,7 +21,7 @@ permissions:
jobs:
prepare:
name: 🧑‍💻 Workspace Matrix
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 5
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
@@ -44,7 +44,7 @@ jobs:
test-workspace:
name: ${{ matrix.icon }} Workspace (${{ matrix.label }})
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 345
needs: prepare
@@ -88,6 +88,11 @@ jobs:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🛡️ Tear down host AppArmor profiles (dev-host parity)
if: ${{ !env.ACT }}
shell: bash
run: make security-apparmor-teardown
- name: "📥 Load the rescue paths into job env"
shell: bash
run: |
+1 -1
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
test:
name: 🧪 Test
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 45
env:
INFINITO_TEST_RUNNER: host
+3 -3
View File
@@ -84,7 +84,7 @@ permissions:
jobs:
validation:
name: ✅ Validation
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout the calling ref for its tooling
uses: actions/checkout@v7.0.0
@@ -148,7 +148,7 @@ jobs:
wait-fork-prereq-run:
name: ⏳ Wait for fork run
if: ${{ inputs.fork_prereqs }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
env:
WAIT_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && !inputs.trusted_pr }}
steps:
@@ -194,7 +194,7 @@ jobs:
name: 🚦 Gate Dependencies
needs: wait-fork-prereq-run
if: ${{ !cancelled() && inputs.fork_prereqs }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7.0.0
- name: ✅ Assert privileged prerequisites are ready
+3 -3
View File
@@ -12,7 +12,7 @@ permissions:
jobs:
stale:
name: 🏷️ Close inactive issues and pull requests
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
issues: write
pull-requests: write
@@ -45,7 +45,7 @@ jobs:
delete-stale-branches:
name: 🌿 Delete inactive branches
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: write
issues: write
@@ -65,7 +65,7 @@ jobs:
cleanup-ghcr-ci-images:
name: 📦 Delete old GHCR CI images
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: write
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
benchmark-collection-sources:
name: ⚖️ Time Galaxy against the git fallback
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
env:
HISTORY_PATH: build/collection-source-history.json
steps:
+1 -1
View File
@@ -22,7 +22,7 @@ concurrency:
jobs:
cleanup:
name: 🧹 Delete old ci-* image versions
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
actions: read
contents: read
+1 -1
View File
@@ -61,7 +61,7 @@ concurrency:
jobs:
sync:
name: 🪞 Mirror
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: write
+1 -1
View File
@@ -19,7 +19,7 @@ env:
jobs:
check-and-trigger:
name: 🔎 Backfill highest missing release tag
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: read
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
runs-on: ubuntu-26.04
permissions:
security-events: write
packages: read
+1 -1
View File
@@ -16,7 +16,7 @@ permissions:
jobs:
fuzz-yaml-bootstrap:
name: 🧬 Fuzz the bootstrap YAML reader
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
timeout-minutes: 30
steps:
- name: ⬇️ Checkout
@@ -26,7 +26,7 @@ permissions: read-all
jobs:
analysis:
name: 🔍 Scorecard analysis
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
permissions:
+47 -5
View File
@@ -21,7 +21,7 @@ jobs:
update-docker-image-versions:
name: 🐳 Update Docker image versions
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
@@ -68,7 +68,7 @@ jobs:
update-version-sources:
name: 🔖 Update declared version sources
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
@@ -115,7 +115,7 @@ jobs:
update-pip-pins:
name: 🐍 Update pinned pip requirements
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
@@ -162,7 +162,7 @@ jobs:
update-galaxy-pins:
name: 🌌 Update pinned Ansible collections
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
@@ -209,7 +209,7 @@ jobs:
update-repository-refs:
name: 🌿 Update repository refs
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
@@ -252,3 +252,45 @@ jobs:
UPDATE_PR_BODY: "Automated update of semver-based git `ref:` values in `roles/*/meta/services.yml`. Upstream tags resolved via `git ls-remote --tags`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open/role_prs.sh roles
update-runner-labels:
name: 🏃 Update GitHub-hosted runner labels
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-26.04
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: 🔄 Update runner labels
run: bash scripts/github/update/runner_labels.sh
- name: 🔎 Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh .github/workflows
- name: ⚙️ Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: 📂 Open PR if runner labels changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/runner-labels
UPDATE_COMMIT_MESSAGE: "update(ci): raise GitHub-hosted runner labels"
UPDATE_PR_TITLE: "update(ci): raise GitHub-hosted runner labels"
UPDATE_PR_BODY: "Automated raise of every `runs-on: ubuntu-NN.NN` in `.github/workflows/` to the newest GA x64 Ubuntu image listed in https://github.com/actions/runner-images. Its CI run is the proof that the stack works on the new image."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open/pr.sh .github/workflows
@@ -27,7 +27,7 @@ jobs:
cancel-branch-runs:
name: 🚫 Cancel superseded branch runs
if: ${{ github.event_name == 'push' && (github.ref_name != 'main' || (github.ref_name == 'main' && startsWith(github.event.head_commit.message, 'Release version '))) }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
actions: write
contents: read
@@ -52,7 +52,7 @@ jobs:
cancel-pull-request-runs:
name: 🚫 Cancel superseded pull request runs
if: ${{ github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
actions: write
contents: read
+1 -1
View File
@@ -20,7 +20,7 @@ permissions:
jobs:
cancel-branch-runs:
if: ${{ github.event.ref_type == 'branch' }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
actions: write
contents: read
@@ -40,7 +40,7 @@ permissions:
jobs:
cleanup:
name: 🗑️ Delete private GHCR mirror packages
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
packages: write
@@ -29,7 +29,7 @@ jobs:
detect-pr-scope:
name: 🔎 Detect PR scope
if: ${{ github.event.pull_request.draft == false && (github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
scope: ${{ steps.resolve.outputs.scope }}
run_ci_orchestrator: ${{ steps.resolve.outputs.run_ci_orchestrator }}
@@ -59,7 +59,7 @@ jobs:
- detect-pr-scope
- validation
if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'false' }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ✅ Confirm lightweight PR scope
shell: bash
@@ -73,7 +73,7 @@ jobs:
- detect-pr-scope
- validation
if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
whitelist: ${{ steps.resolve.outputs.whitelist }}
steps:
@@ -124,7 +124,7 @@ jobs:
- detect-pr-scope
- validation
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
checkout_ref: ${{ steps.out.outputs.checkout_ref }}
image_tag: ${{ steps.out.outputs.image_tag }}
@@ -15,7 +15,7 @@ permissions:
jobs:
cancel-pr-runs:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
actions: write
contents: read
@@ -19,7 +19,7 @@ jobs:
close-when-disabled:
name: 🚫 Close Dependabot PR while gated
if: github.event.pull_request.user.login == 'dependabot[bot]' && vars.CI_ENABLE_AUTO_UPDATES != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: 📪 Close PR
env:
+5 -5
View File
@@ -21,7 +21,7 @@ permissions:
jobs:
run-policy:
name: 🧭 Decide CI policy
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
permissions:
contents: read
pull-requests: read
@@ -59,7 +59,7 @@ jobs:
name: ⏭️ Skip CI
needs: run-policy
if: needs.run-policy.outputs.should_run != 'true'
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: ⏭️ Explain skip
shell: bash
@@ -72,7 +72,7 @@ jobs:
if: needs.run-policy.outputs.should_run == 'true'
needs: run-policy
name: 🏷️ Detect release tag
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
is_version: ${{ steps.pick.outputs.is_version }}
version_tag: ${{ steps.pick.outputs.version_tag }}
@@ -91,7 +91,7 @@ jobs:
if: needs.run-policy.outputs.should_run == 'true'
name: ⭐ Resolve new-role priority
needs: run-policy
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
priority: ${{ steps.resolve.outputs.priority }}
steps:
@@ -132,7 +132,7 @@ jobs:
- run-policy
- detect-release
- ci-orchestrator
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
outputs:
allowed: ${{ steps.guard.outputs.allowed }}
steps:
+54
View File
@@ -1,5 +1,59 @@
# Changelog
## [16.2.1] - 2026-10-20
Fixed:
- Cache: the package cache frontend starts again; 16.2.0 broke its nginx
- Snipe-IT: SSO login works again on v8.8.1 behind oauth2-proxy
- Snipe-IT: the SSO header is only trusted with the proxy's shared secret
- Wazuh: seeded bcrypt salts no longer leak a fast password digest
Version jumps (net since 16.2.0):
- *web-app-socialhome*: 2026.10.17 to 2026.10.20
Upgrade notes:
- Snipe-IT: re-run *infinito administration inventory provision* once
Contributors:
- [Kevin Veen-Birkenbach](https://veen.world): cache, Snipe-IT SSO, bcrypt
## [16.2.0] - 2026-10-20
Added:
- Compose: *make compose-network-recreate* rebuilds the stack network safely
- CI: the updater keeps the GitHub-hosted runner labels current
Fixed:
- Backup: hard links, ACLs and xattrs survive every backup and recover hop
- NFS client: clients on Linux 6.19+ no longer fail writes on ganesha < 9.11
- AppArmor: the teardown keeps docker-default, so containers keep starting
- Wazuh: hosts can be put into WireGuard meshes like every other role
- CI: GitHub-hosted runners are pinned to ubuntu-24.04
Version jumps (net since 16.1.0):
- *svc-db-rabbitmq*: 4.3-management-alpine to 4.4-management-alpine
- *web-app-n8n*: 2.44.4 to 2.45.0
- *web-app-pixelfed*: 20261019 to 20261020
- *web-app-snipe-it*: v8.8.0-alpine to v8.8.1-alpine
- *web-app-taiga*: RabbitMQ 4.3-management-alpine to 4.4-management-alpine
- *eslint* (dev): ^10.12.0 to ^10.13.0
Upgrade notes:
- Backup: a target without xattr support now fails instead of losing them
- Dev: worktree hosts apply the IP pool with *make compose-network-recreate*
Contributors:
- [Kevin Veen-Birkenbach](https://veen.world): backups, NFS, AppArmor, CI
## [16.1.0] - 2026-10-19
Added:
+5
View File
@@ -227,6 +227,11 @@ compose-inner-run:
compose-inventory-refresh:
@bash scripts/tests/deploy/local/reset/inventory.sh
.PHONY: compose-network-recreate
# Recreate the stack's default network after an IPAM change, keeping volumes and reattaching foreign stacks.
compose-network-recreate: install
@"$${PYTHON}" -m cli.administration.deploy.development recreate
.PHONY: compose-playwright
# Rerun a role-local Playwright spec against the live running stack (no redeploy).
# Usage: make compose-playwright role=<role> [pw="--grep <pattern>"] [keep=true]
+1
View File
@@ -27,6 +27,7 @@ alias i8co2='m console'
alias i8coape='m compose-app-exec'
alias i8codo='m compose-down'
alias i8cone='m compose-entity-purge'
alias i8conere='m compose-network-recreate'
alias i8coni='m compose-inner-run'
alias i8conire='m compose-inventory-refresh'
alias i8copa='m compose-playwright'
+2
View File
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[1]
__all__ = ["PROJECT_ROOT"]
@@ -53,6 +53,7 @@ def _build_parser(base_pkg: str) -> argparse.ArgumentParser:
"run",
"exec",
"logs",
"recreate",
]
for _, mod in _iter_command_modules(base_pkg, command_names):
@@ -0,0 +1,74 @@
from __future__ import annotations
import argparse
import json
import subprocess
from .common import make_compose
from .up import handler as up_handler
def foreign_endpoints(rows: str, project: str) -> list[str]:
"""Return the running containers of other projects on the network.
Args:
rows: ``docker ps`` lines of ``<name>\\t<compose project>``.
project: compose project that owns the network.
"""
names = []
for row in rows.splitlines():
name, _, owner = row.partition("\t")
if name and owner != project:
names.append(name)
return names
def _docker(*args: str, capture: bool = False) -> str:
result = subprocess.run(
["docker", *args], check=True, capture_output=capture, text=True
)
return result.stdout if capture else ""
def add_parser(sub: argparse._SubParsersAction) -> None:
p = sub.add_parser(
"recreate",
help="Recreate the default network, keeping volumes and foreign stacks.",
)
p.set_defaults(_handler=handler)
def handler(args: argparse.Namespace) -> int:
compose = make_compose()
rendered = compose.run(["config", "--format", "json"], capture=True).stdout
config = json.loads(rendered)
project = config["name"]
network = config["networks"]["default"]["name"]
rows = _docker(
"ps",
"--filter",
f"network={network}",
"--format",
'{{.Names}}\t{{.Label "com.docker.compose.project"}}',
capture=True,
)
detached = foreign_endpoints(rows, project)
for name in detached:
print(f">>> Detaching {name} from {network}")
_docker("network", "disconnect", network, name)
rc = 1
try:
compose.run(["down", "--remove-orphans"])
rc = up_handler(argparse.Namespace(skip_entry_init=False, when_down=False))
finally:
if rc != 0 and detached:
print(f">>> Still detached from {network}: {' '.join(detached)}")
if rc != 0:
return rc
for name in detached:
print(f">>> Attaching {name} to {network}")
_docker("network", "connect", network, name)
return 0
@@ -3,3 +3,5 @@ from __future__ import annotations
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
+1 -1
View File
@@ -78,7 +78,7 @@ expanding `volume` per docker volume.
## Safety
Each recover mirrors a snapshot with `rsync -a --delete` and first runs the
Each recover mirrors a snapshot with `rsync -aHAX --delete` and first runs the
role's backup unit as a rollback point. `--no-safety-backup` skips that
(only when the target holds nothing worth saving: fresh host / empty /
disposable). `--preview` prints the exact commands without running them.
+1 -1
View File
@@ -72,7 +72,7 @@ def plan(root: Path) -> tuple[list[tuple[str, str]], list[str]]:
def _pull_cmd(source: str) -> list[str]:
remote = source if ":" in source else f"{source}:{paths.BACKUP_ROOT}"
return ["rsync", "-a", f"{remote}/", f"{PULL_STAGE}/"]
return ["rsync", "-aHAX", f"{remote}/", f"{PULL_STAGE}/"]
def _is_device(source: str) -> bool:
+1 -1
View File
@@ -47,7 +47,7 @@ def commands(
return (
[
["ssh", host, shlex.join(remote)],
["rsync", "-a", f"{host}:{REMOTE_ROOT}/", f"{LOCAL_ROOT}/"],
["rsync", "-aHAX", f"{host}:{REMOTE_ROOT}/", f"{LOCAL_ROOT}/"],
],
LOCAL_ROOT,
)
+1 -1
View File
@@ -58,4 +58,4 @@ flags:
skip the pre-recover safety backup of the current target
(only when it holds nothing worth saving: fresh host / empty / disposable)
mechanism: "rsync -a --delete, preceded by a pre-recover safety backup by default"
mechanism: "rsync -aHAX --delete, preceded by a pre-recover safety backup by default"
@@ -10,3 +10,5 @@ from __future__ import annotations
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
+9
View File
@@ -4,6 +4,15 @@ from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[3]
__all__ = [
"PROJECT_ROOT",
"build_mappings",
"find_roles",
"main",
"output_graph",
"process_role",
]
from . import __main__ as _main # noqa: E402
build_mappings = _main.build_mappings
@@ -3,3 +3,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
@@ -3,3 +3,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
@@ -3,3 +3,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
+2
View File
@@ -3,3 +3,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
@@ -3,3 +3,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
@@ -0,0 +1,7 @@
"""CLI entry point package for the GitHub-hosted runner label update."""
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
from utils.update.fetch import document
from utils.update.runner import IMAGES_README, bump_workflows, newest_ubuntu_label
from . import PROJECT_ROOT
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Raise every workflow's runs-on Ubuntu label to the newest GA "
"GitHub-hosted x64 image."
)
)
parser.add_argument(
"--repo-root",
type=Path,
default=PROJECT_ROOT,
help="Repository root whose .github/workflows are updated",
)
args = parser.parse_args()
label = newest_ubuntu_label(document(IMAGES_README))
if label is None:
print(f"No GA x64 Ubuntu label found in {IMAGES_README}.")
return 1
repo_root = args.repo_root.resolve()
changed = bump_workflows(repo_root / ".github" / "workflows", label)
if not changed:
print(f"Every workflow already runs on {label} or newer.")
return 0
print(f"Raised runs-on to {label} in:")
for path in changed:
print(f"- {path.relative_to(repo_root)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -3,3 +3,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[4]
__all__ = ["PROJECT_ROOT"]
+2
View File
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[2]
__all__ = ["PROJECT_ROOT"]
+2
View File
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[3]
__all__ = ["PROJECT_ROOT"]
+2
View File
@@ -9,3 +9,5 @@ resulting `.env` at the repo root. Implementation lives in
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[3]
__all__ = ["PROJECT_ROOT"]
@@ -1,7 +1,7 @@
from __future__ import annotations
__all__: list[str] = []
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[5]
__all__ = ["PROJECT_ROOT"]
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[6]
__all__ = ["PROJECT_ROOT"]
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[6]
__all__ = ["PROJECT_ROOT"]
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[6]
__all__ = ["PROJECT_ROOT"]
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[5]
__all__ = ["PROJECT_ROOT"]
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[5]
__all__ = ["PROJECT_ROOT"]
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
__all__: list[str] = []
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[5]
__all__ = ["PROJECT_ROOT"]
+2
View File
@@ -5,6 +5,8 @@ from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[3]
__all__ = ["PROJECT_ROOT", "detect_runtime"]
def detect_runtime() -> str:
"""
+3 -3
View File
@@ -49,11 +49,11 @@ Some CI failures (most notably OOM-kills such as [#162](https://github.com/infin
### GitHub-Hosted Runner Specs
All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-latest`, which maps to the GitHub-hosted standard Linux runner. The effective ceiling depends on the repository's visibility and plan:
All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-26.04`, the GitHub-hosted standard Linux runner pinned to one Ubuntu release. GitHub still refreshes that image weekly. The `🏃 Update GitHub-hosted runner labels` job of [cron-update.yml](../../../../.github/workflows/cron-update.yml) opens a pull request when a newer GA x64 Ubuntu image is listed in [actions/runner-images](https://github.com/actions/runner-images), and `tests/lint/repository/test_workflow_runner_pinned.py` rejects `*-latest` labels. The effective ceiling depends on the repository's visibility and plan:
| Repository / plan | vCPU | RAM | SSD |
|------------------------------------------|------|--------|-------|
| Public repo (this repo, `ubuntu-latest`) | 4 | 16 GB | 14 GB |
| Public repo (this repo, `ubuntu-26.04`) | 4 | 16 GB | 14 GB |
| Private repo on Free / Pro / Team | 2 | 7 GB | 14 GB |
| Private repo on Enterprise | 4 | 16 GB | 14 GB |
@@ -70,7 +70,7 @@ For the authoritative specs, see the upstream docs for [standard hosted runners]
Pick the profile that matches the runner you want to mimic:
```sh
# Public repo runner (ubuntu-latest, 4 vCPU / 16 GB): the default CI target
# Public repo runner (ubuntu-26.04, 4 vCPU / 16 GB): the default CI target
INFINITO_MEM_LIMIT=16g INFINITO_MEMSWAP_LIMIT=16g INFINITO_CPUS=4 make <target>
# Private repo runner on Free/Pro/Team (2 vCPU / 7 GB)
+4 -4
View File
@@ -10,7 +10,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.64.1",
"eslint": "^10.12.0",
"eslint": "^10.13.0",
"eslint-plugin-playwright": "^2.12.1",
"globals": "^17.13.0",
"stylelint": "^17.16.0"
@@ -848,9 +848,9 @@
}
},
"node_modules/eslint": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz",
"integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==",
"version": "10.13.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
"integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
"dev": true,
"license": "MIT",
"workspaces": [
+1 -1
View File
@@ -10,7 +10,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.64.1",
"eslint": "^10.12.0",
"eslint": "^10.13.0",
"eslint-plugin-playwright": "^2.12.1",
"globals": "^17.13.0",
"stylelint": "^17.16.0"
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=infinito-nexus
pkgver=16.1.0
pkgver=16.2.1
pkgrel=1
pkgdesc="Meta package for Infinito.Nexus host dependencies (dev + CI)."
arch=('any')
+58
View File
@@ -1,3 +1,61 @@
infinito-nexus (16.2.1-1) unstable; urgency=medium
Fixed:
- Cache: the package cache frontend starts again; 16.2.0 broke its nginx
- Snipe-IT: SSO login works again on v8.8.1 behind oauth2-proxy
- Snipe-IT: the SSO header is only trusted with the proxy's shared secret
- Wazuh: seeded bcrypt salts no longer leak a fast password digest
Version jumps (net since 16.2.0):
- *web-app-socialhome*: 2026.10.17 to 2026.10.20
Upgrade notes:
- Snipe-IT: re-run *infinito administration inventory provision* once
Contributors:
- [Kevin Veen-Birkenbach](https://veen.world): cache, Snipe-IT SSO, bcrypt
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 20 Oct 2026 22:35:05 +0200
infinito-nexus (16.2.0-1) unstable; urgency=medium
Added:
- Compose: *make compose-network-recreate* rebuilds the stack network safely
- CI: the updater keeps the GitHub-hosted runner labels current
Fixed:
- Backup: hard links, ACLs and xattrs survive every backup and recover hop
- NFS client: clients on Linux 6.19+ no longer fail writes on ganesha < 9.11
- AppArmor: the teardown keeps docker-default, so containers keep starting
- Wazuh: hosts can be put into WireGuard meshes like every other role
- CI: GitHub-hosted runners are pinned to ubuntu-24.04
Version jumps (net since 16.1.0):
- *svc-db-rabbitmq*: 4.3-management-alpine to 4.4-management-alpine
- *web-app-n8n*: 2.44.4 to 2.45.0
- *web-app-pixelfed*: 20261019 to 20261020
- *web-app-snipe-it*: v8.8.0-alpine to v8.8.1-alpine
- *web-app-taiga*: RabbitMQ 4.3-management-alpine to 4.4-management-alpine
- *eslint* (dev): ^10.12.0 to ^10.13.0
Upgrade notes:
- Backup: a target without xattr support now fails instead of losing them
- Dev: worktree hosts apply the IP pool with *make compose-network-recreate*
Contributors:
- [Kevin Veen-Birkenbach](https://veen.world): backups, NFS, AppArmor, CI
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 20 Oct 2026 21:11:50 +0200
infinito-nexus (16.1.0-1) unstable; urgency=medium
Added:
+53 -1
View File
@@ -1,5 +1,5 @@
Name: infinito-nexus
Version: 16.1.0
Version: 16.2.1
Release: 1%{?dist}
Summary: Meta package for Infinito.Nexus host dependencies
@@ -58,6 +58,58 @@ install -d %{buildroot}%{_docdir}/%{name}
%doc %{_docdir}/%{name}/DEPENDENCIES
%changelog
* Tue Oct 20 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 16.2.1-1
- Fixed:
- - Cache: the package cache frontend starts again; 16.2.0 broke its nginx
- - Snipe-IT: SSO login works again on v8.8.1 behind oauth2-proxy
- - Snipe-IT: the SSO header is only trusted with the proxy's shared secret
- - Wazuh: seeded bcrypt salts no longer leak a fast password digest
- Version jumps (net since 16.2.0):
- - *web-app-socialhome*: 2026.10.17 to 2026.10.20
- Upgrade notes:
- - Snipe-IT: re-run *infinito administration inventory provision* once
- Contributors:
- - [Kevin Veen-Birkenbach](https://veen.world): cache, Snipe-IT SSO, bcrypt
* Tue Oct 20 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 16.2.0-1
- Added:
- - Compose: *make compose-network-recreate* rebuilds the stack network safely
- - CI: the updater keeps the GitHub-hosted runner labels current
- Fixed:
- - Backup: hard links, ACLs and xattrs survive every backup and recover hop
- - NFS client: clients on Linux 6.19+ no longer fail writes on ganesha < 9.11
- - AppArmor: the teardown keeps docker-default, so containers keep starting
- - Wazuh: hosts can be put into WireGuard meshes like every other role
- - CI: GitHub-hosted runners are pinned to ubuntu-24.04
- Version jumps (net since 16.1.0):
- - *svc-db-rabbitmq*: 4.3-management-alpine to 4.4-management-alpine
- - *web-app-n8n*: 2.44.4 to 2.45.0
- - *web-app-pixelfed*: 20261019 to 20261020
- - *web-app-snipe-it*: v8.8.0-alpine to v8.8.1-alpine
- - *web-app-taiga*: RabbitMQ 4.3-management-alpine to 4.4-management-alpine
- - *eslint* (dev): ^10.12.0 to ^10.13.0
- Upgrade notes:
- - Backup: a target without xattr support now fails instead of losing them
- - Dev: worktree hosts apply the IP pool with *make compose-network-recreate*
- Contributors:
- - [Kevin Veen-Birkenbach](https://veen.world): backups, NFS, AppArmor, CI
* Mon Oct 19 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 16.1.0-1
- Added:
-3
View File
@@ -1,3 +0,0 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[2]
+5 -6
View File
@@ -23,18 +23,17 @@ _STD_TO_BCRYPT_B64 = bytes.maketrans(
)
def _seeded_salt(seed: str, secret: bytes) -> bytes:
"""A fixed bcrypt salt derived from the seed and the secret.
def _seeded_salt(seed: str) -> bytes:
"""A fixed bcrypt salt derived from the seed alone.
Args:
seed: caller-chosen, non-empty string that separates otherwise equal secrets.
secret: the UTF-8 encoded secret being hashed.
Returns:
``$2b$12$`` plus 22 bcrypt-base64 characters encoding the first 16
bytes of sha256(seed + secret).
bytes of sha256(seed).
"""
digest = hashlib.sha256(seed.encode("utf-8") + secret).digest()[:16]
digest = hashlib.sha256(seed.encode("utf-8")).digest()[:16]
encoded = base64.b64encode(digest)[:22].translate(_STD_TO_BCRYPT_B64)
return b"$2b$12$" + encoded
@@ -61,7 +60,7 @@ class FilterModule:
if salt_seed is None:
salt = bcrypt.gensalt()
elif isinstance(salt_seed, str) and salt_seed:
salt = _seeded_salt(salt_seed, secret)
salt = _seeded_salt(salt_seed)
else:
raise ValueError(
"bcrypt_hash: salt_seed must be a non-empty string when given"
+2
View File
@@ -1,3 +1,5 @@
from pathlib import Path
PROJECT_ROOT: Path = Path(__file__).resolve().parents[2]
__all__ = ["PROJECT_ROOT"]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "infinito-nexus"
version = "16.1.0"
version = "16.2.1"
description = "Infinito.Nexus"
readme = "README.md"
requires-python = ">=3.11"
+2 -1
View File
@@ -9,6 +9,7 @@ The transfer is hard-link deduplicated against the previous snapshot, so each pl
This role asserts that the mount, source, and target configuration values are present, deploys the rsync-driver script, and installs a systemd mount unit plus a oneshot service.
The oneshot service is triggered automatically when the configured mount path appears, so the only operator action is to physically plug the device in.
The filesystem mounted at the configured mount path must store owners, POSIX ACLs and extended attributes (for example ext4 or xfs). The backup runs `rsync -aHAXP`, which fails on FAT or exFAT.
## Cosmos
@@ -97,7 +98,7 @@ Run `files/python/recover.py` on the host with the backup device attached:
recover.py /dev/sdX1 /mnt/usb-recover /var/lib/infinito/backup [--snapshot <timestamp>]
```
The script opens the LUKS device (interactive passphrase prompt; `--passphrase-stdin` for automation), mounts it, picks the newest device snapshot across every machine hash (a fresh host after total loss has a new machine id), mirrors it into the target (`rsync -a --delete`) and unmounts/closes the device again. No pre-recover service backup runs: the device itself is the backup this role maintains. Afterwards restore individual applications from the backup root with the matching role's `recover.py` (`svc-bkp-volume-2-local` for docker volumes, `svc-bkp-nfs-2-local` for NFS exports).
The script opens the LUKS device (interactive passphrase prompt; `--passphrase-stdin` for automation), mounts it, picks the newest device snapshot across every machine hash (a fresh host after total loss has a new machine id), mirrors it into the target (`rsync -aHAX --delete`) and unmounts/closes the device again. No pre-recover service backup runs: the device itself is the backup this role maintains. Afterwards restore individual applications from the backup root with the matching role's `recover.py` (`svc-bkp-volume-2-local` for docker volumes, `svc-bkp-nfs-2-local` for NFS exports).
## Credits
@@ -8,7 +8,7 @@ Opens the LUKS device (interactive passphrase prompt, or stdin with
``--passphrase-stdin``), mounts it, picks the newest device snapshot
(any machine hash: a fresh host after total loss has a new
/etc/machine-id) and mirrors it into the target
(``rsync -a --delete``), then unmounts and closes the device again.
(``rsync -aHAX --delete``), then unmounts and closes the device again.
No pre-recover service backup runs: the device itself is the backup
this role maintains.
@@ -53,7 +53,7 @@ def main():
try:
rsync_command = [
"rsync",
"-aP",
"-aHAXP",
"--numeric-ids",
"--delete",
"--delete-excluded",
+2 -2
View File
@@ -44,7 +44,7 @@ flowchart TD
UNIT["svc-bkp-nfs-2-local.&lt;version&gt;.&lt;domain&gt;.service"] --> LOCK["ExecStartPre: sys-lock against the manipulation group"]
LOCK --> SCRIPT["ExecStart: script.sh &lt;export_base&gt; &lt;backups_dir&gt;<br>backup-nfs-to-local &lt;state&gt;/backup"]
SCRIPT -->|export_base missing| ALARM["ERROR exit 1<br>OnFailure alarm"]
SCRIPT --> RSYNC["rsync -a --delete<br>--exclude /&lt;state&gt;/backup (no backups-in-backups)<br>--link-dest previous generation (hard links)"]
SCRIPT --> RSYNC["rsync -aHAX --delete<br>--exclude /&lt;state&gt;/backup (no backups-in-backups)<br>--link-dest previous generation (hard links)"]
RSYNC -->|exit 24: live export churn| WARN["WARN, snapshot kept"]
RSYNC -->|any other failure| CLEAN["generation removed, exit != 0"]
RSYNC --> TREE["&lt;backups_dir&gt;/&lt;sha256(machine-id)&gt;/<br>backup-nfs-to-local/&lt;YYYYmmddHHMMSS&gt;/files/..."]
@@ -105,7 +105,7 @@ recover.py <backups>/<machine-hash>/backup-nfs-to-local/<generation>/files/<stat
```
1. Stop every stack consuming the subtree: `docker stack rm <stack>`.
2. Run the script; it first starts the role's deployed backup unit (a fresh differential `backup-nfs-to-local` generation of the live export), then mirrors the snapshot into the target (`rsync -a --delete`, with the shared `<state>/backup` root protected from deletion and never copied in). `--no-safety-backup` skips the unit run when the target holds nothing worth saving.
2. Run the script; it first starts the role's deployed backup unit (a fresh differential `backup-nfs-to-local` generation of the live export), then mirrors the snapshot into the target (`rsync -aHAX --delete`, with the shared `<state>/backup` root protected from deletion and never copied in). `--no-safety-backup` skips the unit run when the target holds nothing worth saving.
3. Redeploy the stack; the NFS clients re-mount and pick up the restored state.
The target subtree must already exist; the script refuses to create export subtrees implicitly.
@@ -6,7 +6,7 @@
Runs the role's deployed backup unit first (the usual differential
``backup-nfs-to-local`` generation of the live export), then mirrors the
snapshot into the target (``rsync -a --delete``). Run on the host
snapshot into the target (``rsync -aHAX --delete``). Run on the host
serving the export with every consuming stack stopped; NFS clients
re-mount on redeploy.
@@ -45,7 +45,7 @@ trap cleanup_failed_generation ERR
mkdir -p "${DEST_DIR}"
rsync_args=(-a --numeric-ids --delete)
rsync_args=(-aHAX --numeric-ids --delete)
if [[ -n "${EXCLUDE_REL}" ]]; then
rsync_args+=(--exclude "/${EXCLUDE_REL#/}")
fi
@@ -120,7 +120,7 @@ def pull_backups(hostname: str, backups_dir: str) -> None:
Path(local_backup_destination_path).mkdir(parents=True, exist_ok=True)
rsync_command = (
f"rsync -a --partial --info=stats2 "
f"rsync -aHAX --partial --info=stats2 "
f"--numeric-ids --delete --delete-excluded --timeout=300 "
f'-e "ssh {SSH_OPTS}" '
f'--rsync-path="sudo rsync" '
+2 -2
View File
@@ -78,7 +78,7 @@ flowchart TD
TREE --> RECOVER["files/python/recover.py volume<br>resolves Mountpoint + Options"]
RECOVER -->|"Options set"| PROBE{"mountpoint -q<br>(ssh for --docker-host)"}
PROBE -->|unmounted| REFUSE["SystemExit: a restore would land<br>on the node disk and be shadowed<br>on the next mount"]
PROBE -->|mounted| RSYNC["rsync -a --delete through the<br>mounted export onto the server"]
PROBE -->|mounted| RSYNC["rsync -aHAX --delete through the<br>mounted export onto the server"]
DRILL["DR drill [9/9]<br>mounts the export at the volume<br>mountpoint with the volume's own opts"] --> RECOVER
RSYNC --> VERIFY["marker asserted on the<br>NFS server's disk"]
```
@@ -304,7 +304,7 @@ recover.py <backups>/<machine-hash>/backup-docker-to-local/<generation>/<volume>
```
1. Stop the consuming project (`docker compose down` / `docker stack rm <stack>`).
2. Run the script; it first starts the role's deployed backup unit (a fresh differential baudolo generation of every volume and database), resolves the volume's mountpoint and mirrors the snapshot into it (`rsync -a --delete`). `--no-safety-backup` skips the unit run when the target holds nothing worth saving.
2. Run the script; it first starts the role's deployed backup unit (a fresh differential baudolo generation of every volume and database), resolves the volume's mountpoint and mirrors the snapshot into it (`rsync -aHAX --delete`). `--no-safety-backup` skips the unit run when the target holds nothing worth saving.
3. Restore the databases with the `--databases` mode above **while the project is still down**, then start it again; on swarm, NFS-backed volumes are restored via `svc-bkp-nfs-2-local`'s `recover.py` instead (see below).
## Credits
@@ -9,7 +9,7 @@ Both modes run the role's deployed backup unit first, unless
``--no-safety-backup`` says the target holds nothing worth saving.
Volume mode mirrors the snapshot into the volume's mountpoint
(``rsync -a --delete``); stop the consuming project first. With
(``rsync -aHAX --delete``); stop the consuming project first. With
``--docker-host ssh://user@host`` it works on a remote host.
Database mode (``--databases``) replays every dump with
+1 -1
View File
@@ -12,7 +12,7 @@ rabbitmq:
bond: 1
image: rabbitmq
name: rabbitmq
version: "4.3-management-alpine"
version: "4.4-management-alpine"
min_storage: 289MB
backup:
database_routine: true
+1 -1
View File
@@ -30,7 +30,7 @@ buildkit:
enabled: false
shared: false
image: moby/buildkit
version: "v0.34.0"
version: "v0.34.1"
# nocheck: dynamic-flag
act-runner:
enabled: false
+6
View File
@@ -50,6 +50,12 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (
Generations written to the share before the bind stay under the
export's `backup/` directory, hidden from the nodes; remove them there
by hand.
- **Directory delegations off against old ganesha:** When the server
runs nfs-ganesha older than 9.11, every client and the controller set
the kernel's `nfsv4` parameter `directory_delegations` to `N`, at
runtime and in `/etc/modprobe.d`. The parameter is kernel-wide, so on
a lab whose nodes share one kernel it covers every NFS mount of that
host until the module reloads.
## Quick Setup
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -eu
param=/sys/module/nfsv4/parameters/directory_delegations
[ -e "${param}" ] || exit 0
[ "$(cat "${param}")" = N ] && exit 0
echo N >"${param}"
echo CHANGED
@@ -0,0 +1,27 @@
---
- name: "🔍 Read the NFS server's installed packages"
ansible.builtin.package_facts:
delegate_to: "{{ groups['svc-storage-nfs-server'] | first }}"
delegate_facts: true
run_once: true
- name: "🔧 Stop requesting directory delegations from a ganesha that rejects them"
when: >-
(hostvars[groups['svc-storage-nfs-server'] | first].ansible_facts.packages['nfs-ganesha'] | first).version
is version('9.11', '<')
block:
- name: "🔧 Keep directory delegations off whenever nfsv4 loads"
ansible.builtin.lineinfile:
path: /etc/modprobe.d/infinito-nfs-directory-delegations.conf
line: "options nfsv4 directory_delegations=N"
create: true
owner: root
group: root
mode: "0644"
- name: "🔧 Turn directory delegations off in the running kernel"
ansible.builtin.script:
cmd: "{{ lookup('path_absolute', 'roles/svc-storage-nfs-client/files/shell/directory_delegations.sh') }}"
executable: /bin/bash
register: svc_storage_nfs_client_directory_delegations
changed_when: "'CHANGED' in svc_storage_nfs_client_directory_delegations.stdout"
@@ -51,6 +51,10 @@
failed_when:
- svc_storage_nfs_client_probe is failed
- name: "🔧 Align the NFS client's directory delegations with the ganesha server"
ansible.builtin.include_tasks: directory_delegations.yml
when: lookup('nfs_flavor') == 'ganesha'
- name: "🔍 Probe-write a marker to confirm the mount is RW"
ansible.builtin.copy:
dest: "{{ SVC_STORAGE_NFS_CLIENT_PROBE_DIR }}/.svc-storage-nfs-client-probe.{{ inventory_hostname }}"
@@ -59,3 +59,7 @@
retries: 12
delay: 5
until: _ctrl_nfs_mount is succeeded
- name: "🔧 Align the controller's directory delegations with the ganesha server"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-storage-nfs-client/tasks/directory_delegations.yml') }}"
when: lookup('nfs_flavor') == 'ganesha'
@@ -4,6 +4,7 @@ LOG {
components {
INIT = FULL_DEBUG;
DISPATCH = DEBUG;
NFS_V4 = DEBUG;
}
}
@@ -53,6 +53,10 @@
- name: ⬇️ Pull the Playwright image once for every role of this run
ansible.builtin.command:
argv:
- timeout
- -k
- "60"
- "{{ lookup('timeout', 1800) | int }}"
- container
- pull
- "{{ TEST_E2E_PLAYWRIGHT_IMAGE }}"
+1 -1
View File
@@ -67,7 +67,7 @@ n8n:
backup:
no_stop_required: false
image: docker.n8n.io/n8nio/n8n
version: "2.44.4"
version: "2.45.0"
update:
type: registry_tags
image: docker.n8n.io/n8nio/n8n
+1 -1
View File
@@ -142,7 +142,7 @@ rabbit:
retries: 5
name: opentalk-rabbit
image: rabbitmq
version: 4.3.6
version: 4.4.0
min_storage: 314MB
cpus: "0.5"
mem_reservation: 256m
+1 -1
View File
@@ -49,7 +49,7 @@ pixelfed:
shared: true
enabled: true # nocheck: dynamic-flag
image: zknt/pixelfed
version: "20261019"
version: "20261020"
name: pixelfed
min_storage: 588MB
backup:
+8 -3
View File
@@ -88,9 +88,14 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (
When the oauth2-proxy SSO flavor is active, Snipe-IT's native
`loginViaRemoteUser()` is enabled via the `Setting` model
(`login_remote_user_enabled`, `login_remote_user_header_name =
HTTP_X_FORWARDED_PREFERRED_USERNAME`). nginx gates `/login` through
oauth2-proxy/Keycloak and injects the verified
`X-Forwarded-Preferred-Username` header, which Snipe-IT matches against
INFINITO_SSO_USER`). nginx gates `/login` through
oauth2-proxy/Keycloak, injects the verified
`X-Forwarded-Preferred-Username` header and sends the
`sso_proxy_secret` credential as `X-Infinito-Sso-Proxy` with every
request. Apache in the
container copies the username into the `INFINITO_SSO_USER` server
variable only when that secret matches `INFINITO_SSO_PROXY_SECRET`.
Snipe-IT matches the username against
`users.username` (the user must pre-exist and be activated — sync them
via LDAP) to mint a native `snipeit_session`. Visitors without a matching
activated user silently fall through to the normal login form (no error).
+2
View File
@@ -3,3 +3,5 @@ ARG SNIPE_IT_VERSION
FROM ${SNIPE_IT_IMAGE}:${SNIPE_IT_VERSION}
RUN sed -i 's#php artisan migrate --force#php artisan migrate --force --isolated#' /startup.sh
RUN printf 'SetEnvIf X-Forwarded-Preferred-Username "^(.+)$" INFINITO_SSO_USER=$1\nSetEnvIfExpr "-z env(\047INFINITO_SSO_PROXY_SECRET\047) || req(\047X-Infinito-Sso-Proxy\047) != env(\047INFINITO_SSO_PROXY_SECRET\047)" !INFINITO_SSO_USER\n' > /etc/apache2/conf.d/infinito-sso.conf
@@ -2,14 +2,6 @@
// nocheck: mirrored-unit-test - boots Snipe-IT's Laravel kernel through
// bootstrap/app.php and writes the trusted-reverse-proxy login settings through
// App\Models\Setting; the models only exist once the install has booted
// Configure Snipe-IT trusted-reverse-proxy login via the Laravel `Setting`
// model (same path the UI uses). The oauth2-proxy in front of Snipe-IT
// authenticates the user against Keycloak and nginx injects the verified
// identity into the `X-Forwarded-Preferred-Username` request header, which
// PHP exposes as $_SERVER['HTTP_X_FORWARDED_PREFERRED_USERNAME'].
// LoginController::loginViaRemoteUser() then matches users.username and mints
// a native snipeit_session. The password/LDAP form stays available as a
// fallback (login_common_disabled is kept at 0).
require "vendor/autoload.php";
$app = require "bootstrap/app.php";
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
+6
View File
@@ -10,3 +10,9 @@ credentials:
algorithm: alphanumeric
validation: "^[A-Za-z0-9]{32,}$"
length: 48
sso_proxy_secret:
description: "Shared secret the reverse proxy sends with every request; Apache in the Snipe-IT container only turns the trusted identity header into the login
variable when it matches."
algorithm: alphanumeric
validation: "^[A-Za-z0-9]{32,}$"
length: 48
+1 -1
View File
@@ -58,7 +58,7 @@ snipe-it:
enabled: true
swarm:
enabled: true
version: v8.8.0-alpine
version: v8.8.1-alpine
custom: true
name: snipe-it
image: snipe/snipe-it
+1
View File
@@ -13,6 +13,7 @@
name: sys-stk-full
vars:
compose_handlers_flush: true
webserver_location_extra_configuration: 'proxy_set_header {{ SNIPE_IT_SSO_PROXY_HEADER }} "{{ SNIPE_IT_SSO_PROXY_SECRET }}";'
- name: "👔 Snipe-IT manager-side operations (container exec)"
include_tasks: 01_install/manager_ops.yml
+15
View File
@@ -26,3 +26,18 @@
ansible.builtin.shell:
cmd: "container exec {{ SNIPE_IT_CONTAINER_ADDRESS }} php artisan optimize:clear"
changed_when: true
- name: "🔒 snipe-it: the SSO header bridge logs in only proxy-secret requests"
ansible.builtin.shell:
cmd: >-
container exec {{ SNIPE_IT_CONTAINER_ADDRESS }} sh -c
'{{ 30 | curl }} -s -o /dev/null -w "%{http_code}" -H "X-Forwarded-Preferred-Username: $1" {{ item.proxy_header }} http://localhost/login'
_ {{ lookup('users', 'administrator').username | quote }}
loop:
- { label: "forged header without the proxy secret", proxy_header: "", expected: "200" }
- { label: "header with the proxy secret", proxy_header: '-H "{{ SNIPE_IT_SSO_PROXY_HEADER }}: $INFINITO_SSO_PROXY_SECRET"', expected: "302" }
loop_control:
label: "{{ item.label }}"
register: snipe_it_sso_bridge_probe
changed_when: false
failed_when: snipe_it_sso_bridge_probe.stdout != item.expected
+1
View File
@@ -78,6 +78,7 @@ SECURE_COOKIES={{ SNIPE_IT_TLS_ENABLED | string | lower }}
API_TOKEN_EXPIRATION_YEARS=40
APP_TRUSTED_PROXIES=**
INFINITO_SSO_PROXY_SECRET={{ SNIPE_IT_SSO_PROXY_SECRET }}
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more