From 2b9e7f302cb021987c77cab50ec7fe81dad34f77 Mon Sep 17 00:00:00 2001 From: jknapp Date: Wed, 21 Oct 2026 16:46:03 -0700 Subject: [PATCH] ci: enable PKG_CACHE via a TLS probe that falls back to upstream; host repo tool - build-push.yaml: Build-and-Push, Build-FPM-Images, Build-Shared-httpd and Build-Shared-OLS probe https://192.168.1.40 and :5000 with the pinned pkg-cache CA (5 s timeouts). When the cache answers they pass PKG_CACHE and configure the BuildKit Docker Hub mirror (ca=). When it doesn't, they build from upstream exactly as before and emit a warning. Fallback measured from the runner bridge: refused <1 s, silent drop 10 s. - scripts/host/pkgcache-host-repos.sh: apply/revert/status for WHP hosts. Each rewritten repo uses baseurl= as the real fallback; mirrorlist/metalink stay commented because measured dnf ignores baseurl when they are present. Keeps gpgcheck=1 / repo_gpgcheck=1 and backs up every file. Tested on 192.168.1.148, including cache-down and silent-drop fallback. - docs: runner firewall rule (applied), measured fallback results. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/build-push.yaml | 125 ++++++++++++++++++++++++++++ docs/pkg-cache.md | 120 ++++++++++++++------------ scripts/host/pkgcache-host-repos.sh | 107 ++++++++++++++++++++++++ 3 files changed, 301 insertions(+), 51 deletions(-) create mode 100644 scripts/host/pkgcache-host-repos.sh diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index e6d8a20..bbca7d2 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -85,10 +85,40 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if + # it answers over TLS with our pinned CA; otherwise (down, unreachable, + # bad/expired cert) this job builds from the upstream repos and Docker Hub + # exactly as before. The repo cache and the registry mirror are probed + # separately so either can fail back on its own. + - name: Probe package cache + id: pkgcache + run: | + url=""; mirror="" + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi + echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" + [ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories" + echo "url=${url}" >> "$GITHUB_OUTPUT" + echo "mirror=${mirror}" >> "$GITHUB_OUTPUT" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + # BuildKit (docker-container driver) ignores dockerd's registry-mirrors, + # so the mirror is configured on the builder; BuildKit itself still falls + # back to Docker Hub if the mirror stops answering mid-run. + - name: Set up Docker Buildx (Docker Hub via pkg-cache mirror) + if: steps.pkgcache.outputs.mirror != '' + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["192.168.1.40:5000"] + [registry."192.168.1.40:5000"] + ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"] + - name: Set up Docker Buildx + if: steps.pkgcache.outputs.mirror == '' uses: docker/setup-buildx-action@v3 # Per-run cache-bust value. Each job's buildx builder is created fresh by @@ -135,6 +165,7 @@ jobs: build-args: | PHPVER=${{ matrix.phpver }} CACHE_BUST=${{ steps.bust.outputs.value }} + PKG_CACHE=${{ steps.pkgcache.outputs.url }} EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }} tags: | repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }} @@ -152,10 +183,40 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if + # it answers over TLS with our pinned CA; otherwise (down, unreachable, + # bad/expired cert) this job builds from the upstream repos and Docker Hub + # exactly as before. The repo cache and the registry mirror are probed + # separately so either can fail back on its own. + - name: Probe package cache + id: pkgcache + run: | + url=""; mirror="" + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi + echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" + [ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories" + echo "url=${url}" >> "$GITHUB_OUTPUT" + echo "mirror=${mirror}" >> "$GITHUB_OUTPUT" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + # BuildKit (docker-container driver) ignores dockerd's registry-mirrors, + # so the mirror is configured on the builder; BuildKit itself still falls + # back to Docker Hub if the mirror stops answering mid-run. + - name: Set up Docker Buildx (Docker Hub via pkg-cache mirror) + if: steps.pkgcache.outputs.mirror != '' + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["192.168.1.40:5000"] + [registry."192.168.1.40:5000"] + ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"] + - name: Set up Docker Buildx + if: steps.pkgcache.outputs.mirror == '' uses: docker/setup-buildx-action@v3 # Per-run cache-bust value. Each job's buildx builder is created fresh by @@ -203,6 +264,7 @@ jobs: build-args: | PHPVER=${{ matrix.phpver }} CACHE_BUST=${{ steps.bust.outputs.value }} + PKG_CACHE=${{ steps.pkgcache.outputs.url }} EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }} tags: | repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }} @@ -417,10 +479,40 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if + # it answers over TLS with our pinned CA; otherwise (down, unreachable, + # bad/expired cert) this job builds from the upstream repos and Docker Hub + # exactly as before. The repo cache and the registry mirror are probed + # separately so either can fail back on its own. + - name: Probe package cache + id: pkgcache + run: | + url=""; mirror="" + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi + echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" + [ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories" + echo "url=${url}" >> "$GITHUB_OUTPUT" + echo "mirror=${mirror}" >> "$GITHUB_OUTPUT" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + # BuildKit (docker-container driver) ignores dockerd's registry-mirrors, + # so the mirror is configured on the builder; BuildKit itself still falls + # back to Docker Hub if the mirror stops answering mid-run. + - name: Set up Docker Buildx (Docker Hub via pkg-cache mirror) + if: steps.pkgcache.outputs.mirror != '' + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["192.168.1.40:5000"] + [registry."192.168.1.40:5000"] + ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"] + - name: Set up Docker Buildx + if: steps.pkgcache.outputs.mirror == '' uses: docker/setup-buildx-action@v3 - name: Login to Gitea @@ -436,6 +528,8 @@ jobs: file: ./Dockerfile.shared-httpd platforms: linux/amd64 push: true + build-args: | + PKG_CACHE=${{ steps.pkgcache.outputs.url }} tags: | repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest @@ -445,10 +539,40 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if + # it answers over TLS with our pinned CA; otherwise (down, unreachable, + # bad/expired cert) this job builds from the upstream repos and Docker Hub + # exactly as before. The repo cache and the registry mirror are probed + # separately so either can fail back on its own. + - name: Probe package cache + id: pkgcache + run: | + url=""; mirror="" + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi + if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi + echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" + [ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories" + echo "url=${url}" >> "$GITHUB_OUTPUT" + echo "mirror=${mirror}" >> "$GITHUB_OUTPUT" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + # BuildKit (docker-container driver) ignores dockerd's registry-mirrors, + # so the mirror is configured on the builder; BuildKit itself still falls + # back to Docker Hub if the mirror stops answering mid-run. + - name: Set up Docker Buildx (Docker Hub via pkg-cache mirror) + if: steps.pkgcache.outputs.mirror != '' + uses: docker/setup-buildx-action@v3 + with: + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["192.168.1.40:5000"] + [registry."192.168.1.40:5000"] + ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"] + - name: Set up Docker Buildx + if: steps.pkgcache.outputs.mirror == '' uses: docker/setup-buildx-action@v3 - name: Login to Gitea @@ -469,5 +593,6 @@ jobs: build-args: | PHPVER=83 OLS_VERSION=1.8.4 + PKG_CACHE=${{ steps.pkgcache.outputs.url }} tags: | repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest diff --git a/docs/pkg-cache.md b/docs/pkg-cache.md index f476dcd..ceae8fa 100644 --- a/docs/pkg-cache.md +++ b/docs/pkg-cache.md @@ -95,72 +95,90 @@ Remaining risks: - It passes the results as `EXPECT_OPENSSL_MIN` / `EXPECT_HTTPD_MIN` / `EXPECT_IMAGEMAGICK_MIN`. - The Dockerfile asserts `rpm -q --qf '%{EVR}'` is not older (`rpm --eval "%{lua: print(rpm.vercmp('A','B'))}"`). -## CI integration plan (NOT applied — needs Josh) +## CI integration (APPLIED 2026-10-21) -1. **Network (blocker).** The ephemeral runners are NATed, and `iptables FORWARD` **drops all RFC1918 destinations**, including 192.168.1.40: +1. **Runner network.** Ephemeral runners are NATed and `iptables FORWARD` drops + all RFC1918 destinations: - `eph-pve-*` on `pve`, vmbr2, 10.99.98.0/24; - `eph-pve3-*` on `pve3`, vmbr1, 10.99.99.0/24. - A narrow exception is needed, placed before the DROP lines: + One rule per node now sits **above** those DROPs. It is persistent through a + `post-up`/`post-down` pair placed before the first DROP line in + `/etc/network/interfaces.d/vmbr2-ci-isolated` (pve) and + `/etc/network/interfaces.d/vmbr1-ci-isolated` (pve3). The Proxmox firewall is + disabled on both nodes, so its reloads do not touch these chains. Backups: + `/root/vmbr{2,1}-ci-isolated.bak-pkgcache-20261021`. ``` - # pve, /etc/network/interfaces.d/ vmbr2 stanza, BEFORE the 192.168.0.0/16 DROP: - post-up iptables -A FORWARD -i vmbr2 -o vmbr0 -d 192.168.1.40 -p tcp -m multiport --dports 443,5000 -j ACCEPT - post-down iptables -D FORWARD -i vmbr2 -o vmbr0 -d 192.168.1.40 -p tcp -m multiport --dports 443,5000 -j ACCEPT - # live: iptables -I FORWARD 1 -i vmbr2 -o vmbr0 -d 192.168.1.40 -p tcp -m multiport --dports 443,5000 -j ACCEPT - # pve3: the same with vmbr1 + iptables -I FORWARD 1 -i vmbr2 -o vmbr0 -d 192.168.1.40/32 -p tcp -m multiport --dports 443,5000 \ + -m comment --comment pkg-cache-ct130 -j ACCEPT ``` - `gitea-runner.internal.cloud-hosting.io` resolves to 10.10.14.20, which is not on this LAN. Whether it can reach the cache is untested. -2. **Probe with the CA, then opt in.** Keep `${{ }}` out of `run:`. The probe fails when the cache is down **or its certificate is expired or untrusted**, and the build then uses upstream. - ```yaml - - name: Probe package cache - id: pkgcache - run: | - if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/ ; then - echo "url=https://192.168.1.40" >> "$GITHUB_OUTPUT" - else - echo "url=" >> "$GITHUB_OUTPUT"; echo "::warning::pkg-cache unreachable - building from upstream" - fi - # docker/build-push-action `build-args:` add PKG_CACHE=${{ steps.pkgcache.outputs.url }} - ``` -3. **Docker Hub mirror for BuildKit**, over TLS and without `insecure`. `setup-buildx-action` uses the docker-container driver, which **ignores dockerd's `registry-mirrors`**. buildx copies the `ca` file into the builder. - ```yaml - - uses: docker/setup-buildx-action@v3 - with: - buildkitd-config-inline: | - [registry."docker.io"] - mirrors = ["192.168.1.40:5000"] - [registry."192.168.1.40:5000"] - ca = ["keys/pkg-cache/ca.crt"] - ``` - The exact equivalent (absolute `ca` path) was measured on 192.168.1.193. BuildKit falls back to Docker Hub when the mirror is down; this was measured too, with the registry stopped. The relative `ca` path inside a workflow is **untested**; use `${{ github.workspace }}/keys/pkg-cache/ca.crt` if it doesn't resolve. -4. **dockerd clients** (plain `docker pull`). Untested, because it needs a daemon restart. Use TLS, no `insecure-registries`: + Verified from throwaway CTs on vmbr2 and vmbr1: + - :443 and :5000 work with TLS verified; + - CT130 :22/:80, pve :8006, .193 :22, the router's :53 and 10.10.14.20 stay **blocked**; + - internet access is unchanged. + + The LAN runner VM *Git-Builders* (VM103, 192.168.1.126 on vmbr0) needs no + rule. `gitea-runner.internal.cloud-hosting.io` resolves to 10.10.14.20, which + is unroutable from both the LAN and the DC (stale record?). +2. **Workflow** (`.gitea/workflows/build-push.yaml`). Jobs Build-and-Push, + Build-FPM-Images, Build-Shared-httpd and Build-Shared-OLS run a + **"Probe package cache"** step. It curls `https://192.168.1.40/` and + `https://192.168.1.40:5000/v2/` with `--cacert keys/pkg-cache/ca.crt + --max-time 5`. + - If both answer: `PKG_CACHE=https://192.168.1.40` is passed as a build arg, + and buildx is set up with the Docker Hub mirror (`buildkitd-config-inline`, + `ca=`). + - If either fails: that output is empty, the build uses upstream repos / + Docker Hub, and a `::warning::` is emitted. + + Litespeed and lsphp jobs are unchanged (their Dockerfiles are not wired). + **Fail-back measured** with the workflow's own step code, from a runner-bridge CT: + + | cache state | probe result | time | + |---|---|---| + | up | url + mirror set | <1 s | + | nginx + registry stopped | both empty, warning | <1 s (connection refused) | + | silently dropped | both empty, warning | 10 s (2 x 5 s) | + + Mid-build cache loss is also covered: dnf has the origin as a 2nd baseurl, and + BuildKit falls back from the mirror to Docker Hub (measured with the registry stopped). +3. **dockerd clients** (plain `docker pull`). Untested, because it needs a daemon restart. + Use TLS, no `insecure-registries`: ``` install -D -m0644 keys/pkg-cache/ca.crt /etc/docker/certs.d/192.168.1.40:5000/ca.crt # /etc/docker/daemon.json: { "registry-mirrors": ["https://192.168.1.40:5000"] } then restart docker ``` -## WHP hosts (plan only) +## WHP hosts (`scripts/host/pkgcache-host-repos.sh`) -The production hosts (whp01, whp02, sdbees) are in the datacenter and **cannot reach 192.168.1.40**. Only LAN hosts can, e.g. 192.168.1.148. Options for Josh: -- (a) a second identical cache in the datacenter, with its own name-constrained CA; -- (b) expose this one over NetBird; -- (c) LAN hosts only. +`pkgcache-host-repos.sh apply https://192.168.1.40` does the following: +- backs up every `/etc/yum.repos.d/*.repo` to `/root/pkgcache-repo-backup-/`; +- installs the name-constrained CA as a system anchor; +- rewrites the AlmaLinux, EPEL and Remi sections to `baseurl=/... /...`; +- comments out mirrorlist/metalink in those sections; +- sets `gpgcheck=1`, plus `repo_gpgcheck=1` for AlmaLinux and Remi. -For a host that can reach a cache: -- install the CA at `/etc/pki/pkg-cache/ca.crt`; -- create a bundle (`cat /etc/pki/tls/certs/ca-bundle.crt /etc/pki/pkg-cache/ca.crt > /etc/pki/pkg-cache/bundle.pem`); -- in each repo section, keep the origin as the second URL: +`revert [DIR]` restores byte-identical files (verified on AL9 and AL10) and +removes the anchor. `status` shows the current state. -```ini -[remi-safe] -baseurl=https:///remi/enterprise/$releasever_major/safe/$basearch/ https://rpms.remirepo.net/enterprise/$releasever_major/safe/$basearch/ -#mirrorlist=... -sslcacert=/etc/pki/pkg-cache/bundle.pem -gpgcheck=1 -repo_gpgcheck=1 # Remi + AlmaLinux sign repomd; EPEL does not -``` +**Why not "cache first, mirrorlist as fallback":** measured on AL10, with +mirrorlist/metalink left in place dnf **ignores `baseurl` completely** (0 cache +requests). The origin as second baseurl is the fallback dnf really performs. -Hosts have no php.net gate. On a security-release day, run `pkgcache-refresh` on the cache and check `dnf info php-cli` against php.net. +**Measured on 192.168.1.148** (AlmaLinux 10.2), 2026-10-21: + +| test | result | +|---|---| +| baseline `dnf makecache --refresh` (original mirrorlists) | rc=0, **1209 s** | +| via cache `makecache --refresh` | rc=0, **22 s** (2nd run, non-interactive: 2 s) | +| via cache `dnf download php-cli` | rc=0, 1 s | +| `dnf -y --downloadonly update php-cli` | "Nothing to do" (already 8.5.12) | +| cache **down** (nginx stopped): makecache --refresh / download | rc=0 **16 s** / rc=0 24 s, served by origin | +| cache **silently dropped** (iptables DROP): makecache --refresh / download | rc=0 **225 s** / rc=0 59 s (dnf's 30 s connect timeout per attempt) | + +**Datacenter hosts** (whp01, whp02, sdbees, TrueSelfCA) cannot reach +192.168.1.40 and are **not configured**. That needs CT130 on NetBird (see the +report: setup key / route pending). ## Operating the cache (CT130) diff --git a/scripts/host/pkgcache-host-repos.sh b/scripts/host/pkgcache-host-repos.sh new file mode 100644 index 0000000..2f44b04 --- /dev/null +++ b/scripts/host/pkgcache-host-repos.sh @@ -0,0 +1,107 @@ +#!/bin/bash +## pkgcache-host-repos.sh — point a WHP HOST's dnf repos (AlmaLinux 9/10 BaseOS/ +## AppStream/CRB/extras/..., EPEL, Remi) at the internal package cache, with the +## ORIGIN as automatic fallback. Not used by image builds (see pkg-cache-repos.sh). +## +## pkgcache-host-repos.sh apply https://192.168.1.40 # backs up, then rewrites +## pkgcache-host-repos.sh status +## pkgcache-host-repos.sh revert [BACKUP_DIR] # default: newest backup +## +## Fallback design (measured 2026-10-21 on AlmaLinux 10.2): +## * each rewritten section gets baseurl=// / +## — dnf/librepo treats the second URL as a failover mirror. +## * the section's mirrorlist=/metalink= is COMMENTED OUT. Measured: with a +## mirrorlist/metalink present dnf ignores baseurl entirely (0 cache requests), +## so "cache first, mirrorlist as fallback" is not something dnf can do. +## * gpgcheck=1 everywhere; repo_gpgcheck=1 for AlmaLinux + Remi (both sign +## repomd.xml); EPEL has no repomd signature and relies on HTTPS end to end. +## * trust: the name-constrained pkg-cache CA is installed as a system anchor +## (/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt). Its nameConstraints +## (IP 192.168.1.40, pkg-cache.* DNS) are enforced by OpenSSL — a cert it +## signs for any other name fails ("permitted subtree violation"). +## Every changed file is copied to /root/pkgcache-repo-backup-/ first. +set -euo pipefail + +MODE="${1:-status}" +CA_SRC="$(dirname "$0")/pkg-cache-ca.crt" +ANCHOR=/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt +die() { echo "FATAL(pkgcache-host-repos): $*" >&2; exit 1; } +same() { [ "$(sha256sum < "$1")" = "$(sha256sum < "$2")" ]; } # cmp/diffutils is not on minimal hosts + +rewrite() { # $1 file $2 cache url + python3 - "$1" "$2" <<'PY' +import re, sys +f, C = sys.argv[1], sys.argv[2].rstrip('/') +maps = [ # regex on a (commented or live) baseurl, cache prefix, origin base, repomd signed? + (r'^#?\s*baseurl\s*=\s*https?://repo\.almalinux\.org/almalinux/(\S+)', 'almalinux', 'https://repo.almalinux.org/almalinux/', True), + (r'^#?\s*baseurl\s*=\s*https?://download\.example/pub/epel/(\S+)', 'epel', 'https://dl.fedoraproject.org/pub/epel/', False), + (r'^#?\s*baseurl\s*=\s*https?://rpms\.remirepo\.net/(\S+)', 'remi', 'https://rpms.remirepo.net/', True), +] +out, sec = [], [] +state = {'hit': False, 'sig': False} +def flush(): + for x in sec: + if state['hit']: + if re.match(r'^(mirrorlist|metalink)\s*=', x): + x = '#' + x + elif re.match(r'^(gpgcheck|repo_gpgcheck)\s*=', x): + continue + out.append(x) + if state['hit']: + # keep blank-line layout: insert before trailing blanks + tail = [] + while out and out[-1].strip() == '': + tail.append(out.pop()) + out.append('gpgcheck=1') + if state['sig']: + out.append('repo_gpgcheck=1') + out.extend(tail) + sec.clear(); state['hit'] = state['sig'] = False +for line in open(f).read().split('\n'): + if line.startswith('['): + flush() + for rx, prefix, origin, sig in maps: + m = re.match(rx, line) + if m: + p = m.group(1) + line = f'baseurl={C}/{prefix}/{p} {origin}{p}' + state['hit'] = True; state['sig'] = state['sig'] or sig + break + sec.append(line) +flush() +open(f, 'w').write('\n'.join(out)) +PY +} + +case "$MODE" in + apply) + C="${2:?usage: apply https://}"; C="${C%/}" + case "$C" in https://*) ;; *) die "cache URL must be https://" ;; esac + [ -s "$CA_SRC" ] || die "missing $CA_SRC (copy keys/pkg-cache/ca.crt next to this script)" + B=/root/pkgcache-repo-backup-$(date -u +%Y%m%dT%H%M%SZ) + mkdir -p "$B/yum.repos.d" + cp -a /etc/yum.repos.d/*.repo "$B/yum.repos.d/" + [ -e "$ANCHOR" ] && cp -a "$ANCHOR" "$B/" || touch "$B/.no-anchor-before" + install -m 0644 "$CA_SRC" "$ANCHOR" && update-ca-trust + for f in /etc/yum.repos.d/almalinux*.repo /etc/yum.repos.d/epel*.repo /etc/yum.repos.d/remi*.repo; do + [ -f "$f" ] || continue + grep -q -E '^#?\s*baseurl\s*=\s*https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net)/' "$f" && rewrite "$f" "$C" + done + echo "backup: $B" + echo "changed files:"; for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || echo " /etc/yum.repos.d/$(basename "$f")"; done + ;; + revert) + B="${2:-$(ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sort | tail -1)}" + [ -d "$B/yum.repos.d" ] || die "no backup dir ($B)" + for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || { cp -a "$f" /etc/yum.repos.d/; echo "restored /etc/yum.repos.d/$(basename "$f")"; }; done + if [ -e "$B/.no-anchor-before" ]; then rm -f "$ANCHOR"; update-ca-trust; echo "removed $ANCHOR"; fi + dnf clean metadata -q || true + echo "reverted from $B" + ;; + status) + grep -l -E '^baseurl=https://[^ ]+/(almalinux|epel|remi)/' /etc/yum.repos.d/*.repo 2>/dev/null | sed 's/^/via cache: /' || echo "no repo uses the cache" + ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sed 's/^/backup: /' || true + [ -e "$ANCHOR" ] && echo "CA anchor: $ANCHOR" || echo "CA anchor: absent" + ;; + *) die "usage: apply URL | status | revert [DIR]" ;; +esac