#!/bin/bash # SPDX-License-Identifier: GPL-3.0-or-later # Run net-doctor control-plane repair scenarios against a booted VM. # Author: Craig Jennings # License: GNU GPLv3 # # Live verification for net Phase 1's three privileged fixes (unmask-nm, # disable-rival, chmod-keyfile). Each scenario breaks the target in a known # way, runs the real `net doctor --fix` inside it, and asserts the post-state. # These states are dangerous on a daily driver (masking NM kills the network), # so they run against a DISPOSABLE VM you can revert, never ratio/velox. # # The target must be a booted VM (a real kernel + network stack — NetworkManager # does not run reliably in an nspawn container) reachable over ssh as root, with # NetworkManager and dhcpcd installed. The runner rsyncs the net + panelkit # source trees in and runs the doctor from them via a small wrapper (no install # needed), matching the stowed shim. jq is used host-side to read the JSON. # # STATUS: first-draft harness. The scenario break/fix/assert logic is the # reviewed part; the ssh transport plumbing has not been exercised against a # live target yet and may need a shakeout on the first real run. # # Usage: run-net-scenarios.sh --target root@HOST [--list] [--profile NAME] # --target ssh destination of the booted VM (required unless --list) # --profile a saved NM profile name for the keyfile scenario # (else the keyfile scenario is skipped) # --list print the scenario plan and exit # # Reverting the VM to a clean snapshot between runs is the caller's job; each # scenario also restores what it broke on the way out. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCENARIO_DIR="${NET_SCENARIO_DIR:-$SCRIPT_DIR/net-scenarios}" DOTFILES="${DOTFILES:-$HOME/.dotfiles}" T_NET="/root/net-scenario-tree" # where the source trees land in the target TARGET="" LIST_ONLY=false export NET_SCENARIO_PROFILE="${NET_SCENARIO_PROFILE:-}" while [[ $# -gt 0 ]]; do case $1 in --target) TARGET="$2"; shift 2 ;; --profile) NET_SCENARIO_PROFILE="$2"; shift 2 ;; --list) LIST_ONLY=true; shift ;; *) echo "usage: $0 --target root@HOST [--list] [--profile NAME]" >&2; exit 1 ;; esac done mapfile -t S_FILES < <(find "$SCENARIO_DIR" -maxdepth 1 -name '*.sh' | sort) if [ "$LIST_ONLY" = true ]; then echo "net doctor scenario plan:" for f in "${S_FILES[@]}"; do ( . "$f"; printf ' %-24s %s\n' "$(basename "$f" .sh)" "$SCENARIO_DESC" ) done exit 0 fi [ -n "$TARGET" ] || { echo "--target root@HOST is required" >&2; exit 1; } command -v jq >/dev/null || { echo "jq is required host-side" >&2; exit 1; } SSH() { ssh -o BatchMode=yes "$TARGET" "$@"; } # ── target helpers (available to the scenario scripts) ─────────────────── # nexec runs a payload in the target as root; the doctor runs through the # /root/net-run wrapper synced below, so no fragile inline quoting is needed. nexec() { SSH "bash -lc $(printf '%q' "$*")"; } nfix() { nexec "/root/net-run doctor --fix --json"; } ndoctor_json() { nexec "/root/net-run doctor --json" | jq -er "$1"; } info() { printf ' %s\n' "$*"; } pass() { printf ' PASS %s\n' "$*"; } fail() { printf ' FAIL %s\n' "$*"; } # ── sync the source trees + the doctor wrapper into the target ─────────── echo "==> syncing net + panelkit into $TARGET:$T_NET" SSH "mkdir -p $T_NET/net $T_NET/panelkit" rsync -a -e "ssh -o BatchMode=yes" "$DOTFILES/net/src" "$TARGET:$T_NET/net/" rsync -a -e "ssh -o BatchMode=yes" "$DOTFILES/panelkit/src" "$TARGET:$T_NET/panelkit/" # The wrapper: run as root, so NET_SUDO empty (commands run directly) and # PANELKIT_SUDO=sudo (root's `sudo -n true` succeeds -> the model resolves RUN). SSH "cat > /root/net-run" </dev/null; then if scenario_diagnose_expect; then pass "$name: diagnose named it" else fail "$name: diagnose did NOT name it (inspect net doctor --json)"; fi fi scenario_fix || info "$name: net doctor --fix returned non-zero" if scenario_assert; then pass "$name: repaired" else fail "$name: NOT repaired"; exit 1; fi ) || fails=$((fails + 1)) done echo [ "$fails" -eq 0 ] && echo "all scenarios passed" || echo "$fails scenario(s) failed" exit "$fails"