diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1e3c1f0..f8b7a09 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -77,11 +77,15 @@ gate: rehearsal exception REJECTED -- **D-135 ADOPTED** (per-DC mirror BUILT on the rack hosts at the D-134 utility .4 address; item 1 apt+UCA debmirror+nginx builds in-stage; items 2-3 pinned - to Stage 5; egress narrowing is the closing mutation). NEXT: - build+install dc-mirror item 1 (script+harness, gated installs, - initial sync) -> Step 6/7/8 verify closure -> lib-hosts.sh per-DC - population + lib-net VID comment (delivery items) -> mirror - reachability gate -> stage close-out set. + to Stage 5; egress narrowing is the closing mutation). + **dc-mirror.sh SHIPPED 2026-07-23** (site-keyed check/install/sync; + D-134 utility .4 + edge default route -- egress path MEASURED + working, edge DNS answers; debmirror GPG-verified jammy triple + + UCA caracal; harness 18/18; gauntlet **ALL GREEN (77)**, + `docs/audit/gauntlet-20260723-stage4-dcmirror.txt`). NEXT: gated + installs on both racks + initial sync -> Step 6/7/8 verify closure + -> lib-hosts.sh per-DC population + lib-net VID comment (delivery + items) -> mirror reachability gate -> stage close-out set. - Position inside Stage 3: deploy step A EXECUTED 2026-07-19 (6/0/6 exact; convergence zero -- `docs/audit/outer-plan-20260719-postA-converged.txt`). **Deploy step B diff --git a/docs/audit/gauntlet-20260723-stage4-dcmirror.txt b/docs/audit/gauntlet-20260723-stage4-dcmirror.txt new file mode 100644 index 0000000..56489e4 --- /dev/null +++ b/docs/audit/gauntlet-20260723-stage4-dcmirror.txt @@ -0,0 +1,79 @@ + carve-host-interfaces ALL PASS + checks ALL PASS (62 checks) + claude-guard ALL PASS + clientdocs-scripts ALL PASS + clientdocs-skill ALL PASS + cloud-assert ALL PASS + cloudinit-vm cloudinit-vm: 10 passed, 0 failed + cloud-snapshot ALL PASS + creds-audit creds-audit: 7/7 PASS + d063-apply ALL PASS (7/7) + d115-office-carve d115-office-carve: 20/20 PASS + d120-compose-bands d120-compose-bands: 16/16 PASS + d124-transit-seed d124-transit-seed: 21/21 PASS + dc-dc-ceph-disk-budget ALL PASS + dc-dc-dr-drill ALL PASS + dc-dc-mtu-geneve-budget ALL PASS + dc-dc-prefixes-import ALL PASS (91 checks) + dc-dc-radosgw-multisite ALL PASS + dc-dc-rbd-mirror ALL PASS + dc-dc-whole-host-budget dc-dc-whole-host-budget: 13/13 PASS + dc-edge-wan-import dc-edge-wan-import: 58/58 PASS + dc-mirror dc-mirror: 18 passed, 0 failed + dc-rack-mgmt-import dc-rack-mgmt-import: 117/117 PASS + dc-rack-net dc-rack-net: 18 passed, 0 failed + dc-selector ALL PASS (40 checks) + juju-spaces-check ALL PASS (5/5) + keystone-policy-drift ALL PASS + ledger-scan ALL PASS (48 checks) + lib-validate ALL PASS (35 checks) + maas-node-power maas-node-power: 24/24 PASS + netem-link netem-link: 12 passed, 0 failed + netem-sudoers netem-sudoers: 9 passed, 0 failed + node-vm node-vm: 15 passed, 0 failed + opentofu-validate ALL PASS + opnsense-api ALL PASS + opnsense-bootstrap-apikey ALL PASS + opnsense-plugins opnsense-plugins: 20 passed, 0 failed + opnsense-prep-image ALL PASS + opnsense-set-interface-v4 opnsense-set-interface-v4: 53/53 PASS + opnsense-set-interface-v6 opnsense-set-interface-v6: 40/40 PASS + phase-00-maas-standup ALL PASS + phase-00-teardown-d061 ALL PASS + phase-02 ALL PASS + phase-03-adminrc ALL PASS + phase-03 ALL PASS + phase-04-create ALL PASS + phase-04-internal-cert-san ALL PASS + phase-04 ALL PASS + phase-05-amphora ALL PASS + phase-05 ALL PASS + phase-06-bootstrap ALL PASS + phase-06-capi-stack ALL PASS + phase-06-k8s-bootstrap ALL PASS + phase-06-kubeconfig-gate ALL PASS + phase-06-mgmt-vm ALL PASS + phase-06-net-setup ALL PASS + phase-07-conductor-graft ALL PASS + preflight ALL PASS + prereqs prereqs: 32/32 PASS + provider-bundle-check ALL PASS + record-audit record-audit: 35/35 PASS + reenroll-hosts ALL PASS + repo-lint ALL PASS + roles-aggregates-import roles-aggregates-import: 24/24 PASS + sandbox-fidelity-check sandbox-fidelity-check: 14/14 PASS + sandbox-seed sandbox-seed: 22/22 PASS + site-baseleg site-baseleg: 24 passed, 0 failed + site-forward site-forward: 38/38 PASS + site-headend-install site-headend-install: 59/59 PASS + site-ssh-config site-ssh-config: 30/30 PASS + tenant-acceptance ALL PASS + tenant-assert ALL PASS + tenant-offboard ALL PASS (22/22) + tenant-onboard ALL PASS + trust_filter ALL PASS + validate ALL PASS (29 checks) + vault-kv-health ALL PASS (10/10) + +GAUNTLET: ALL GREEN (77 harnesses) diff --git a/scripts/dc-mirror.sh b/scripts/dc-mirror.sh new file mode 100755 index 0000000..be942b1 --- /dev/null +++ b/scripts/dc-mirror.sh @@ -0,0 +1,320 @@ +#!/usr/bin/env bash +# scripts/dc-mirror.sh -- 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 -J voffice1 @ \ +# '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. -mirror-net.service -- oneshot, After=-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/-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/-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. -mirror-sync.service (oneshot) + -mirror-sync.timer (daily, +# randomized) -- the upstream re-sync. +# 5. /etc/nginx/sites-available/-mirror (+ sites-enabled symlink) -- +# serves /var/lib/dc-mirror on http:/// (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 " >&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 < "\$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 </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 diff --git a/tests/dc-mirror/run-tests.sh b/tests/dc-mirror/run-tests.sh new file mode 100755 index 0000000..c0f42aa --- /dev/null +++ b/tests/dc-mirror/run-tests.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# tests/dc-mirror/run-tests.sh -- guard for scripts/dc-mirror.sh (D-135 item 1: +# the per-DC apt+UCA mirror, D-107 build-out). Static: syntax, both sites' +# identity values (D-134 utility .4 + ruled edge gateways -- changing one +# silently breaks the mirror address or the sync egress), stable-identity +# keying (libvirt net names, never virbrN), GPG-verified sync (keyrings +# present in both debmirror calls, no --ignore-release-gpg), read-only check +# path, and the MEASURED-tag discipline on site rows. +# Exit: 0 all pass | 1 any case failed. ASCII + LF. +set -uo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$(cd "$HERE/../.." && pwd)/scripts/dc-mirror.sh" +PASS=0; FAIL=0 +ok(){ echo " PASS $1"; PASS=$((PASS+1)); } +no(){ echo " FAIL $1"; FAIL=$((FAIL+1)); } + +[ -f "$SCRIPT" ] && ok "T1 script present" || { no "T1 script present"; echo; echo "dc-mirror: $PASS passed, $FAIL failed"; exit 1; } +bash -n "$SCRIPT" 2>/dev/null && ok "T2 bash -n clean" || no "T2 bash -n clean" + +# T3-T4: dc0 identity values (D-134 utility .4 + ruled edge gw) +grep -q 'LISTEN_CIDR="10.12.8.4/22"' "$SCRIPT" \ + && grep -q 'MIRROR_NET="vr1-dc0-metal-admin"' "$SCRIPT" \ + && ok "T3 dc0 LISTEN 10.12.8.4/22 on metal-admin (utility .4)" \ + || no "T3 dc0 LISTEN 10.12.8.4/22 on metal-admin (utility .4)" +awk '/^ dc0\)/{f=1} f{print} f&&/;;/{exit}' "$SCRIPT" | grep -q 'EDGE_GW="10.12.4.1"' \ + && ok "T4 dc0 edge gw 10.12.4.1" || no "T4 dc0 edge gw 10.12.4.1" + +# T5-T6: dc1 identity values +grep -q 'LISTEN_CIDR="10.12.68.4/22"' "$SCRIPT" \ + && grep -q 'MIRROR_NET="vr1-dc1-metal-admin"' "$SCRIPT" \ + && ok "T5 dc1 LISTEN 10.12.68.4/22 on metal-admin (utility .4)" \ + || no "T5 dc1 LISTEN 10.12.68.4/22 on metal-admin (utility .4)" +awk '/^ dc1\)/{f=1} f{print} f&&/;;/{exit}' "$SCRIPT" | grep -q 'EDGE_GW="10.12.64.1"' \ + && ok "T6 dc1 edge gw 10.12.64.1" || no "T6 dc1 edge gw 10.12.64.1" + +# T7: stable-identity keying -- no virbrN literal; bridges from net names +grep -q 'net-dumpxml' "$SCRIPT" && ! grep -q 'virbr[0-9]' "$SCRIPT" \ + && ok "T7 bridges resolved from net names; no virbrN literal" \ + || no "T7 bridges resolved from net names; no virbrN literal" + +# T8: both debmirror calls carry a keyring; gpg verification never disabled +KEYRING_COUNT="$(grep -c -- '--keyring=' "$SCRIPT")" +[ "$KEYRING_COUNT" -ge 2 ] && ! grep -q 'ignore-release-gpg' "$SCRIPT" \ + && ok "T8 both debmirror calls GPG-verified (no ignore-release-gpg)" \ + || no "T8 both debmirror calls GPG-verified (keyrings=$KEYRING_COUNT)" + +# T9: binary-only mirror -- no sources pulled +[ "$(grep -c -- '--nosource' "$SCRIPT")" -ge 2 ] \ + && ok "T9 both mirrors --nosource (binary only)" \ + || no "T9 both mirrors --nosource (binary only)" + +# T10: the ruled dist set -- jammy triple + UCA caracal +grep -q -- '--dist=jammy,jammy-updates,jammy-security' "$SCRIPT" \ + && grep -q -- '--dist=jammy-updates/caracal' "$SCRIPT" \ + && ok "T10 dists: jammy triple + UCA jammy-updates/caracal" \ + || no "T10 dists: jammy triple + UCA jammy-updates/caracal" + +# T11: unknown site refuses with exit 2 and demands measured rows +OUT="$(bash "$SCRIPT" check nosuchsite 2>&1)"; RC=$? +[ "$RC" -eq 2 ] && grep -q 'MEASURED' <<<"$OUT" \ + && ok "T11 unknown site exits 2 and demands measured rows" \ + || no "T11 unknown site exits 2 and demands measured rows (rc=$RC)" + +# T12: bad mode refuses +bash "$SCRIPT" frobnicate dc0 >/dev/null 2>&1; RC=$? +[ "$RC" -eq 2 ] && ok "T12 bad mode exits 2" || no "T12 bad mode exits 2 (rc=$RC)" + +# T13: sync unit ordered after + requires the net unit (egress must exist) +grep -q 'After=\${SITE}-mirror-net.service' "$SCRIPT" \ + && grep -q 'Requires=\${SITE}-mirror-net.service' "$SCRIPT" \ + && ok "T13 sync unit Requires/After net unit" \ + || no "T13 sync unit Requires/After net unit" + +# T14: check path never mutates +CHECK_BLOCK="$(awk '/^do_check\(\)/{f=1} f{print} f&&/^}/{exit}' "$SCRIPT")" +grep -qE 'addr (add|replace|del)|route (add|replace|del)|systemctl (enable|restart|start|disable|reload)|daemon-reload|ln -|mkdir' <<<"$CHECK_BLOCK" \ + && no "T14 do_check is read-only" \ + || ok "T14 do_check is read-only" + +# T15: install requires root (non-root exits 4 before any mutation) +if [ "$(id -u)" != "0" ]; then + bash "$SCRIPT" install dc0 >/dev/null 2>&1; RC=$? + [ "$RC" -eq 4 ] && ok "T15 non-root install exits 4" || no "T15 non-root install exits 4 (rc=$RC)" +else + ok "T15 skipped (running as root)" +fi + +# T16: site rows carry MEASURED citations (hard rule 2) +DC0_BLOCK="$(awk '/^ dc0\)/{f=1} f{print} f&&/;;/{exit}' "$SCRIPT")" +DC1_BLOCK="$(awk '/^ dc1\)/{f=1} f{print} f&&/;;/{exit}' "$SCRIPT")" +grep -q 'MEASURED' <<<"$DC0_BLOCK" && grep -q 'MEASURED' <<<"$DC1_BLOCK" \ + && ok "T16 both site rows cite MEASURED provenance" \ + || no "T16 both site rows cite MEASURED provenance" + +# T17: nginx vhost binds the LISTEN address only (not 0.0.0.0) +grep -q 'listen \${LISTEN}:80;' "$SCRIPT" \ + && ok "T17 nginx binds the utility address only" \ + || no "T17 nginx binds the utility address only" + +# T18: sync is niced/idle-IO (a background job on a load-bearing rack host) +grep -q 'IOSchedulingClass=idle' "$SCRIPT" && grep -q 'Nice=10' "$SCRIPT" \ + && ok "T18 sync unit is niced + idle IO" \ + || no "T18 sync unit is niced + idle IO" + +echo; echo "dc-mirror: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ]