#!/usr/bin/env bash
# scripts/dc-mirror.sh <check|install|sync> <site> -- per-DC artifact mirror,
# D-135 item 1 (apt + Ubuntu Cloud Archive; the D-107 build-out, staged).
# Part of DC-standup definition-of-done from Stage 4 on.
#
# RUNS ON THE DC RACK HOST (e.g. vvr1-dc0), not on vcloud/voffice1. Invoke
# from a repo host over ssh, piping the script (no repo clone needed there):
#
# check: ssh -i <dc-key> -J voffice1 <user>@<rack-transit-ip> \
# 'sudo bash -s' -- check dc0 < scripts/dc-mirror.sh
# install: same with 'install dc0' (idempotent; safe to re-run).
# sync: same with 'sync dc0' -- starts the systemd sync job (async;
# the initial jammy+UCA pull is several hundred GB, hours).
#
# WHAT IT OWNS (site-keyed table below; every value MEASURED or RULED, cited):
# 1. <site>-mirror-net.service -- oneshot, After=<site>-rack-legs.service:
# (a) adds the mirror LISTEN alias (the FIRST D-134 utility-band address,
# .4 on metal-admin) and (b) sets the rack's default route via the DC
# edge's LAN gateway -- the D-107 "mirror's upstream sync" egress path,
# MEASURED WORKING 2026-07-23 (rack->edge->internet ping 0% loss, curl
# HTTP 301 -- the D-125 signature; as-executed log stage4-carve). Bridge
# devices are resolved FROM LIBVIRT NETWORK NAMES at runtime (virbrN is
# auto-assigned, a drifting ID -- hard rule 3).
# 2. /etc/systemd/resolved.conf.d/<site>-mirror.conf -- system DNS = the DC
# edge's resolver (MEASURED 2026-07-23: edge Unbound answers
# archive.ubuntu.com from the rack). Additive: the rack previously had NO
# working system DNS; the MAAS rack agent ignores resolv.conf entirely
# (measured, D-131), so this cannot regress node DNS.
# 3. /usr/local/sbin/<site>-mirror-sync -- debmirror runner: Ubuntu jammy
# amd64 BINARY mirror (main/restricted/universe/multiverse; jammy,
# jammy-updates, jammy-security; no sources) into
# /var/lib/dc-mirror/ubuntu, plus Ubuntu Cloud Archive
# jammy-updates/caracal into /var/lib/dc-mirror/cloud-archive. GPG
# verified against the ubuntu-archive / ubuntu-cloud keyrings. Status in
# /var/lib/dc-mirror/last-sync.status.
# 4. <site>-mirror-sync.service (oneshot) + <site>-mirror-sync.timer (daily,
# randomized) -- the upstream re-sync.
# 5. /etc/nginx/sites-available/<site>-mirror (+ sites-enabled symlink) --
# serves /var/lib/dc-mirror on http://<LISTEN>/ (autoindex). Stage-5
# wiring consumes this via `juju model-config apt-mirror=` per DC model.
#
# PREREQ PACKAGES (install FAILS LOUD if absent; run the printed apt line
# after the net unit is up -- the rack has no egress until it is):
# debmirror nginx ubuntu-keyring ubuntu-cloud-keyring
#
# EXIT: 0 ok | 1 check failed | 2 bad args/unknown site | 4 install failed.
# ASCII + LF only.
set -uo pipefail
MODE="${1:-}"; SITE="${2:-}"
case "$MODE" in check|install|sync) ;; *)
echo "usage: dc-mirror.sh <check|install|sync> <site>" >&2; exit 2 ;; esac
# ---------------------------------------------------------------------------
# Site table. ADD A SITE ONLY WITH MEASURED/RULED VALUES (hard rule 2); the
# harness rejects rows without a MEASURED tag.
# ---------------------------------------------------------------------------
case "$SITE" in
dc0)
# MEASURED/RULED 2026-07-23 (Stage 4, D-134 amendment + D-135; egress
# probe in as-executed log stage4-carve):
# LISTEN 10.12.8.4/22 on vr1-dc0-metal-admin -- FIRST utility-band
# address (D-134 .4-.9), mirror service address (D-135).
# EDGE_GW 10.12.4.1 -- dc0 edge LAN gw (lib-net vr1-dc0 PLANE_GW;
# ruled D-124/D-113 addressing), reached via the rack's existing
# provider-public leg 10.12.4.2/22 (dc-rack-net.sh dc0 row).
MIRROR_NET="vr1-dc0-metal-admin"
LISTEN_CIDR="10.12.8.4/22"
EDGE_GW="10.12.4.1"
;;
dc1)
# MEASURED/RULED 2026-07-23 (same session; dc1 values from the D-124
# amendment plane map + lib-net vr1-dc1 arm):
# LISTEN 10.12.68.4/22 on vr1-dc1-metal-admin -- utility-band .4.
# EDGE_GW 10.12.64.1 -- dc1 edge LAN gw (PLANE_GW, ruled provider
# gateway), via the rack's provider-public leg 10.12.64.2/22.
MIRROR_NET="vr1-dc1-metal-admin"
LISTEN_CIDR="10.12.68.4/22"
EDGE_GW="10.12.64.1"
;;
*) echo "FAIL: unknown site '$SITE' -- add a MEASURED row block first" >&2; exit 2 ;;
esac
LISTEN="${LISTEN_CIDR%%/*}"
MIRROR_ROOT="/var/lib/dc-mirror"
HELPER="/usr/local/sbin/${SITE}-mirror-net-apply"
NET_UNIT="/etc/systemd/system/${SITE}-mirror-net.service"
RESOLVED_DROPIN="/etc/systemd/resolved.conf.d/${SITE}-mirror.conf"
SYNC_HELPER="/usr/local/sbin/${SITE}-mirror-sync"
SYNC_UNIT="/etc/systemd/system/${SITE}-mirror-sync.service"
SYNC_TIMER="/etc/systemd/system/${SITE}-mirror-sync.timer"
NGINX_SITE="/etc/nginx/sites-available/${SITE}-mirror"
NGINX_LINK="/etc/nginx/sites-enabled/${SITE}-mirror"
ARCHIVE_KEYRING="/usr/share/keyrings/ubuntu-archive-keyring.gpg"
CLOUD_KEYRING="/usr/share/keyrings/ubuntu-cloud-keyring.gpg"
# ---------------------------------------------------------------------------
# Generated file contents (single source of truth for check AND install).
# ---------------------------------------------------------------------------
gen_helper() {
cat <<EOF
#!/usr/bin/env bash
# ${SITE}-mirror-net-apply -- generated by scripts/dc-mirror.sh; do not hand-edit.
# Adds the mirror LISTEN alias (D-134 utility .4) and the default route via the
# DC edge (D-107 mirror-sync egress). Bridge resolved from the libvirt network
# NAME at runtime (virbrN is auto-assigned, not stable identity).
set -euo pipefail
br_of() {
virsh -c qemu:///system net-dumpxml "\$1" | sed -n "s/.*bridge name='\\([^']*\\)'.*/\\1/p"
}
ip addr replace ${LISTEN_CIDR} dev "\$(br_of ${MIRROR_NET})"
ip route replace default via ${EDGE_GW}
EOF
}
gen_net_unit() {
cat <<EOF
[Unit]
Description=${SITE} mirror net (dc-mirror.sh; D-135 -- LISTEN alias + edge default route)
After=libvirtd.service ${SITE}-rack-legs.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${HELPER}
[Install]
WantedBy=multi-user.target
EOF
}
gen_resolved_dropin() {
cat <<EOF
# ${SITE} mirror DNS (generated by scripts/dc-mirror.sh; D-135). System DNS =
# the DC edge resolver (MEASURED answering from the rack, 2026-07-23). The
# MAAS rack agent ignores resolv.conf (D-131), so node DNS is unaffected.
[Resolve]
DNS=${EDGE_GW}
EOF
}
gen_sync_helper() {
cat <<EOF
#!/usr/bin/env bash
# ${SITE}-mirror-sync -- generated by scripts/dc-mirror.sh (D-135 item 1);
# do not hand-edit. Jammy amd64 binary mirror + UCA caracal, GPG-verified.
set -uo pipefail
STATUS="${MIRROR_ROOT}/last-sync.status"
echo "RUNNING \$(date -u +%FT%TZ)" > "\$STATUS"
mkdir -p ${MIRROR_ROOT}/ubuntu ${MIRROR_ROOT}/cloud-archive
debmirror ${MIRROR_ROOT}/ubuntu \\
--host=archive.ubuntu.com --root=ubuntu --method=http \\
--dist=jammy,jammy-updates,jammy-security \\
--section=main,restricted,universe,multiverse \\
--arch=amd64 --nosource --diff=none \\
--keyring=${ARCHIVE_KEYRING} --progress
RC1=\$?
debmirror ${MIRROR_ROOT}/cloud-archive \\
--host=ubuntu-cloud.archive.canonical.com --root=ubuntu --method=http \\
--dist=jammy-updates/caracal --section=main \\
--arch=amd64 --nosource --diff=none \\
--keyring=${CLOUD_KEYRING} --progress
RC2=\$?
if [ "\$RC1" -eq 0 ] && [ "\$RC2" -eq 0 ]; then
echo "OK \$(date -u +%FT%TZ) ubuntu=0 uca=0" > "\$STATUS"; exit 0
fi
echo "FAIL \$(date -u +%FT%TZ) ubuntu=\$RC1 uca=\$RC2" > "\$STATUS"; exit 1
EOF
}
gen_sync_unit() {
cat <<EOF
[Unit]
Description=${SITE} mirror upstream sync (dc-mirror.sh; D-135 item 1: apt+UCA)
After=${SITE}-mirror-net.service network-online.target
Requires=${SITE}-mirror-net.service
[Service]
Type=oneshot
ExecStart=${SYNC_HELPER}
Nice=10
IOSchedulingClass=idle
EOF
}
gen_sync_timer() {
cat <<EOF
[Unit]
Description=${SITE} mirror daily upstream re-sync (dc-mirror.sh; D-135)
[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
EOF
}
gen_nginx_site() {
cat <<EOF
# ${SITE} mirror vhost (generated by scripts/dc-mirror.sh; D-135). Serves the
# per-DC apt+UCA mirror on the D-134 utility address. Stage-5 wiring:
# juju model-config apt-mirror=http://${LISTEN}/ubuntu (per DC model).
server {
listen ${LISTEN}:80;
root ${MIRROR_ROOT};
autoindex on;
access_log /var/log/nginx/${SITE}-mirror.access.log;
}
EOF
}
# ---------------------------------------------------------------------------
# check -- mutate NOTHING; report and exit 0 ok / 1 incomplete.
# ---------------------------------------------------------------------------
FAILED=0
say(){ echo " $1"; }
miss(){ echo " MISS $1"; FAILED=1; }
check_file() { # path gen_fn
if [ ! -f "$1" ]; then miss "$1 absent"; return; fi
if diff -q <("$2") "$1" >/dev/null 2>&1; then say "OK $1 matches"; else
miss "$1 DIFFERS from generated content"; fi
}
do_check() {
check_file "$HELPER" gen_helper
check_file "$NET_UNIT" gen_net_unit
check_file "$RESOLVED_DROPIN" gen_resolved_dropin
check_file "$SYNC_HELPER" gen_sync_helper
check_file "$SYNC_UNIT" gen_sync_unit
check_file "$SYNC_TIMER" gen_sync_timer
check_file "$NGINX_SITE" gen_nginx_site
[ -L "$NGINX_LINK" ] && say "OK $NGINX_LINK enabled" || miss "$NGINX_LINK absent"
for u in "${SITE}-mirror-net.service" "${SITE}-mirror-sync.timer"; do
[ "$(systemctl is-enabled "$u" 2>/dev/null)" = "enabled" ] \
&& say "OK $u enabled" || miss "$u not enabled"
done
br="$(virsh -c qemu:///system net-dumpxml "$MIRROR_NET" 2>/dev/null \
| sed -n "s/.*bridge name='\([^']*\)'.*/\1/p")"
if [ -n "$br" ] && ip -4 -o addr show dev "$br" 2>/dev/null | grep -q " ${LISTEN}/"; then
say "OK ${LISTEN_CIDR} on $br ($MIRROR_NET)"
else miss "${LISTEN_CIDR} absent on metal-admin bridge"; fi
ip route show default 2>/dev/null | grep -q "via ${EDGE_GW}" \
&& say "OK default route via edge ${EDGE_GW}" \
|| miss "default route via ${EDGE_GW} absent"
systemctl is-active --quiet nginx \
&& say "OK nginx active" || miss "nginx not active"
if command -v curl >/dev/null 2>&1; then
HTTP="$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://${LISTEN}/" 2>/dev/null)"
[ "$HTTP" = "200" ] && say "OK mirror answers http://${LISTEN}/ (200)" \
|| miss "mirror not answering on http://${LISTEN}/ (got '$HTTP')"
fi
[ -f "${MIRROR_ROOT}/last-sync.status" ] \
&& say "OK last-sync: $(cat "${MIRROR_ROOT}/last-sync.status")" \
|| say "note last-sync.status absent (no sync attempted yet)"
[ "$FAILED" -eq 0 ] && { echo "dc-mirror check ($SITE): PASS"; exit 0; }
echo "dc-mirror check ($SITE): FAIL"; exit 1
}
# ---------------------------------------------------------------------------
# install -- idempotent; requires root. Net layer first (the rack has no
# egress until it is up), then fail loud on missing packages.
# ---------------------------------------------------------------------------
do_install() {
[ "$(id -u)" = "0" ] || { echo "FAIL: install requires root" >&2; exit 4; }
umask 022
mkdir -p /etc/systemd/resolved.conf.d "$MIRROR_ROOT" || exit 4
gen_helper > "$HELPER" && chmod 755 "$HELPER" || exit 4
gen_net_unit > "$NET_UNIT" || exit 4
gen_resolved_dropin > "$RESOLVED_DROPIN" || exit 4
systemctl daemon-reload || exit 4
systemctl enable --now "${SITE}-mirror-net.service" >/dev/null 2>&1 || exit 4
systemctl try-restart systemd-resolved >/dev/null 2>&1
MISSING=""
command -v debmirror >/dev/null 2>&1 || MISSING="$MISSING debmirror"
command -v nginx >/dev/null 2>&1 || MISSING="$MISSING nginx"
[ -f "$ARCHIVE_KEYRING" ] || MISSING="$MISSING ubuntu-keyring"
[ -f "$CLOUD_KEYRING" ] || MISSING="$MISSING ubuntu-cloud-keyring"
if [ -n "$MISSING" ]; then
echo "FAIL: missing packages:$MISSING" >&2
echo " net layer is up -- run: apt-get update && apt-get install -y$MISSING" >&2
echo " then re-run: dc-mirror.sh install $SITE" >&2
exit 4
fi
gen_sync_helper > "$SYNC_HELPER" && chmod 755 "$SYNC_HELPER" || exit 4
gen_sync_unit > "$SYNC_UNIT" || exit 4
gen_sync_timer > "$SYNC_TIMER" || exit 4
gen_nginx_site > "$NGINX_SITE" || exit 4
ln -sf "$NGINX_SITE" "$NGINX_LINK" || exit 4
systemctl daemon-reload || exit 4
systemctl enable "${SITE}-mirror-sync.timer" >/dev/null 2>&1 || exit 4
systemctl start "${SITE}-mirror-sync.timer" >/dev/null 2>&1 || exit 4
nginx -t >/dev/null 2>&1 || { echo "FAIL: nginx config test" >&2; exit 4; }
systemctl enable --now nginx >/dev/null 2>&1
systemctl reload nginx || exit 4
echo "dc-mirror install ($SITE): done -- running check:"
echo " (initial sync NOT started -- run: dc-mirror.sh sync $SITE)"
do_check
}
# ---------------------------------------------------------------------------
# sync -- start the systemd sync job (async; hours for the initial pull).
# ---------------------------------------------------------------------------
do_sync() {
[ "$(id -u)" = "0" ] || { echo "FAIL: sync requires root" >&2; exit 4; }
systemctl start --no-block "${SITE}-mirror-sync.service" || exit 4
echo "dc-mirror sync ($SITE): started (async). Watch:"
echo " systemctl status ${SITE}-mirror-sync.service"
echo " cat ${MIRROR_ROOT}/last-sync.status"
}
case "$MODE" in
check) do_check ;;
install) do_install ;;
sync) do_sync ;;
esac