feat: Oracle Linux supported-OS gate and host capability probes

setup.sh accepts Oracle Linux 10.2+ (<11) on x86_64 with a running UEK R8
kernel and an x86-64-v3 CPU, beside Debian 13+ and Ubuntu 24.04+, and parses
os-release instead of sourcing it. New scripts/lib/distro.sh probes the
package manager, OVS umbrella unit, OVN central layout, CA trust store and
OVN logrotate file; it ships in the release tarball, is installed to
/usr/local/share/spinifex/lib/, and is sourced by setup-ovn.sh,
uninstall-spx.sh and dev-install.sh. setup.sh and node-reset.sh carry inline
copies, kept identical by scripts/lib/distro_test.sh.

New runtime/host/hostos resolves the QEMU binary, CA trust store and boot
tools by probing, naming every candidate on failure. Callers are wired in
later stages.
This commit is contained in:
Julian Sommer
2026-10-21 14:11:54 +10:00
parent f707df92e7
commit 520478dc37
13 changed files with 1167 additions and 9 deletions
+3 -3
View File
@@ -167,7 +167,7 @@ preflight:
# reason: between them they install the software, wire OVN, erase the state and
# remove the install, so a shellcheck warning in one is an outage or a wrong
# deletion, not a style note.
NODE_SCRIPTS := scripts/setup.sh scripts/setup-ovn.sh scripts/install-node.sh scripts/smoke-test.sh scripts/node-reset.sh scripts/uninstall-spx.sh
NODE_SCRIPTS := scripts/setup.sh scripts/setup-ovn.sh scripts/install-node.sh scripts/smoke-test.sh scripts/node-reset.sh scripts/uninstall-spx.sh scripts/lib/distro.sh
test-build-scripts:
@echo -e "\n....Running build/scripts/**/*_test.sh...."
@@ -175,8 +175,8 @@ test-build-scripts:
echo "-- $$t"; \
sh "$$t" || exit 1; \
done
@echo -e "\n....Running scripts/*_test.sh...."
@for t in $$(find scripts -maxdepth 1 -name '*_test.sh' | sort); do \
@echo -e "\n....Running scripts/*_test.sh and scripts/lib/*_test.sh...."
@for t in $$( (find scripts -maxdepth 1 -name '*_test.sh'; find scripts/lib -name '*_test.sh') | sort); do \
echo "-- $$t"; \
sh "$$t" || exit 1; \
done
+1
View File
@@ -45,6 +45,7 @@ RUN cd viperblock && GOFIPS140=v1.0.0 go build -o /out/nbdkit-viperblock-plugin.
# Copy setup scripts and systemd units
RUN cp spinifex/scripts/setup.sh /out/setup.sh 2>/dev/null || true
RUN cp spinifex/scripts/setup-ovn.sh /out/setup-ovn.sh
RUN mkdir -p /out/lib && cp spinifex/scripts/lib/distro.sh /out/lib/distro.sh
RUN mkdir -p /out/systemd && cp spinifex/build/systemd/* /out/systemd/
RUN mkdir -p /out/scripts && cp spinifex/build/scripts/* /out/scripts/
RUN cp spinifex/build/logrotate/spinifex /out/logrotate-spinifex
+18
View File
@@ -12,6 +12,24 @@ Node installation, dev-environment lifecycle, guest image builds, and verificati
| `dev-install.sh` | Full local dev setup through the production installer: builds from source, packs a tarball, runs `setup.sh`, initialises the cluster, and starts services under systemd. |
| `clone-deps.sh` | Clones or updates the viperblock and predastore checkouts alongside spinifex for cross-repo development. |
### `lib/distro.sh`: host-OS capability probes
The node scripts select host-OS behaviour by probing for the capability, not by reading `/etc/os-release`; only `setup.sh`'s supported-OS gate keys on the OS ID.
Each probe prints its answer, or names every candidate it tried on stderr and returns 1, which it also does when it finds both candidates of a pair.
| Probe | Answers |
|---|---|
| `distro_pkg_manager` | `apt` or `dnf` |
| `distro_ovs_unit` | `openvswitch-switch.service` (Debian, Ubuntu) or `openvswitch.service` (EL) |
| `distro_ovn_central_layout` | `split` (Debian's `ovn-central` plus separate NB and SB units) or `lone-northd` (EL's single `ovn-northd.service`) |
| `distro_ca_trust` | the trust-store update command and its anchor directory |
| `distro_ovn_logrotate` | `/etc/logrotate.d/ovn-common` or `/etc/logrotate.d/ovn` |
`setup-ovn.sh`, `uninstall-spx.sh` and `dev-install.sh` source it from `lib/` beside themselves, then from `/usr/local/share/spinifex/lib/distro.sh`, where `setup.sh` installs it, and stop naming both paths when neither exists.
`setup.sh` is piped from the install endpoint before the tarball is downloaded, and `install-node.sh --wipe` copies `node-reset.sh` alone to each host, so those two carry inline copies of the probes they need between `# --- BEGIN distro.sh probes ---` markers.
`lib/distro_test.sh` fails when an inline copy differs from the library.
Go code uses `spinifex/runtime/host/hostos` for the same purpose.
## Teardown
Both ship to `/usr/local/share/spinifex/` on every node, so they are available on a host installed with `curl … | bash` and not only from a checkout. They differ in what they take away:
+15
View File
@@ -19,6 +19,19 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MULGA_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
# Host-OS capability probes: beside this script in a checkout and in the
# installed share dir, else where setup.sh installs them.
DISTRO_LIB=""
for candidate in "$(dirname "${BASH_SOURCE[0]}")/lib/distro.sh" /usr/local/share/spinifex/lib/distro.sh; do
if [ -f "$candidate" ]; then DISTRO_LIB="$candidate"; break; fi
done
if [ -z "$DISTRO_LIB" ]; then
echo "ERROR: distro.sh not found at $(dirname "${BASH_SOURCE[0]}")/lib/distro.sh or /usr/local/share/spinifex/lib/distro.sh; re-run setup.sh to install it" >&2
exit 1
fi
# shellcheck source=lib/distro.sh
. "$DISTRO_LIB"
echo "=== Building ==="
cd "$PROJECT_ROOT" && make build
cd "$MULGA_ROOT/viperblock" && make go_build_nbd
@@ -31,6 +44,8 @@ cp "$MULGA_ROOT/viperblock/lib/nbdkit-viperblock-plugin.so" "$STAGING/"
cp "$PROJECT_ROOT/scripts/setup-ovn.sh" "$STAGING/"
cp "$PROJECT_ROOT/scripts/node-reset.sh" "$STAGING/"
cp "$PROJECT_ROOT/scripts/uninstall-spx.sh" "$STAGING/"
mkdir -p "$STAGING/lib"
cp "$PROJECT_ROOT/scripts/lib/distro.sh" "$STAGING/lib/"
mkdir -p "$STAGING/systemd"
cp "$PROJECT_ROOT/build/systemd/"* "$STAGING/systemd/"
mkdir -p "$STAGING/scripts"
+105
View File
@@ -0,0 +1,105 @@
# shellcheck shell=bash
# distro.sh — host-OS capability probes. Each prints its answer, or names every
# candidate it tried on stderr and returns 1, also when it finds both of a pair.
# setup.sh and node-reset.sh carry inline copies; distro_test.sh keeps them equal.
# Reads unit files off disk, not through systemctl, so it answers in a chroot.
# DISTRO_ROOT prefixes every host path the probes read; tests set it.
distro_unit_exists() {
local dir
for dir in /etc/systemd/system /run/systemd/system /usr/lib/systemd/system /lib/systemd/system; do
if [ -e "${DISTRO_ROOT:-}$dir/$1" ] || [ -L "${DISTRO_ROOT:-}$dir/$1" ]; then
return 0
fi
done
return 1
}
distro_pkg_manager() {
local apt=false dnf=false
command -v apt-get >/dev/null 2>&1 && apt=true
command -v dnf >/dev/null 2>&1 && dnf=true
if $apt && $dnf; then
echo "distro.sh: package manager: found both apt-get and dnf; cannot tell which one manages this host" >&2
return 1
fi
if $apt; then echo apt; return 0; fi
if $dnf; then echo dnf; return 0; fi
echo "distro.sh: package manager: found neither apt-get nor dnf on PATH" >&2
return 1
}
# The OVS umbrella unit: openvswitch-switch.service on Debian, openvswitch.service on EL.
distro_ovs_unit() {
local deb=false el=false
distro_unit_exists openvswitch-switch.service && deb=true
distro_unit_exists openvswitch.service && el=true
if $deb && $el; then
echo "distro.sh: OVS unit: found both openvswitch-switch.service and openvswitch.service; cannot tell which one owns OVS" >&2
return 1
fi
if $deb; then echo openvswitch-switch.service; return 0; fi
if $el; then echo openvswitch.service; return 0; fi
echo "distro.sh: OVS unit: found neither openvswitch-switch.service nor openvswitch.service; is Open vSwitch installed?" >&2
return 1
}
# "split": Debian's ovn-central plus separate NB and SB DB units.
# "lone-northd": EL's single ovn-northd.service, which starts both DB servers.
distro_ovn_central_layout() {
local split=false lone=false unit missing=""
for unit in ovn-central.service ovn-ovsdb-server-nb.service ovn-ovsdb-server-sb.service; do
if distro_unit_exists "$unit"; then
split=true
else
missing="${missing:+$missing, }$unit"
fi
done
if distro_unit_exists ovn-northd.service && [ -f "${DISTRO_ROOT:-}/etc/sysconfig/ovn" ]; then
lone=true
fi
if $split && $lone; then
echo "distro.sh: OVN central layout: found both the split units (ovn-central.service) and a lone ovn-northd.service with /etc/sysconfig/ovn; cannot tell which one owns the OVN databases" >&2
return 1
fi
if $split; then
if [ -n "$missing" ]; then
echo "distro.sh: OVN central layout: ovn-central is installed but $missing is missing" >&2
return 1
fi
echo split
return 0
fi
if $lone; then echo lone-northd; return 0; fi
echo "distro.sh: OVN central layout: found neither ovn-central.service nor ovn-northd.service with /etc/sysconfig/ovn; is OVN central installed?" >&2
return 1
}
# The trust-store update command and its local anchor directory, space-separated.
distro_ca_trust() {
local deb=false el=false
command -v update-ca-certificates >/dev/null 2>&1 && deb=true
command -v update-ca-trust >/dev/null 2>&1 && el=true
if $deb && $el; then
echo "distro.sh: CA trust: found both update-ca-certificates and update-ca-trust; cannot tell which trust store TLS clients read" >&2
return 1
fi
if $deb; then echo "update-ca-certificates /usr/local/share/ca-certificates"; return 0; fi
if $el; then echo "update-ca-trust /etc/pki/ca-trust/source/anchors"; return 0; fi
echo "distro.sh: CA trust: found neither update-ca-certificates nor update-ca-trust on PATH" >&2
return 1
}
distro_ovn_logrotate() {
local deb=false el=false
[ -f "${DISTRO_ROOT:-}/etc/logrotate.d/ovn-common" ] && deb=true
[ -f "${DISTRO_ROOT:-}/etc/logrotate.d/ovn" ] && el=true
if $deb && $el; then
echo "distro.sh: OVN logrotate: found both /etc/logrotate.d/ovn-common and /etc/logrotate.d/ovn; cannot tell which one the OVN packages own" >&2
return 1
fi
if $deb; then echo /etc/logrotate.d/ovn-common; return 0; fi
if $el; then echo /etc/logrotate.d/ovn; return 0; fi
echo "distro.sh: OVN logrotate: found neither /etc/logrotate.d/ovn-common nor /etc/logrotate.d/ovn; is OVN installed?" >&2
return 1
}
+201
View File
@@ -0,0 +1,201 @@
#!/bin/sh
# distro_test.sh — tests for the host-OS probes and the inline copies of them.
#
# Every probe runs against a fixture tree (DISTRO_ROOT) and a PATH holding only
# stub commands, so the result never depends on the distro running the test.
set -eu
LIB_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
SCRIPTS_DIR=$(dirname -- "$LIB_DIR")
DISTRO="$LIB_DIR/distro.sh"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
fails=0
pass() { echo " ok - $1"; }
fail() { echo " FAIL - $1"; fails=$((fails + 1)); }
echo "-- lib/distro.sh"
# fixture <name> — a fresh empty host root and stub bin dir.
fixture() {
ROOT="$TMP/$1/root"
BIN="$TMP/$1/bin"
mkdir -p "$ROOT" "$BIN"
}
unit() { mkdir -p "$ROOT/usr/lib/systemd/system" && : > "$ROOT/usr/lib/systemd/system/$1"; }
file() { mkdir -p "$ROOT$(dirname "$1")" && : > "$ROOT$1"; }
cmd() { printf '#!/bin/sh\nexit 0\n' > "$BIN/$1" && chmod +x "$BIN/$1"; }
# probe <fn> — runs one probe in bash against the current fixture, with only
# the stub dir on PATH. Sets OUT, ERR and RC.
probe() {
RC=0
OUT=$(DISTRO_ROOT="$ROOT" PATH="$BIN" /bin/bash -c '. "$1"; "$2"' _ "$DISTRO" "$1" 2>"$TMP/err") || RC=$?
ERR=$(cat "$TMP/err")
}
# expect_ok <desc> <want>; expect_fail <desc> <word>... — every word must appear
# in the error, which is how "names every candidate" is checked.
expect_ok() {
if [ "$RC" = 0 ] && [ "$OUT" = "$2" ]; then pass "$1"; else fail "$1 (rc=$RC out='$OUT' err='$ERR')"; fi
}
expect_fail() {
desc=$1; shift
if [ "$RC" = 0 ]; then fail "$desc (succeeded with '$OUT')"; return; fi
for word in "$@"; do
case "$ERR" in
*"$word"*) ;;
*) fail "$desc (error does not name $word: '$ERR')"; return ;;
esac
done
pass "$desc"
}
# --------------------------------------------------------------------------
# Package manager.
# --------------------------------------------------------------------------
fixture pm-apt; cmd apt-get; probe distro_pkg_manager
expect_ok "package manager: apt-get alone is apt" apt
fixture pm-dnf; cmd dnf; probe distro_pkg_manager
expect_ok "package manager: dnf alone is dnf" dnf
fixture pm-both; cmd apt-get; cmd dnf; probe distro_pkg_manager
expect_fail "package manager: both fails naming both" apt-get dnf
fixture pm-none; probe distro_pkg_manager
expect_fail "package manager: neither fails naming both" apt-get dnf
# --------------------------------------------------------------------------
# OVS umbrella unit. Unit files are found under any systemd unit directory, and
# a masked unit (a symlink to /dev/null) still counts as installed.
# --------------------------------------------------------------------------
fixture ovs-deb; unit openvswitch-switch.service; probe distro_ovs_unit
expect_ok "OVS unit: Debian name" openvswitch-switch.service
fixture ovs-el; unit openvswitch.service; probe distro_ovs_unit
expect_ok "OVS unit: EL name" openvswitch.service
fixture ovs-masked; mkdir -p "$ROOT/etc/systemd/system"
ln -s /dev/null "$ROOT/etc/systemd/system/openvswitch.service"; probe distro_ovs_unit
expect_ok "OVS unit: a masked unit in /etc counts" openvswitch.service
fixture ovs-lib; mkdir -p "$ROOT/lib/systemd/system"
: > "$ROOT/lib/systemd/system/openvswitch-switch.service"; probe distro_ovs_unit
expect_ok "OVS unit: found under /lib on an unmerged-/usr host" openvswitch-switch.service
fixture ovs-both; unit openvswitch-switch.service; unit openvswitch.service; probe distro_ovs_unit
expect_fail "OVS unit: both fails naming both" openvswitch-switch.service openvswitch.service
fixture ovs-none; probe distro_ovs_unit
expect_fail "OVS unit: neither fails naming both" openvswitch-switch.service openvswitch.service
# --------------------------------------------------------------------------
# OVN central layout. Both distros ship ovn-northd.service, so the unit alone
# must not decide it: Debian's split layout carries one too.
# --------------------------------------------------------------------------
split_units() { unit ovn-central.service; unit ovn-ovsdb-server-nb.service; unit ovn-ovsdb-server-sb.service; unit ovn-northd.service; }
fixture ovn-split; split_units; probe distro_ovn_central_layout
expect_ok "OVN layout: Debian split units" split
fixture ovn-lone; unit ovn-northd.service; file /etc/sysconfig/ovn; probe distro_ovn_central_layout
expect_ok "OVN layout: EL lone ovn-northd" lone-northd
fixture ovn-both; split_units; file /etc/sysconfig/ovn; probe distro_ovn_central_layout
expect_fail "OVN layout: both fails naming both" ovn-central.service ovn-northd.service /etc/sysconfig/ovn
fixture ovn-partial; unit ovn-central.service; unit ovn-ovsdb-server-nb.service; probe distro_ovn_central_layout
expect_fail "OVN layout: a partial split install names the missing unit" ovn-ovsdb-server-sb.service
fixture ovn-northd-only; unit ovn-northd.service; probe distro_ovn_central_layout
expect_fail "OVN layout: ovn-northd without /etc/sysconfig/ovn is not EL" ovn-central.service ovn-northd.service /etc/sysconfig/ovn
fixture ovn-none; probe distro_ovn_central_layout
expect_fail "OVN layout: neither fails naming both" ovn-central.service ovn-northd.service
# --------------------------------------------------------------------------
# CA trust.
# --------------------------------------------------------------------------
fixture ca-deb; cmd update-ca-certificates; probe distro_ca_trust
expect_ok "CA trust: Debian command and anchor dir" "update-ca-certificates /usr/local/share/ca-certificates"
fixture ca-el; cmd update-ca-trust; probe distro_ca_trust
expect_ok "CA trust: EL command and anchor dir" "update-ca-trust /etc/pki/ca-trust/source/anchors"
fixture ca-both; cmd update-ca-certificates; cmd update-ca-trust; probe distro_ca_trust
expect_fail "CA trust: both fails naming both" update-ca-certificates update-ca-trust
fixture ca-none; probe distro_ca_trust
expect_fail "CA trust: neither fails naming both" update-ca-certificates update-ca-trust
# --------------------------------------------------------------------------
# OVN logrotate file.
# --------------------------------------------------------------------------
fixture lr-deb; file /etc/logrotate.d/ovn-common; probe distro_ovn_logrotate
expect_ok "OVN logrotate: Debian file" /etc/logrotate.d/ovn-common
fixture lr-el; file /etc/logrotate.d/ovn; probe distro_ovn_logrotate
expect_ok "OVN logrotate: EL file" /etc/logrotate.d/ovn
fixture lr-both; file /etc/logrotate.d/ovn-common; file /etc/logrotate.d/ovn; probe distro_ovn_logrotate
expect_fail "OVN logrotate: both fails naming both" /etc/logrotate.d/ovn-common /etc/logrotate.d/ovn
fixture lr-none; probe distro_ovn_logrotate
expect_fail "OVN logrotate: neither fails naming both" /etc/logrotate.d/ovn-common /etc/logrotate.d/ovn
# --------------------------------------------------------------------------
# Inline copies. setup.sh and node-reset.sh cannot source the library, so they
# carry copies between markers; each copied function must match the library's
# byte for byte, or a fix to one silently misses the other.
# --------------------------------------------------------------------------
# fn_body <file> <name> — the function from "name() {" to the first "}" line.
fn_body() {
awk -v name="$2" '$0 == name "() {" { on = 1 } on { print } on && $0 == "}" { exit }' "$1"
}
for copy in setup.sh node-reset.sh; do
path="$SCRIPTS_DIR/$copy"
block=$(sed -n '/^# --- BEGIN distro.sh probes ---$/,/^# --- END distro.sh probes ---$/p' "$path")
if [ -z "$block" ]; then
fail "$copy: no distro.sh probe block"
continue
fi
names=$(printf '%s\n' "$block" | sed -n 's/^\([a-z_][a-z0-9_]*\)() {$/\1/p')
if [ -z "$names" ]; then
fail "$copy: the distro.sh probe block holds no functions"
continue
fi
for name in $names; do
want=$(fn_body "$DISTRO" "$name")
got=$(fn_body "$path" "$name")
if [ -z "$want" ]; then
fail "$copy: $name is not in lib/distro.sh"
elif [ "$got" = "$want" ]; then
pass "$copy: $name matches lib/distro.sh"
else
fail "$copy: $name differs from lib/distro.sh"
fi
done
# A copied probe calling a probe helper the block lacks fails only when run.
for called in $(printf '%s\n' "$block" | grep -v '^ *#' | grep -o 'distro_[a-z_]*' | sort -u); do
case " $(echo $names) " in
*" $called "*) ;;
*) fail "$copy: copies a probe that calls $called without copying it" ;;
esac
done
done
# --------------------------------------------------------------------------
# A consumer with no library beside it and none installed must stop naming
# both paths, not run on with the probes undefined. Skipped on a host where
# setup.sh has installed the library, since the fallback then finds it.
# --------------------------------------------------------------------------
if [ -e /usr/local/share/spinifex/lib/distro.sh ]; then
echo " skip - consumers without distro.sh: /usr/local/share/spinifex/lib/distro.sh is installed here"
else
# uninstall-spx.sh sources the library after its --help, so it is run into
# its multi-node refusal, which exits 1 without touching the host either way.
mkdir -p "$TMP/alone/bin" "$TMP/alone/etc"
printf '#!/bin/sh\nexec "$@"\n' > "$TMP/alone/bin/sudo"
chmod +x "$TMP/alone/bin/sudo"
printf '[nodes.node1]\n[nodes.node2]\n' > "$TMP/alone/etc/spinifex.toml"
for consumer in "setup-ovn.sh --help" "uninstall-spx.sh --dry-run"; do
script=${consumer%% *}
cp "$SCRIPTS_DIR/$script" "$TMP/alone/"
PATH="$TMP/alone/bin:$PATH" SPX_ETC_DIR="$TMP/alone/etc" "$TMP/alone/$script" "${consumer#* }" >"$TMP/out" 2>&1 && rc=0 || rc=$?
if [ "$rc" != 0 ] && grep -q "distro.sh not found at $TMP/alone/lib/distro.sh or /usr/local/share/spinifex/lib/distro.sh" "$TMP/out"; then
pass "$script: stops naming both paths when distro.sh is missing"
else
fail "$script: without distro.sh exited $rc: $(head -3 "$TMP/out")"
fi
done
fi
if [ "$fails" -gt 0 ]; then
echo "$fails failure(s)"
exit 1
fi
+71
View File
@@ -57,6 +57,77 @@ run() {
"$@"
}
# --- BEGIN distro.sh probes ---
# Copied from scripts/lib/distro.sh: install-node.sh --wipe copies this script
# alone to each host, so it cannot source the library. distro_test.sh keeps them equal.
distro_unit_exists() {
local dir
for dir in /etc/systemd/system /run/systemd/system /usr/lib/systemd/system /lib/systemd/system; do
if [ -e "${DISTRO_ROOT:-}$dir/$1" ] || [ -L "${DISTRO_ROOT:-}$dir/$1" ]; then
return 0
fi
done
return 1
}
distro_ovs_unit() {
local deb=false el=false
distro_unit_exists openvswitch-switch.service && deb=true
distro_unit_exists openvswitch.service && el=true
if $deb && $el; then
echo "distro.sh: OVS unit: found both openvswitch-switch.service and openvswitch.service; cannot tell which one owns OVS" >&2
return 1
fi
if $deb; then echo openvswitch-switch.service; return 0; fi
if $el; then echo openvswitch.service; return 0; fi
echo "distro.sh: OVS unit: found neither openvswitch-switch.service nor openvswitch.service; is Open vSwitch installed?" >&2
return 1
}
distro_ovn_central_layout() {
local split=false lone=false unit missing=""
for unit in ovn-central.service ovn-ovsdb-server-nb.service ovn-ovsdb-server-sb.service; do
if distro_unit_exists "$unit"; then
split=true
else
missing="${missing:+$missing, }$unit"
fi
done
if distro_unit_exists ovn-northd.service && [ -f "${DISTRO_ROOT:-}/etc/sysconfig/ovn" ]; then
lone=true
fi
if $split && $lone; then
echo "distro.sh: OVN central layout: found both the split units (ovn-central.service) and a lone ovn-northd.service with /etc/sysconfig/ovn; cannot tell which one owns the OVN databases" >&2
return 1
fi
if $split; then
if [ -n "$missing" ]; then
echo "distro.sh: OVN central layout: ovn-central is installed but $missing is missing" >&2
return 1
fi
echo split
return 0
fi
if $lone; then echo lone-northd; return 0; fi
echo "distro.sh: OVN central layout: found neither ovn-central.service nor ovn-northd.service with /etc/sysconfig/ovn; is OVN central installed?" >&2
return 1
}
distro_ca_trust() {
local deb=false el=false
command -v update-ca-certificates >/dev/null 2>&1 && deb=true
command -v update-ca-trust >/dev/null 2>&1 && el=true
if $deb && $el; then
echo "distro.sh: CA trust: found both update-ca-certificates and update-ca-trust; cannot tell which trust store TLS clients read" >&2
return 1
fi
if $deb; then echo "update-ca-certificates /usr/local/share/ca-certificates"; return 0; fi
if $el; then echo "update-ca-trust /etc/pki/ca-trust/source/anchors"; return 0; fi
echo "distro.sh: CA trust: found neither update-ca-certificates nor update-ca-trust on PATH" >&2
return 1
}
# --- END distro.sh probes ---
WIPE_DIRS=("$ETC_DIR" "$LOG_DIR" "$RUN_DIR")
$KEEP_DATA || WIPE_DIRS+=("$DATA_DIR")
+13
View File
@@ -121,6 +121,19 @@
set -e
# Host-OS capability probes: beside this script in a checkout and in the
# installed share dir, else where setup.sh installs them.
DISTRO_LIB=""
for candidate in "$(dirname "${BASH_SOURCE[0]}")/lib/distro.sh" /usr/local/share/spinifex/lib/distro.sh; do
if [ -f "$candidate" ]; then DISTRO_LIB="$candidate"; break; fi
done
if [ -z "$DISTRO_LIB" ]; then
echo "ERROR: distro.sh not found at $(dirname "${BASH_SOURCE[0]}")/lib/distro.sh or /usr/local/share/spinifex/lib/distro.sh; re-run setup.sh to install it" >&2
exit 1
fi
# shellcheck source=lib/distro.sh
. "$DISTRO_LIB"
# Defaults
MANAGEMENT=false
WAN_BRIDGE=""
+88 -6
View File
@@ -58,6 +58,24 @@ info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
fatal() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
# --- BEGIN distro.sh probes ---
# Copied from scripts/lib/distro.sh, which this script installs but cannot source
# when piped from the install endpoint. distro_test.sh keeps the copies equal.
distro_pkg_manager() {
local apt=false dnf=false
command -v apt-get >/dev/null 2>&1 && apt=true
command -v dnf >/dev/null 2>&1 && dnf=true
if $apt && $dnf; then
echo "distro.sh: package manager: found both apt-get and dnf; cannot tell which one manages this host" >&2
return 1
fi
if $apt; then echo apt; return 0; fi
if $dnf; then echo dnf; return 0; fi
echo "distro.sh: package manager: found neither apt-get nor dnf on PATH" >&2
return 1
}
# --- END distro.sh probes ---
stage() {
[ "${VERBOSE:-0}" = "1" ] && echo "[setup] $*"
return 0
@@ -92,14 +110,25 @@ setup_sudo() {
}
# --- OS detection ---
# os_release_field <key> <file> — the value of KEY=value, with one level of quotes removed.
os_release_field() {
sed -n "s/^$1=//p" "$2" | head -n 1 | sed -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'$/\1/"
}
# The only place that keys on the OS ID: the gate and, later, the package table.
# Everything else probes for the capability (scripts/lib/distro.sh).
detect_os() {
if [ ! -f /etc/os-release ]; then
fatal "Cannot detect OS: /etc/os-release not found"
local os_release="${SPINIFEX_OS_RELEASE:-/etc/os-release}"
if [ ! -f "$os_release" ]; then
fatal "Cannot detect OS: $os_release not found"
fi
. /etc/os-release
# Parsed, not sourced, so a malformed file is refused instead of executed.
ID=$(os_release_field ID "$os_release")
VERSION_ID=$(os_release_field VERSION_ID "$os_release")
PRETTY_NAME=$(os_release_field PRETTY_NAME "$os_release")
case "$ID" in
case "${ID:-}" in
debian)
if [ "${VERSION_ID%%.*}" -lt 13 ] 2>/dev/null; then
fatal "Debian $VERSION_ID is not supported. Minimum: Debian 13"
@@ -111,12 +140,58 @@ detect_os() {
fatal "Ubuntu $VERSION_ID is not supported. Minimum: Ubuntu 24.04"
fi
;;
ol)
check_oracle_linux
;;
*)
fatal "Unsupported OS: $ID $VERSION_ID. Spinifex requires Debian 13+ or Ubuntu 24.04+"
fatal "Unsupported OS: ${ID:-unknown} ${VERSION_ID:-}. Spinifex requires Debian 13+, Ubuntu 24.04+ or Oracle Linux 10.2+"
;;
esac
info "Detected OS: $PRETTY_NAME"
info "Detected OS: ${PRETTY_NAME:-$ID $VERSION_ID}"
}
# Oracle Linux 10.2+ on x86_64 only, running UEK R8: the KVM Utilities QEMU
# needs UEK, and Oracle ships kernel modules per kernel, so installing a newer
# kernel-uek alone leaves the running one without openvswitch.ko.
check_oracle_linux() {
local unsupported="Oracle Linux ${VERSION_ID:-<no VERSION_ID>} is not supported. Spinifex requires Oracle Linux 10.2 or a later 10.x"
local major="${VERSION_ID%%.*}" minor="${VERSION_ID#*.}" kernel
minor="${minor%%.*}"
case "${VERSION_ID:-}" in *.*) ;; *) fatal "$unsupported" ;; esac
case "$major" in "" | *[!0-9]*) fatal "$unsupported" ;; esac
case "$minor" in "" | *[!0-9]*) fatal "$unsupported" ;; esac
if [ "$major" -ne 10 ] || [ "$minor" -lt 2 ]; then
fatal "$unsupported"
fi
if [ "$(uname -m)" != "x86_64" ]; then
fatal "Oracle Linux on $(uname -m) is not supported. Spinifex supports Oracle Linux on x86_64 only"
fi
kernel=$(uname -r)
case "$kernel" in
6.12.0-*uek*) ;;
*) fatal "The running kernel $kernel is not UEK R8 (6.12.0-*uek). Install kernel-uek, reboot into it, then re-run this installer" ;;
esac
check_x86_64_v3
}
# The NFV SIG OVS/OVN and the KVM Utilities QEMU are built for x86-64-v3, so an
# older CPU installs cleanly and then dies on an illegal instruction.
check_x86_64_v3() {
local cpuinfo="${SPINIFEX_CPUINFO:-/proc/cpuinfo}" flags flag missing=""
flags=" $(grep -m1 '^flags' "$cpuinfo" 2>/dev/null | cut -d: -f2) "
for flag in cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3 avx avx2 bmi1 bmi2 f16c fma abm movbe xsave; do
case "$flags" in
*" $flag "*) ;;
*) missing="${missing:+$missing }$flag" ;;
esac
done
if [ -n "$missing" ]; then
fatal "This CPU is below x86-64-v3 (missing: $missing). Oracle Linux's OVS, OVN and QEMU builds require x86-64-v3"
fi
}
# --- Architecture detection ---
@@ -891,6 +966,13 @@ install_files() {
info " /usr/local/share/spinifex/setup-ovn.sh"
fi
# Sourced, not run, by setup-ovn.sh and uninstall-spx.sh wherever they are
# installed, including the ISO's /usr/local/bin/setup-ovn.sh.
if [ -f "$EXTRACT_DIR/lib/distro.sh" ]; then
$SUDO install -D -m 0644 "$EXTRACT_DIR/lib/distro.sh" /usr/local/share/spinifex/lib/distro.sh
info " /usr/local/share/spinifex/lib/distro.sh"
fi
# Install setup.sh itself so firstboot and future re-runs can find it at a
# stable path on both ISO and curl|bash installs.
if [ -f "$EXTRACT_DIR/setup.sh" ]; then
+138
View File
@@ -0,0 +1,138 @@
#!/bin/sh
# setup_test.sh — tests for setup.sh's supported-OS gate.
#
# setup.sh is sourced with INSTALL_SPINIFEX_LIB_ONLY=1 so main never runs, and
# detect_os reads a fixture os-release, a fixture cpuinfo and a stub uname.
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 OS gate"
V3_FLAGS="fpu cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3 avx avx2 bmi1 bmi2 f16c fma abm movbe xsave"
V2_FLAGS="fpu cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3"
UEK_R8="6.12.0-207.111.5.4.el10uek.x86_64"
# gate <os-release body> [machine] [kernel] [cpu flags] — runs detect_os and
# sets RC and OUT (stdout and stderr together).
gate() {
printf '%s\n' "$1" > "$TMP/os-release"
printf 'processor\t: 0\nflags\t\t: %s\n' "${4:-$V3_FLAGS}" > "$TMP/cpuinfo"
mkdir -p "$TMP/bin"
cat > "$TMP/bin/uname" <<EOF
#!/bin/sh
case "\$1" in
-m) echo "${2:-x86_64}" ;;
-r) echo "${3:-$UEK_R8}" ;;
*) exit 1 ;;
esac
EOF
chmod +x "$TMP/bin/uname"
RC=0
OUT=$(PATH="$TMP/bin:$PATH" SPINIFEX_OS_RELEASE="$TMP/os-release" SPINIFEX_CPUINFO="$TMP/cpuinfo" \
INSTALL_SPINIFEX_LIB_ONLY=1 bash -c '. "$1"; detect_os' _ "$SETUP" 2>&1) || RC=$?
}
accepts() {
if [ "$RC" = 0 ]; then pass "accepts $1"; else fail "rejects $1: $OUT"; fi
}
# rejects <desc> <word> — the refusal must say why, not just exit.
rejects() {
if [ "$RC" = 0 ]; then
fail "accepts $1"
else
case "$OUT" in
*"$2"*) pass "rejects $1" ;;
*) fail "rejects $1 without naming '$2': $OUT" ;;
esac
fi
}
OL='ID="ol"
NAME="Oracle Linux Server"
PRETTY_NAME="Oracle Linux Server 10.2"'
gate 'ID=debian
VERSION_ID="13"
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"'
accepts "Debian 13"
# Debian testing and sid carry no VERSION_ID. The gate has always let them
# through, and Debian behaviour is not this gate's to change.
gate 'ID=debian
PRETTY_NAME="Debian GNU/Linux forky/sid"'
accepts "Debian testing with no VERSION_ID"
gate 'ID=debian
VERSION_ID="12"'
rejects "Debian 12" "Minimum: Debian 13"
gate 'ID=ubuntu
VERSION_ID="26.04"
PRETTY_NAME="Ubuntu 26.04 LTS"'
accepts "Ubuntu 26.04"
gate 'ID=ubuntu
VERSION_ID="22.04"'
rejects "Ubuntu 22.04" "Minimum: Ubuntu 24.04"
gate "$OL
VERSION_ID=\"10.2\""
accepts "Oracle Linux 10.2 on UEK R8, x86_64, x86-64-v3"
gate "$OL
VERSION_ID=\"10.4\""
accepts "Oracle Linux 10.4"
gate "$OL
VERSION_ID=\"10.1\""
rejects "Oracle Linux 10.1" "Oracle Linux 10.2 or a later 10.x"
gate "$OL
VERSION_ID=\"10\""
rejects "Oracle Linux with no minor version" "Oracle Linux 10.2 or a later 10.x"
gate "$OL
VERSION_ID=\"11.0\""
rejects "Oracle Linux 11" "Oracle Linux 10.2 or a later 10.x"
gate "$OL
VERSION_ID=\"9.6\"" x86_64 "5.15.0-310.184.5.2.el9uek.x86_64"
rejects "Oracle Linux 9" "Oracle Linux 10.2 or a later 10.x"
gate "$OL" x86_64
rejects "Oracle Linux with no VERSION_ID" "Oracle Linux 10.2 or a later 10.x"
gate "$OL
VERSION_ID=\"10.2\"" aarch64 "6.12.0-207.111.5.4.el10uek.aarch64"
rejects "Oracle Linux on aarch64" "x86_64 only"
gate "$OL
VERSION_ID=\"10.2\"" x86_64 "6.12.0-55.9.1.el10_0.x86_64"
rejects "Oracle Linux on the Red Hat compatible kernel" "is not UEK R8"
gate "$OL
VERSION_ID=\"10.2\"" x86_64 "5.15.0-310.184.5.2.el9uek.x86_64"
rejects "Oracle Linux on a UEK older than R8" "is not UEK R8"
gate "$OL
VERSION_ID=\"10.2\"" x86_64 "$UEK_R8" "$V2_FLAGS"
rejects "Oracle Linux on an x86-64-v2 CPU" "missing: avx avx2 bmi1 bmi2 f16c fma abm movbe xsave"
gate 'ID="rocky"
VERSION_ID="10.0"'
rejects "Rocky Linux 10" "Unsupported OS: rocky"
gate 'this is not an os-release file'
rejects "a malformed os-release" "Unsupported OS"
if [ "$fails" -gt 0 ]; then
echo "$fails failure(s)"
exit 1
fi
+13
View File
@@ -73,6 +73,19 @@ while [[ $# -gt 0 ]]; do
shift
done
# Host-OS capability probes: beside this script in a checkout and in the
# installed share dir, else where setup.sh installs them.
DISTRO_LIB=""
for candidate in "$(dirname "${BASH_SOURCE[0]}")/lib/distro.sh" /usr/local/share/spinifex/lib/distro.sh; do
if [ -f "$candidate" ]; then DISTRO_LIB="$candidate"; break; fi
done
if [ -z "$DISTRO_LIB" ]; then
echo "ERROR: distro.sh not found at $(dirname "${BASH_SOURCE[0]}")/lib/distro.sh or /usr/local/share/spinifex/lib/distro.sh; re-run setup.sh to install it" >&2
exit 1
fi
# shellcheck source=lib/distro.sh
. "$DISTRO_LIB"
log() { echo "[uninstall-spx] $*"; }
run() {
if $DRY_RUN; then
+205
View File
@@ -0,0 +1,205 @@
// Package hostos resolves the host tools whose names or paths differ between
// host operating systems. Each resolver probes for the tool itself, never reads
// /etc/os-release, and names every candidate it tried when nothing matches.
package hostos
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
)
// QEMUKVMPath is where EL-family distributions install QEMU. It only runs
// guests of the host's own architecture.
const QEMUKVMPath = "/usr/libexec/qemu-kvm"
// Tool is a host command found by a resolver.
type Tool struct {
// Name is the candidate that matched, such as "grubby" or "update-grub".
Name string
// Path is where it was found on PATH.
Path string
}
// CATrust is the host's system trust store.
type CATrust struct {
// Update rebuilds the trust bundle from the anchor directory.
Update Tool
// AnchorDir is where locally added CA certificates go.
AnchorDir string
}
// BootTools are the commands that change the kernel command line and rebuild
// the initramfs.
type BootTools struct {
Bootloader Tool
Initramfs Tool
}
type qemuArch struct {
binary string
pkg string
}
// qemuArches maps the guest architectures the VM config uses to the upstream
// QEMU binary and the Debian package that ships it.
var qemuArches = map[string]qemuArch{
"x86_64": {binary: "qemu-system-x86_64", pkg: "qemu-system-x86"},
"arm64": {binary: "qemu-system-aarch64", pkg: "qemu-system-arm"},
}
var caTrustAnchorDirs = map[string]string{
"update-ca-certificates": "/usr/local/share/ca-certificates",
"update-ca-trust": "/etc/pki/ca-trust/source/anchors",
}
var std = newResolver()
// QEMUBinary returns the QEMU system emulator for guests of arch ("x86_64" or
// "arm64"). QEMUKVMPath wins for host-arch guests, so a stray qemu-system-* on
// PATH never replaces the distro's packaged build.
func QEMUBinary(arch string) (string, error) {
return std.qemuBinary(arch)
}
// SystemCATrust returns the host's trust-store update command and anchor directory.
func SystemCATrust() (CATrust, error) {
return std.caTrust()
}
// SystemBootTools returns the host's bootloader and initramfs tools.
func SystemBootTools() (BootTools, error) {
return std.bootTools()
}
// resolver holds the host lookups behind fields so tests run against a temp
// root and a stubbed PATH. Only successes are cached, so a tool installed
// after a failed lookup is found on the next call.
type resolver struct {
root string
hostArch string
lookPath func(string) (string, error)
mu sync.Mutex
qemu map[string]string
ca *CATrust
boots *BootTools
}
func newResolver() *resolver {
return &resolver{
hostArch: hostArch(runtime.GOARCH),
lookPath: exec.LookPath,
qemu: map[string]string{},
}
}
// hostArch maps a GOARCH to the architecture names the VM config uses.
func hostArch(goarch string) string {
switch goarch {
case "amd64":
return "x86_64"
case "arm64":
return "arm64"
default:
return goarch
}
}
func (r *resolver) qemuBinary(arch string) (string, error) {
want, ok := qemuArches[arch]
if !ok {
return "", fmt.Errorf("no QEMU for guest architecture %q: supported architectures are x86_64 and arm64", arch)
}
r.mu.Lock()
defer r.mu.Unlock()
if path, ok := r.qemu[arch]; ok {
return path, nil
}
haveKVM := r.isExecutable(QEMUKVMPath)
if haveKVM && arch == r.hostArch {
r.qemu[arch] = QEMUKVMPath
return QEMUKVMPath, nil
}
path, err := r.lookPath(want.binary)
if err == nil {
r.qemu[arch] = path
return path, nil
}
if haveKVM {
return "", fmt.Errorf("no QEMU for %s guests: %s (package qemu-kvm) runs only %s guests, and %s (package %s) is not on PATH",
arch, QEMUKVMPath, r.hostArch, want.binary, want.pkg)
}
if arch != r.hostArch {
return "", fmt.Errorf("no QEMU for %s guests: %s (package %s) is not on PATH", arch, want.binary, want.pkg)
}
return "", fmt.Errorf("no QEMU for %s guests: found neither %s (package qemu-kvm) nor %s (package %s) on PATH",
arch, QEMUKVMPath, want.binary, want.pkg)
}
func (r *resolver) caTrust() (CATrust, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.ca != nil {
return *r.ca, nil
}
update, err := r.pickOne("CA trust store update command", "update-ca-certificates", "update-ca-trust")
if err != nil {
return CATrust{}, err
}
r.ca = &CATrust{Update: update, AnchorDir: caTrustAnchorDirs[update.Name]}
return *r.ca, nil
}
func (r *resolver) bootTools() (BootTools, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.boots != nil {
return *r.boots, nil
}
bootloader, err := r.pickOne("bootloader tool", "update-grub", "grubby")
if err != nil {
return BootTools{}, err
}
initramfs, err := r.pickOne("initramfs tool", "update-initramfs", "dracut")
if err != nil {
return BootTools{}, err
}
r.boots = &BootTools{Bootloader: bootloader, Initramfs: initramfs}
return *r.boots, nil
}
// pickOne returns the one candidate on PATH. Finding several is an error too:
// a host carrying both tools of a pair cannot tell us which one it uses.
func (r *resolver) pickOne(what string, candidates ...string) (Tool, error) {
var found []Tool
for _, name := range candidates {
if path, err := r.lookPath(name); err == nil {
found = append(found, Tool{Name: name, Path: path})
}
}
switch len(found) {
case 1:
return found[0], nil
case 0:
return Tool{}, fmt.Errorf("no %s: found none of %s on PATH", what, strings.Join(candidates, ", "))
default:
names := make([]string, len(found))
for i, t := range found {
names[i] = fmt.Sprintf("%s (%s)", t.Name, t.Path)
}
return Tool{}, fmt.Errorf("ambiguous %s: found %s; cannot tell which one this host uses", what, strings.Join(names, " and "))
}
}
func (r *resolver) isExecutable(path string) bool {
info, err := os.Stat(filepath.Join(r.root, path))
return err == nil && info.Mode().IsRegular() && info.Mode().Perm()&0o111 != 0
}
+296
View File
@@ -0,0 +1,296 @@
package hostos
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// fakeHost is a temp root plus a PATH made of a name-to-path map, counting
// lookups so the caching tests can see whether the host was probed again.
type fakeHost struct {
root string
path map[string]string
lookups int
}
func newFakeHost(t *testing.T) *fakeHost {
t.Helper()
return &fakeHost{root: t.TempDir(), path: map[string]string{}}
}
func (h *fakeHost) resolver(hostArch string) *resolver {
r := newResolver()
r.root = h.root
r.hostArch = hostArch
r.lookPath = func(name string) (string, error) {
h.lookups++
if p, ok := h.path[name]; ok {
return p, nil
}
return "", &exec.Error{Name: name, Err: exec.ErrNotFound}
}
return r
}
func (h *fakeHost) onPath(names ...string) {
for _, n := range names {
h.path[n] = "/usr/bin/" + n
}
}
func (h *fakeHost) installQEMUKVM(t *testing.T, mode os.FileMode) {
t.Helper()
p := filepath.Join(h.root, QEMUKVMPath)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, nil, mode); err != nil {
t.Fatal(err)
}
}
// requireErrNames fails unless err is non-nil and mentions every word.
func requireErrNames(t *testing.T, err error, words ...string) {
t.Helper()
if err == nil {
t.Fatalf("want an error naming %v, got nil", words)
}
for _, w := range words {
if !strings.Contains(err.Error(), w) {
t.Errorf("error does not name %q: %v", w, err)
}
}
}
func TestQEMUBinary_QEMUKVMWinsOverQEMUSystemOnPath(t *testing.T) {
h := newFakeHost(t)
h.installQEMUKVM(t, 0o755)
h.onPath("qemu-system-x86_64")
got, err := h.resolver("x86_64").qemuBinary("x86_64")
if err != nil {
t.Fatal(err)
}
if got != QEMUKVMPath {
t.Fatalf("got %q, want %q", got, QEMUKVMPath)
}
}
func TestQEMUBinary_QEMUSystemWithoutQEMUKVM(t *testing.T) {
h := newFakeHost(t)
h.onPath("qemu-system-x86_64")
got, err := h.resolver("x86_64").qemuBinary("x86_64")
if err != nil {
t.Fatal(err)
}
if got != "/usr/bin/qemu-system-x86_64" {
t.Fatalf("got %q, want the qemu-system-x86_64 on PATH", got)
}
}
func TestQEMUBinary_NonExecutableQEMUKVMIsIgnored(t *testing.T) {
h := newFakeHost(t)
h.installQEMUKVM(t, 0o644)
h.onPath("qemu-system-x86_64")
got, err := h.resolver("x86_64").qemuBinary("x86_64")
if err != nil {
t.Fatal(err)
}
if got != "/usr/bin/qemu-system-x86_64" {
t.Fatalf("got %q, want the qemu-system-x86_64 on PATH", got)
}
}
func TestQEMUBinary_NeitherNamesBothCandidatesAndPackages(t *testing.T) {
h := newFakeHost(t)
_, err := h.resolver("x86_64").qemuBinary("x86_64")
requireErrNames(t, err, QEMUKVMPath, "qemu-kvm", "qemu-system-x86_64", "qemu-system-x86")
}
func TestQEMUBinary_ForeignArchIsNeverQEMUKVM(t *testing.T) {
h := newFakeHost(t)
h.installQEMUKVM(t, 0o755)
_, err := h.resolver("x86_64").qemuBinary("arm64")
requireErrNames(t, err, QEMUKVMPath, "runs only x86_64 guests", "qemu-system-aarch64", "qemu-system-arm")
}
func TestQEMUBinary_ForeignArchUsesQEMUSystemBesideQEMUKVM(t *testing.T) {
h := newFakeHost(t)
h.installQEMUKVM(t, 0o755)
h.onPath("qemu-system-aarch64")
got, err := h.resolver("x86_64").qemuBinary("arm64")
if err != nil {
t.Fatal(err)
}
if got != "/usr/bin/qemu-system-aarch64" {
t.Fatalf("got %q, want the qemu-system-aarch64 on PATH", got)
}
}
func TestQEMUBinary_ForeignArchWithoutQEMUKVMNamesOnlyQEMUSystem(t *testing.T) {
h := newFakeHost(t)
_, err := h.resolver("x86_64").qemuBinary("arm64")
requireErrNames(t, err, "qemu-system-aarch64", "qemu-system-arm")
if strings.Contains(err.Error(), QEMUKVMPath) {
t.Errorf("error offers qemu-kvm, which cannot run arm64 guests on x86_64: %v", err)
}
}
func TestQEMUBinary_UnknownArch(t *testing.T) {
h := newFakeHost(t)
h.onPath("qemu-system-riscv64")
_, err := h.resolver("x86_64").qemuBinary("riscv64")
requireErrNames(t, err, `"riscv64"`, "x86_64", "arm64")
}
func TestQEMUBinary_CachesSuccessPerArch(t *testing.T) {
h := newFakeHost(t)
h.onPath("qemu-system-x86_64", "qemu-system-aarch64")
r := h.resolver("x86_64")
first, err := r.qemuBinary("x86_64")
if err != nil {
t.Fatal(err)
}
lookups := h.lookups
h.installQEMUKVM(t, 0o755)
again, err := r.qemuBinary("x86_64")
if err != nil {
t.Fatal(err)
}
if again != first || h.lookups != lookups {
t.Fatalf("second call re-probed: got %q (first %q), lookups %d -> %d", again, first, lookups, h.lookups)
}
other, err := r.qemuBinary("arm64")
if err != nil {
t.Fatal(err)
}
if other != "/usr/bin/qemu-system-aarch64" {
t.Fatalf("arm64 got %q from the x86_64 cache entry", other)
}
}
func TestQEMUBinary_FailureIsNotCached(t *testing.T) {
h := newFakeHost(t)
r := h.resolver("x86_64")
if _, err := r.qemuBinary("x86_64"); err == nil {
t.Fatal("want an error before QEMU is installed")
}
h.installQEMUKVM(t, 0o755)
got, err := r.qemuBinary("x86_64")
if err != nil {
t.Fatalf("QEMU installed after a failed lookup is not found: %v", err)
}
if got != QEMUKVMPath {
t.Fatalf("got %q, want %q", got, QEMUKVMPath)
}
}
func TestHostArch(t *testing.T) {
for goarch, want := range map[string]string{"amd64": "x86_64", "arm64": "arm64"} {
if got := hostArch(goarch); got != want {
t.Errorf("hostArch(%q) = %q, want %q", goarch, got, want)
}
}
}
func TestSystemCATrust(t *testing.T) {
cases := []struct {
name string
onPath []string
wantTool string
wantDir string
wantError []string
}{
{name: "debian", onPath: []string{"update-ca-certificates"}, wantTool: "update-ca-certificates", wantDir: "/usr/local/share/ca-certificates"},
{name: "el", onPath: []string{"update-ca-trust"}, wantTool: "update-ca-trust", wantDir: "/etc/pki/ca-trust/source/anchors"},
{name: "both", onPath: []string{"update-ca-certificates", "update-ca-trust"}, wantError: []string{"/usr/bin/update-ca-certificates", "/usr/bin/update-ca-trust"}},
{name: "neither", wantError: []string{"update-ca-certificates", "update-ca-trust"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := newFakeHost(t)
h.onPath(tc.onPath...)
got, err := h.resolver("x86_64").caTrust()
if tc.wantError != nil {
requireErrNames(t, err, tc.wantError...)
return
}
if err != nil {
t.Fatal(err)
}
if got.Update.Name != tc.wantTool || got.Update.Path != "/usr/bin/"+tc.wantTool || got.AnchorDir != tc.wantDir {
t.Fatalf("got %+v, want %s in %s", got, tc.wantTool, tc.wantDir)
}
})
}
}
func TestSystemCATrust_CachesSuccess(t *testing.T) {
h := newFakeHost(t)
h.onPath("update-ca-certificates")
r := h.resolver("x86_64")
if _, err := r.caTrust(); err != nil {
t.Fatal(err)
}
h.onPath("update-ca-trust")
got, err := r.caTrust()
if err != nil {
t.Fatalf("cached result was re-probed: %v", err)
}
if got.Update.Name != "update-ca-certificates" {
t.Fatalf("got %q, want the cached update-ca-certificates", got.Update.Name)
}
}
func TestSystemBootTools(t *testing.T) {
cases := []struct {
name string
onPath []string
wantBootloader string
wantInitramfs string
wantError []string
}{
{name: "debian", onPath: []string{"update-grub", "update-initramfs"}, wantBootloader: "update-grub", wantInitramfs: "update-initramfs"},
{name: "el", onPath: []string{"grubby", "dracut"}, wantBootloader: "grubby", wantInitramfs: "dracut"},
{name: "both bootloader tools", onPath: []string{"update-grub", "grubby", "dracut"}, wantError: []string{"update-grub", "grubby"}},
{name: "both initramfs tools", onPath: []string{"grubby", "update-initramfs", "dracut"}, wantError: []string{"update-initramfs", "dracut"}},
{name: "no bootloader tool", onPath: []string{"dracut"}, wantError: []string{"update-grub", "grubby"}},
{name: "no initramfs tool", onPath: []string{"grubby"}, wantError: []string{"update-initramfs", "dracut"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := newFakeHost(t)
h.onPath(tc.onPath...)
got, err := h.resolver("x86_64").bootTools()
if tc.wantError != nil {
requireErrNames(t, err, tc.wantError...)
return
}
if err != nil {
t.Fatal(err)
}
if got.Bootloader.Name != tc.wantBootloader || got.Initramfs.Name != tc.wantInitramfs {
t.Fatalf("got %+v, want %s and %s", got, tc.wantBootloader, tc.wantInitramfs)
}
})
}
}