Newer
Older
openstack-caracal-dc-dc / tests / site-tailscale / run-tests.sh
#!/usr/bin/env bash
# tests/site-tailscale/run-tests.sh -- offline harness for scripts/site-tailscale.sh.
# Fakes tailscale (status/up/version), ip (addr + route), sysctl (forwarding), dpkg (prep),
# curl (control reach); real python3 for jget. NO network, mutates nothing outside mktemp.
# Proves each exit path can FIRE (a new assertion is decoration until it has a failing fixture):
#   prep-happy      -> 0   (pkg present + forwarding on)
#   prep-fromdeb    -> 0   (pkg absent, $TS_DEB staged, dpkg installs it, forwarding on)
#   prep-noforward  -> 4   (forwarding sysctl reads 0 -- load-bearing subnet-router property)
#   prep-nopkg-nodeb-> 4   (pkg absent and no $TS_DEB staged)
#   check-happy     -> 0   (Running + route approved + forwarding + untagged + leg + no-hijack + control-up)
#   check-notrun    -> 1   (BackendState != Running)
#   check-noroute   -> 1   (metal-admin CIDR not in approved PrimaryRoutes)
#   check-noforward -> 1   (IP forwarding off)
#   check-noleg     -> 1   (no .7 metal-admin leg on the host)
#   check-hijack    -> 1   (own /22 routed via tailscale0 -- the 2026-08-07 blackhole)
#   check-ctrldown  -> 1   (control plane unreachable -- registers/stays down, green-except-route)
#   check-tag-happy -> 0   (TS_TAG set + node tagged)
#   check-tag-notag -> 1   (TS_TAG set but node NOT tagged)
#   install-happy   -> 0   (untagged up ok + check green)
#   install-tag-happy-> 0  (tagged up ok + check green)
#   install-nokey   -> 4   (no pre-auth key)
#   install-noTS    -> 4   (tailscale binary absent -> "run prep first")
#   install-upfail  -> 4   (tailscale up rejected)
#   key-not-in-argv        (install passes --authkey=file:, the key VALUE never reaches argv)
#   badmode/badsite/noargs -> 2 ; dc1 resolves its own CIDR ; site rows carry MEASURED tags
# Exit: 0 all pass | 1 any case failed.  ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
SUT="$REPO/scripts/site-tailscale.sh"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
BIN="$TMP/bin"; mkdir -p "$BIN" "$TMP/sysctl.d"
printf 'fake-deb-content\n' > "$TMP/fake.deb"
SECRET='tskey-SECRET-do-not-leak-0xdeadbeef'; printf '%s' "$SECRET" > "$TMP/authkey.file"

# ---- fake tailscale (records `up` argv so we can prove the key never lands there) ----------
cat > "$BIN/tailscale" <<'FB'
#!/usr/bin/env bash
mode="${TS_FAKE_MODE:-happy}"; cidr="${TS_FAKE_CIDR:-10.12.8.0/22}"
case "$1" in
  version) echo "1.98.9"; exit 0;;
  up) [ -n "${TS_ARGV_LOG:-}" ] && printf '%s\n' "$*" >> "$TS_ARGV_LOG"; [ "$mode" = upfail ] && exit 1; exit 0;;
  status)
     st='"Running"'; routes="[\"$cidr\"]"
     tags='[]'; [ -n "${TS_TAG:-}" ] && tags="[\"$TS_TAG\"]"
     case "$mode" in notrun) st='"Stopped"';; noroute) routes='[]';; notag) tags='[]';; esac
     printf '{"BackendState":%s,"Self":{"PrimaryRoutes":%s,"Tags":%s}}\n' "$st" "$routes" "$tags"; exit 0;;
  *) exit 0;;
esac
FB
chmod +x "$BIN/tailscale"

# ---- fake ip: addr show (own_leg_matches) + route show (own-subnet guard) ------------------
cat > "$BIN/ip" <<'FB'
#!/usr/bin/env bash
case "$*" in
  *"route show"*)  # own-subnet guard: emit a tailscale0 route only when hijack fixture set
     [ "${TS_FAKE_HIJACK:-0}" = 1 ] && echo "${TS_FAKE_CIDR:-10.12.8.0/22} dev tailscale0 scope link"
     exit 0;;
esac
# addr show path
[ "${TS_FAKE_MODE:-}" = noleg ] && { echo "1: lo inet 127.0.0.1/8 scope host lo"; exit 0; }
echo "2: enp1s0 inet ${TS_FAKE_LEG:-10.12.8.7}/22 scope global enp1s0"
exit 0
FB
chmod +x "$BIN/ip"

# ---- fake sysctl (forwarding) ----------------------------------------------
cat > "$BIN/sysctl" <<'FB'
#!/usr/bin/env bash
fwd="${TS_FAKE_FWD:-1}"
case "$*" in
  "--system") exit 0;;
  "-n net.ipv4.ip_forward") echo "$fwd"; exit 0;;
  "-n net.ipv6.conf.all.forwarding") echo "$fwd"; exit 0;;
  *) exit 0;;
esac
FB
chmod +x "$BIN/sysctl"

# ---- fake curl (control-plane reachability) --------------------------------
cat > "$BIN/curl" <<'FB'
#!/usr/bin/env bash
[ "${TS_FAKE_CTRL:-up}" = down ] && { printf '000'; exit 7; }
printf '200'; exit 0
FB
chmod +x "$BIN/curl"

# ---- fake dpkg (prep install-from-deb) ------------------------------------
cat > "$BIN/dpkg" <<FB
#!/usr/bin/env bash
if [ "\$1" = "-i" ] && [ -n "\${TS_INSTALL_TARGET:-}" ]; then
  cp "$BIN/tailscale" "\$TS_INSTALL_TARGET" && chmod +x "\$TS_INSTALL_TARGET"; exit 0
