#!/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