Files
spinifex/scripts/setup_host_test.sh
T
Ben Duncan 06f93802b8 fix: keep needrestart from restarting the Spinifex, OVS and OVN units (#1341)
Guest QEMU runs in spinifex-daemon's cgroup, so needrestart charged QEMU's
replaced libraries to the daemon and restarted it after every dpkg run. The
restart never cleared them, so an unattended upgrade ran the unit into its
start limit and left the control plane down.

setup.sh now writes /etc/needrestart/conf.d/90-spinifex.conf, deselecting
spinifex-, ovs-, ovsdb-, ovn- and openvswitch- units. needrestart still lists
them, so restarting after an upgrade stays the operator's choice.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:17:15 -05:00

317 lines
13 KiB
Bash
Executable File

#!/bin/sh
# setup_host_test.sh — tests for setup.sh's DNS handover to systemd-resolved,
# its chrony and needrestart drop-ins 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, needrestart 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 install; 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
# --------------------------------------------------------------------------
# needrestart override.
# --------------------------------------------------------------------------
# restarts <conf> <unit> — how needrestart would treat <unit> under <conf>:
# "no" when an override_rc pattern deselects it, "yes" otherwise.
restarts() {
perl -e 'our %nrconf; do $ARGV[0]; die $@ if $@;
for my $re (keys %{$nrconf{override_rc}}) {
if ($ARGV[1] =~ $re) { print $nrconf{override_rc}{$re} ? "yes" : "no"; exit }
}
print "yes"' "$1" "$2"
}
host
run install_needrestart_conf
conf="$TMP/root/etc/needrestart/conf.d/90-spinifex.conf"
if [ "$RC" != 0 ] || [ ! -f "$conf" ]; then
fail "needrestart: rc=$RC: $OUT"
elif ! command -v perl > /dev/null; then
fail "needrestart: perl is not installed, so the override cannot be checked"
else
wrong=""
for unit in spinifex-daemon.service spinifex-viperblock.service ovs-vswitchd.service \
ovsdb-server.service ovn-controller.service openvswitch-switch.service; do
[ "$(restarts "$conf" "$unit")" = no ] || wrong="$wrong $unit"
done
for unit in ssh.service rsyslog.service polkit.service; do
[ "$(restarts "$conf" "$unit")" = yes ] || wrong="$wrong $unit"
done
if [ -n "$wrong" ]; then
fail "needrestart override treats these wrongly:$wrong"
else
pass "needrestart keeps off the stack's units and still restarts everything else"
fi
fi
before=$(cat "$conf" 2>/dev/null)
run install_needrestart_conf
if [ "$RC" = 0 ] && [ "$(cat "$conf" 2>/dev/null)" = "$before" ]; then
pass "needrestart override is unchanged by a second install"
else
fail "needrestart override changed on re-run: rc=$RC: $OUT"
fi
# --------------------------------------------------------------------------
# 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