From d954b6e90d67641276c865219dc7db2ee2ef2073 Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Sat, 22 Aug 2026 16:01:32 -0600 Subject: [PATCH] fix(smoke): conditional role drill, healthy-gate for drills, exit reaping --- scripts/smoke.sh | 89 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 6524239..3fd2b30 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -18,14 +18,20 @@ # 8. Bad-value drill: feeds --reconcile a wrong bands value and an # oversized SSID pool (25 dummy entries), verifies both are # repaired, restores originals. -# 9. Rollback watchdog drill: runs mk8-watchdog.sh against a config +# 9. RF role drill (only when SMOKE_UPLINK_SSID is set): switches +# radio1 to the uplink role against the named lab AP, expects an +# association, then back to attack with hopping resumed. +# 10. Rollback watchdog drill: runs mk8-watchdog.sh against a config # profile while the web UI is up (expects clean promote exit), # then STOPS the pagerwebui service and expects the watchdog to # roll back and journal a 'rollback' event, then restarts webui. # Preceded by a 5-second warning countdown; brief web outage. +# Drills run only when every read-only check has passed. # # Environment: -# PASS webui password used for POST /api/login (check 7). +# PASS webui password used for POST /api/login (check 7). +# SMOKE_UPLINK_SSID lab AP SSID; enables the --write RF role drill. +# SMOKE_UPLINK_PSK optional PSK for the lab AP. # # Exit status: 0 when every executed check passes, 1 otherwise. @@ -42,9 +48,12 @@ JAR=/tmp/mk8-smoke-cookies.$$ WRITE=0 PASS="${PASS:-}" +UPLINK_SSID="${SMOKE_UPLINK_SSID:-}" +UPLINK_PSK="${SMOKE_UPLINK_PSK:-}" WEB_STOPPED=0 FAILED=0 WAIT_RC=0 +wp="" PY="$(command -v python3 2>/dev/null || true)" [ -n "$PY" ] || PY=/usr/bin/python3 @@ -53,14 +62,20 @@ PY="$(command -v python3 2>/dev/null || true)" usage() { printf 'Usage: smoke.sh [--write]\n' printf '\n' - printf 'Read-only checks run by default. --write adds two destructive\n' - printf 'drills that briefly toggle UCI config and stop/start the\n' - printf 'pagerwebui service; original values are restored automatically.\n' + printf 'Read-only checks run by default. --write adds destructive drills\n' + printf '(run only if every read-only check passed) that briefly toggle\n' + printf 'UCI config and stop/start the pagerwebui service; original values\n' + printf 'are restored automatically.\n' printf 'Set PASS= to enable the authenticated API check.\n' + printf 'Set SMOKE_UPLINK_SSID=[] to enable the\n' + printf '--write RF role drill against a lab AP.\n' } on_exit() { rm -f "$JAR" 2>/dev/null + if [ -n "$wp" ]; then + kill "$wp" 2>/dev/null + fi if [ "$WEB_STOPPED" = "1" ]; then info 'restoring pagerwebui service' "$WEBUI_INIT" start >/dev/null 2>&1 @@ -291,6 +306,67 @@ drill_bad_values() { "$PY" "$REL/server.py" --reconcile >/dev/null 2>&1 } +drill_role() { + if [ -z "$UPLINK_SSID" ]; then + skip 'role drill: set SMOKE_UPLINK_SSID to enable' + return + fi + info "role drill: uplink '$UPLINK_SSID' then attack" + out="$("$PY" - "$REL" "$LEGACY" "$UPLINK_SSID" "$UPLINK_PSK" <<'PYEOF' 2>&1 +import json +import os +import sys + +rel, legacy, ssid, psk = sys.argv[1:5] +mk8_rfplan = None +for d in (rel, legacy): + if os.path.isfile(os.path.join(d, 'mk8_rfplan.py')): + sys.path.insert(0, d) + try: + import mk8_rfplan + break + except Exception: + sys.path.remove(d) +if mk8_rfplan is None: + print('FAIL mk8_rfplan not importable from release or legacy dir') + raise SystemExit(1) + +r1 = mk8_rfplan.set_role( + 'uplink', ssid=ssid or None, psk=psk or None) +if not isinstance(r1, dict) or not r1.get('ok'): + print('FAIL uplink set_role failed: %s' % json.dumps(r1)) + raise SystemExit(1) + +assoc = mk8_rfplan.associated() +if not assoc: + print('FAIL uplink associated() returned nothing after set_role') +else: + print('associated as %s' % assoc) + +try: + r2 = mk8_rfplan.set_role('attack') +except Exception as exc: + r2 = {'ok': False, 'error': str(exc)} +if not isinstance(r2, dict) or not r2.get('ok'): + print('FAIL attack set_role failed: %s' % json.dumps(r2)) + raise SystemExit(1) +raise SystemExit(0 if assoc else 1) +PYEOF +)" + rc=$? + printf '%s\n' "$out" | sed 's/^/ /' + if [ "$rc" -ne 0 ]; then + fail 'role drill: uplink association failed (see detail above)' + return + fi + hop="$(uci_get pineapd.wlan1mon.hop)" + if [ "$hop" = "1" ]; then + pass 'role drill: uplink assoc + attack role + hopping resumed' + else + fail "role drill: pineapd.wlan1mon.hop=$hop after attack role (want 1)" + fi +} + drill_watchdog() { WDOG="$REL/mk8-watchdog.sh" [ -f "$WDOG" ] || WDOG="$LEGACY/mk8-watchdog.sh" @@ -399,9 +475,12 @@ main() { if [ "$WRITE" = "1" ]; then if [ -z "$PY" ]; then fail 'drills: python3 required but not found' + elif [ "$FAILED" -ne 0 ]; then + skip 'drills: read-only checks failed; refusing drills' else printf -- '--- --write drills ---\n' drill_bad_values + drill_role drill_watchdog fi fi