107 lines
5.2 KiB
Bash
107 lines
5.2 KiB
Bash
#!/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=<cache>/<prefix>/<path> <origin>/<path>
|
||
|
|
## — 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-<UTC>/ 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://<cache>}"; 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
|