fi
exit 0
FB
chmod +x "$BIN/dpkg"

run(){ # <name> <expected-exit> <mode> <args...>
  local name="$1" exp="$2" mode="$3"; shift 3
  local out code
  out="$(PATH="$BIN:$PATH" \
         TAILSCALE="${CASE_TS:-$BIN/tailscale}" \
         SYSCTL="$BIN/sysctl" DPKG="$BIN/dpkg" CURL="$BIN/curl" APTGET="/bin/false" \
         TS_SYSCTL_D="$TMP/sysctl.d" \
         TS_FAKE_MODE="$mode" TS_FAKE_FWD="${CASE_FWD:-1}" \
         TS_FAKE_CTRL="${CASE_CTRL:-up}" TS_FAKE_HIJACK="${CASE_HIJACK:-0}" \
         TS_FAKE_CIDR="${CASE_CIDR:-10.12.8.0/22}" TS_FAKE_LEG="${CASE_LEG:-10.12.8.7}" \
         TS_TAG="${CASE_TAG-}" TS_AUTHKEY="${CASE_AUTHKEY:-}" TS_AUTHKEY_FILE="${CASE_KEYFILE:-}" \
         TS_ARGV_LOG="${CASE_ARGV_LOG:-}" \
         TS_DEB="${CASE_DEB:-}" TS_INSTALL_TARGET="${CASE_TS:-}" \
         bash "$SUT" "$@" 2>&1)"; code=$?
  if [ "$code" -eq "$exp" ]; then PASS=$((PASS+1)); echo "PASS: $name (exit $code)"
  else FAIL=$((FAIL+1)); echo "FAIL: $name (got $code want $exp)"; echo "$out" | sed 's/^/    /'; fi
}

echo "=== site-tailscale harness ==="
# prep
run prep-happy       0 happy prep vr1-dc0
CASE_TS="$BIN/staged-ts" CASE_DEB="$TMP/fake.deb" run prep-fromdeb 0 happy prep vr1-dc0
CASE_FWD=0 run prep-noforward 4 happy prep vr1-dc0
CASE_TS="$BIN/nope" run prep-nopkg-nodeb 4 happy prep vr1-dc0
# check (default untagged)
run check-happy      0 happy   check vr1-dc0
run check-notrun     1 notrun  check vr1-dc0
run check-noroute    1 noroute check vr1-dc0
CASE_FWD=0    run check-noforward 1 happy check vr1-dc0
run check-noleg      1 noleg   check vr1-dc0
CASE_HIJACK=1 run check-hijack    1 happy check vr1-dc0
CASE_CTRL=down run check-ctrldown 1 happy check vr1-dc0
# tagged mode
CASE_TAG="tag:subnet-router" run check-tag-happy 0 happy check vr1-dc0
CASE_TAG="tag:subnet-router" run check-tag-notag 1 notag check vr1-dc0
# install
CASE_AUTHKEY="tskey-fake" run install-happy 0 happy install vr1-dc0
CASE_TAG="tag:subnet-router" CASE_AUTHKEY="tskey-fake" run install-tag-happy 0 happy install vr1-dc0
                          run install-nokey  4 happy  install vr1-dc0
CASE_AUTHKEY="tskey-fake" run install-upfail 4 upfail install vr1-dc0
CASE_TS="$BIN/nope" CASE_AUTHKEY="tskey-fake" run install-noTS 4 happy install vr1-dc0

# key-not-in-argv: install via TS_AUTHKEY_FILE must pass --authkey=file:, never the value
CASE_KEYFILE="$TMP/authkey.file" CASE_ARGV_LOG="$TMP/argv.log" run install-keyfile 0 happy install vr1-dc0
if grep -q 'authkey=file:' "$TMP/argv.log" 2>/dev/null && ! grep -qF "$SECRET" "$TMP/argv.log" 2>/dev/null; then
  PASS=$((PASS+1)); echo "PASS: key-not-in-argv (file: scheme used, value absent from argv)"
else FAIL=$((FAIL+1)); echo "FAIL: key-not-in-argv"; sed 's/^/    /' "$TMP/argv.log" 2>/dev/null; fi

# arg/site validation
run badmode  2 happy frobnicate vr1-dc0
run badsite  2 happy check      vr1-dc9
run noargs   2 happy
# dc1 resolves its OWN cidr
CASE_CIDR='10.12.68.0/22' CASE_LEG='10.12.68.7' run dc1-cidr 0 happy check vr1-dc1
out="$(PATH="$BIN:$PATH" TAILSCALE="$BIN/tailscale" SYSCTL="$BIN/sysctl" CURL="$BIN/curl" TS_SYSCTL_D="$TMP/sysctl.d" \
       TS_FAKE_MODE=happy TS_FAKE_CIDR='10.12.68.0/22' TS_FAKE_LEG='10.12.68.7' \
       bash "$SUT" check vr1-dc1 2>&1)"
if printf '%s' "$out" | grep -q '10.12.68.0/22'; then PASS=$((PASS+1)); echo "PASS: dc1 emits 10.12.68.0/22"
else FAIL=$((FAIL+1)); echo "FAIL: dc1 site cidr"; echo "$out" | sed 's/^/    /'; fi
# site-table rows must carry MEASURED tags (hard rule 2)
for s in 'vr1-dc0.*MEASURED' 'vr1-dc1.*MEASURED'; do
  if grep -qE "$s" "$SUT"; then PASS=$((PASS+1)); echo "PASS: site row tagged MEASURED ($s)"
  else FAIL=$((FAIL+1)); echo "FAIL: site row missing MEASURED ($s)"; fi
done

echo "=== $PASS pass / $FAIL fail ==="
[ "$FAIL" -eq 0 ]