Files
spinifex/scripts/setup_host_test.sh
T
2026-10-27 16:13:42 +11:00

273 lines
12 KiB
Bash
Executable File

#!/bin/sh
# setup_host_test.sh — tests for setup.sh's DNS handover to systemd-resolved,
# its chrony drop-in and its nbdkit plugin directory.
#
# setup.sh is sourced with INSTALL_SPINIFEX_LIB_ONLY=1 so main never runs. PATH
# holds stub commands and links to a few real tools only, so no host resolver,
# package manager or service is ever touched.
# shellcheck disable=SC2016 # stub bodies are single-quoted so they expand when the stub runs
set -eu
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
SETUP="$SCRIPT_DIR/setup.sh"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
fails=0
pass() { echo " ok - $1"; }
fail() { echo " FAIL - $1"; fails=$((fails + 1)); }
echo "-- setup.sh host DNS, chrony and nbdkit"
mkdir -p "$TMP/sys"
for tool in bash awk sed grep readlink basename dirname cp ln mkdir cat head tail wc tee rm chmod; do
ln -s "$(command -v "$tool")" "$TMP/sys/$tool"
done
# stub <name> <body> — a stub command; $S is the fixture's state directory.
stub() {
printf '#!/bin/sh\nS="%s"\n%s\n' "$TMP/state" "$2" > "$TMP/bin/$1"
chmod +x "$TMP/bin/$1"
}
# host — a fresh fixture: resolved installed and inactive, resolvconf the
# resolvectl link, NetworkManager running with no DNS settings, a dnf host
# whose first enabled repository is yum.oracle.com.
host() {
rm -rf "${TMP:?}/state" "${TMP:?}/root" "${TMP:?}/bin"
mkdir -p "$TMP/state/active" "$TMP/root/etc" "$TMP/root/usr/lib/systemd/system" "$TMP/bin"
: > "$TMP/root/usr/lib/systemd/system/systemd-resolved.service"
: > "$TMP/state/active/NetworkManager.service"
printf 'nameserver 192.168.1.1\nsearch example.lan\n' > "$TMP/root/etc/resolv.conf"
printf '[main]\n# dns=default\nconfigure-and-quit=no\n' > "$TMP/state/nm-config"
printf 'Global:\nLink 2 (br-wan): 192.168.1.1\n' > "$TMP/state/resolvectl-dns"
printf '[ol10_UEKR8]\nbaseurl = https://mirror.example/uek/\nenabled = 0\n[ol10_baseos_latest]\nbaseurl = https://yum.oracle.com/repo/OracleLinux/OL10/baseos/latest/x86_64/\nenabled = 1\n' > "$TMP/state/dnf-dump"
stub systemctl '[ "$1" = is-active ] && { [ -e "$S/active/$3" ]; exit; }
echo "$*" >> "$S/calls"
[ "$1 $2" = "enable --now" ] && : > "$S/active/$3"
exit 0'
stub NetworkManager 'cat "$S/nm-config"'
stub resolvectl '[ "$1" = dns ] && cat "$S/resolvectl-dns"'
stub resolvectl-real ':'
ln -s resolvectl "$TMP/bin/resolvconf"
stub getent '[ ! -e "$S/getent-fail" ] && echo "$2" >> "$S/looked-up"'
stub dnf '[ "$1 $2" = "config-manager --dump" ] && cat "$S/dnf-dump"'
stub sleep ':'
}
# run <function...> — runs setup.sh functions against the fixture. Sets RC and OUT.
run() {
RC=0
OUT=$(PATH="$TMP/bin:$TMP/sys" DISTRO_ROOT="$TMP/root" \
SPINIFEX_RESOLV_CONF="$TMP/root/etc/resolv.conf" SPINIFEX_RESOLV_CONF_SAVED="$TMP/root/saved/resolv.conf.orig" \
INSTALL_SPINIFEX_LIB_ONLY=1 bash -c '. "$1"; shift; for f in "$@"; do $f; done' _ "$SETUP" "$@" 2>&1) || RC=$?
}
calls() { cat "$TMP/state/calls" 2>/dev/null || true; }
saved() { cat "$TMP/root/saved/resolv.conf.orig" 2>/dev/null || echo "<none>"; }
expect_untouched() {
if [ "$RC" = 0 ] && [ -z "$(calls)" ] && [ ! -L "$TMP/root/etc/resolv.conf" ] && [ "$(saved)" = "<none>" ]; then
pass "$1"
else
fail "$1: rc=$RC calls='$(calls)' saved='$(saved)': $OUT"
fi
}
expect_fatal() { # expect_fatal <desc> <text>...
desc=$1; shift
if [ "$RC" = 0 ]; then fail "$desc (succeeded): $OUT"; return; fi
for want in "$@"; do
case "$OUT" in *"$want"*) ;; *) fail "$desc (output lacks '$want'): $OUT"; return ;; esac
done
pass "$desc"
}
# --------------------------------------------------------------------------
# DNS handover.
# --------------------------------------------------------------------------
host
run hand_dns_to_resolved
if [ "$RC" = 0 ] && [ "$(readlink "$TMP/root/etc/resolv.conf")" = /run/systemd/resolve/stub-resolv.conf ] \
&& [ "$(saved)" = "$(printf 'nameserver 192.168.1.1\nsearch example.lan')" ] \
&& [ "$(calls)" = "$(printf 'enable --now systemd-resolved.service\nrestart NetworkManager.service')" ] \
&& [ "$(cat "$TMP/state/looked-up")" = yum.oracle.com ]; then
pass "hands DNS to resolved: saves resolv.conf, starts resolved, restarts NM, links the stub, looks up the repo host"
else
fail "handover: rc=$RC calls='$(calls)' saved='$(saved)' link='$(readlink "$TMP/root/etc/resolv.conf")': $OUT"
fi
: > "$TMP/state/calls"
rm -f "$TMP/state/looked-up"
run hand_dns_to_resolved
if [ "$RC" = 0 ] && [ -z "$(calls)" ] && [ "$(cat "$TMP/state/looked-up")" = yum.oracle.com ]; then
pass "a rerun after a handover checks DNS again and changes nothing"
else
fail "rerun after handover: rc=$RC calls='$(calls)': $OUT"
fi
host
mkdir -p "$TMP/root/saved"
echo "the original" > "$TMP/root/saved/resolv.conf.orig"
run hand_dns_to_resolved
if [ "$RC" = 0 ] && [ "$(saved)" = "the original" ]; then
pass "a rerun never overwrites the saved resolv.conf"
else
fail "saved copy overwritten: rc=$RC saved='$(saved)': $OUT"
fi
host
rm "$TMP/root/usr/lib/systemd/system/systemd-resolved.service"
run hand_dns_to_resolved
expect_untouched "resolved not installed: DNS is left alone"
host
: > "$TMP/state/active/systemd-resolved.service"
run hand_dns_to_resolved
expect_untouched "resolved already active, with no earlier handover: DNS is left alone"
host
rm "$TMP/bin/resolvconf"
stub resolvconf ':'
run hand_dns_to_resolved
expect_untouched "a real resolvconf: DNS is left alone"
host
rm "$TMP/state/active/NetworkManager.service"
run hand_dns_to_resolved
case "$OUT" in
*"NetworkManager is not running"*) expect_untouched "NetworkManager not running: DNS is left alone, with a warning" ;;
*) fail "NetworkManager not running: no warning: $OUT" ;;
esac
for setting in dns=none dns=dnsmasq systemd-resolved=false; do
host
printf '[main]\n# dns=default\n%s\n[logging]\ndns=none\n' "$setting" > "$TMP/state/nm-config"
run hand_dns_to_resolved
expect_fatal "NetworkManager $setting stops the install, naming it" "NetworkManager sets $setting"
[ -z "$(calls)" ] || fail "NetworkManager $setting: acted before refusing: $(calls)"
done
host
printf '[main]\ndns=systemd-resolved\n[logging]\ndns=none\n' > "$TMP/state/nm-config"
run hand_dns_to_resolved
[ "$RC" = 0 ] && pass "dns=systemd-resolved, and dns= outside [main], are accepted" || fail "dns=systemd-resolved refused: $OUT"
host
printf 'Global:\nLink 2 (br-wan):\n' > "$TMP/state/resolvectl-dns"
run hand_dns_to_resolved
expect_fatal "no upstream servers stops the install, naming the check and printing the restore" \
"'resolvectl dns' lists no DNS servers" "systemctl disable --now systemd-resolved" "cp -a --remove-destination $TMP/root/saved/resolv.conf.orig"
host
: > "$TMP/state/getent-fail"
run hand_dns_to_resolved
expect_fatal "a repository host that no longer resolves stops the install, naming it" "yum.oracle.com no longer resolves" "systemctl restart NetworkManager"
host
printf '[local]\nbaseurl = file:///srv/repo/\nenabled = 1\n' > "$TMP/state/dnf-dump"
run hand_dns_to_resolved
case "$RC:$OUT" in
0:*"not checking that names resolve"*) pass "a file:// repository skips the name check with a warning" ;;
*) fail "file:// repository: rc=$RC: $OUT" ;;
esac
host
printf '[local]\nbaseurl = http://10.0.0.5/repo/\nenabled = 1\n' > "$TMP/state/dnf-dump"
run hand_dns_to_resolved
case "$RC:$OUT" in
0:*"10.0.0.5 is an IP address"*) pass "a repository at an IP address skips the name check with a warning" ;;
*) fail "IP repository: rc=$RC: $OUT" ;;
esac
# --------------------------------------------------------------------------
# Repository URLs and their hosts.
# --------------------------------------------------------------------------
host
printf '[a]\nbaseurl = \nenabled = 1\nmirrorlist = https://mirrors.example/list\n[b]\nbaseurl = https://b.example/\nenabled = 1\n' > "$TMP/state/dnf-dump"
run first_repo_url
[ "$OUT" = https://mirrors.example/list ] && pass "dnf: a mirrorlist stands in for an empty baseurl" || fail "dnf mirrorlist: '$OUT'"
printf '[a]\nbaseurl = https://a1.example/, https://a2.example/\nenabled = 1\n' > "$TMP/state/dnf-dump"
run first_repo_url
[ "$OUT" = https://a1.example/ ] && pass "dnf: the first of several baseurls" || fail "dnf baseurl list: '$OUT'"
host
rm "$TMP/bin/dnf"
stub apt-get '[ "$1" = indextargets ] && printf "http://deb.debian.org/debian/\nhttp://deb.debian.org/debian-security/\n"'
run first_repo_url
[ "$OUT" = http://deb.debian.org/debian/ ] && pass "apt: the first repository URI" || fail "apt: '$OUT'"
for case in "https://yum.oracle.com/repo/=yum.oracle.com" "http://user@mirror.lan:8080/x=mirror.lan" \
"http://[2001:db8::1]:80/x=[2001:db8::1]" "file:///srv/repo=" "cdrom:[Debian]/=" "=" ; do
url=${case%=*}
want=${case##*=}
run "url_host $url"
[ "$OUT" = "$want" ] && pass "url_host '$url' is '$want'" || fail "url_host '$url' = '$OUT', want '$want'"
done
# --------------------------------------------------------------------------
# chrony drop-in.
# --------------------------------------------------------------------------
host
mkdir -p "$TMP/root/etc/chrony"
printf 'pool 2.debian.pool.ntp.org iburst\nmakestep 1 3\nconfdir /etc/chrony/conf.d\n' > "$TMP/root/etc/chrony/chrony.conf"
before=$(cat "$TMP/root/etc/chrony/chrony.conf")
run install_chrony_conf
if [ "$RC" = 0 ] && grep -qx 'makestep 1 -1' "$TMP/root/etc/chrony/conf.d/spinifex.conf" 2>/dev/null \
&& [ "$(cat "$TMP/root/etc/chrony/chrony.conf")" = "$before" ]; then
case "$OUT" in
*WARN*) fail "Debian chrony layout warned: $OUT" ;;
*) pass "Debian: the drop-in goes in the confdir, chrony.conf is not edited" ;;
esac
else
fail "Debian chrony: rc=$RC: $OUT"
fi
host
printf 'pool 2.pool.ntp.org iburst\nmakestep 1.0 3\nrtcsync' > "$TMP/root/etc/chrony.conf"
run install_chrony_conf
run install_chrony_conf
if [ "$RC" = 0 ] && grep -qx 'makestep 1 -1' "$TMP/root/etc/chrony.d/spinifex.conf" 2>/dev/null \
&& [ "$(grep -c '^confdir' "$TMP/root/etc/chrony.conf")" = 1 ] \
&& [ "$(tail -n 1 "$TMP/root/etc/chrony.conf")" = "confdir /etc/chrony.d" ] \
&& grep -qx rtcsync "$TMP/root/etc/chrony.conf"; then
pass "EL: one confdir line is appended after the stock makestep, and the drop-in goes there"
else
fail "EL chrony: rc=$RC: $OUT: $(cat "$TMP/root/etc/chrony.conf")"
fi
host
printf 'confdir /etc/chrony.d\nmakestep 1.0 3\n' > "$TMP/root/etc/chrony.conf"
run install_chrony_conf
case "$RC:$OUT" in
0:*"makestep on line 2, after its confdir line"*) pass "a makestep after the confdir line warns that it overrides the drop-in" ;;
*) fail "late makestep: rc=$RC: $OUT" ;;
esac
host
run install_chrony_conf
case "$RC:$OUT" in
0:*"Skipping the chrony drop-in"*) pass "no chrony config: the drop-in is skipped with a warning" ;;
*) fail "no chrony: rc=$RC: $OUT" ;;
esac
# --------------------------------------------------------------------------
# nbdkit plugin directory.
# --------------------------------------------------------------------------
host
stub nbdkit '[ "$1" = --dump-config ] && printf "binary=/usr/sbin/nbdkit\nplugindir=/usr/lib64/nbdkit/plugins\n"'
OUT=$(PATH="$TMP/bin:$TMP/sys" INSTALL_SPINIFEX_LIB_ONLY=1 bash -c '. "$1"; resolve_nbdkit_plugindir; echo "DIR=$NBDKIT_PLUGINDIR"' _ "$SETUP" 2>&1) && RC=0 || RC=$?
[ "$OUT" = DIR=/usr/lib64/nbdkit/plugins ] && pass "the plugin directory comes from nbdkit --dump-config" || fail "nbdkit plugindir: rc=$RC: $OUT"
host
stub nbdkit 'exit 1'
run resolve_nbdkit_plugindir
expect_fatal "a broken nbdkit stops the install, naming it" "nbdkit --dump-config"
host
run resolve_nbdkit_plugindir
expect_fatal "no nbdkit stops the install, naming it" "nbdkit --dump-config"
if [ "$fails" -gt 0 ]; then
echo "$fails failure(s)"
exit 1
fi