243 lines
8.9 KiB
Bash
243 lines
8.9 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# ─────────────────────────────────────────────────────────────────────
|
||
|
|
# test-bastion.sh — End-to-end test of PXE bastion using QEMU
|
||
|
|
#
|
||
|
|
# Creates an isolated virtual network, starts the bastion in full DHCP
|
||
|
|
# mode, and PXE boots a QEMU VM to test the discovery flow.
|
||
|
|
#
|
||
|
|
# Uses aarch64 + KVM on Apple Silicon for near-native speed.
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# sudo bash test-bastion.sh # discover test (default)
|
||
|
|
# sudo bash test-bastion.sh --install # discover + install test
|
||
|
|
# sudo bash test-bastion.sh --cleanup # remove test artifacts
|
||
|
|
#
|
||
|
|
# Requirements: qemu-system-aarch64, edk2-aarch64, dnsmasq, python3
|
||
|
|
# ─────────────────────────────────────────────────────────────────────
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
|
|
MODE="${1:---discover}"
|
||
|
|
|
||
|
|
# Virtual network config
|
||
|
|
BRIDGE="lab-br0"
|
||
|
|
TAP="lab-tap0"
|
||
|
|
BRIDGE_IP="10.99.0.1"
|
||
|
|
BRIDGE_CIDR="${BRIDGE_IP}/24"
|
||
|
|
BRIDGE_NET="10.99.0.0"
|
||
|
|
|
||
|
|
# Test dir
|
||
|
|
TEST_DIR="/tmp/lab-bastion-test"
|
||
|
|
BASTION_LOG="$TEST_DIR/bastion.log"
|
||
|
|
DISK="$TEST_DIR/test-disk.qcow2"
|
||
|
|
OVMF_CODE="/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw"
|
||
|
|
OVMF_VARS_TEMPLATE="/usr/share/AAVMF/AAVMF_VARS.fd"
|
||
|
|
OVMF_VARS="$TEST_DIR/AAVMF_VARS.fd"
|
||
|
|
|
||
|
|
# Colors
|
||
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||
|
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||
|
|
|
||
|
|
log() { echo -e "${GREEN}[test]${NC} $*"; }
|
||
|
|
warn() { echo -e "${YELLOW}[test]${NC} $*"; }
|
||
|
|
die() { echo -e "${RED}[test]${NC} $*" >&2; exit 1; }
|
||
|
|
|
||
|
|
# ──── Cleanup subcommand ──────────────────────────────────────────
|
||
|
|
if [[ "$MODE" == "--cleanup" ]]; then
|
||
|
|
echo "Cleaning up test artifacts..."
|
||
|
|
ip link del "$TAP" 2>/dev/null || true
|
||
|
|
ip link del "$BRIDGE" 2>/dev/null || true
|
||
|
|
rm -rf "$TEST_DIR"
|
||
|
|
echo "Done."
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ──── Preflight ───────────────────────────────────────────────────
|
||
|
|
[[ $EUID -eq 0 ]] || die "Run as root: sudo bash test-bastion.sh"
|
||
|
|
|
||
|
|
MISSING=""
|
||
|
|
command -v qemu-system-aarch64 >/dev/null || MISSING="$MISSING qemu-system-aarch64"
|
||
|
|
command -v qemu-img >/dev/null || MISSING="$MISSING qemu-img"
|
||
|
|
command -v dnsmasq >/dev/null || MISSING="$MISSING dnsmasq"
|
||
|
|
command -v python3 >/dev/null || MISSING="$MISSING python3"
|
||
|
|
command -v curl >/dev/null || MISSING="$MISSING curl"
|
||
|
|
|
||
|
|
[[ -z "$MISSING" ]] || die "Missing:$MISSING\n Install with: sudo dnf install$MISSING"
|
||
|
|
[[ -f "$OVMF_CODE" ]] || die "UEFI firmware not found: $OVMF_CODE\n Install with: sudo dnf install edk2-aarch64"
|
||
|
|
[[ -e /dev/kvm ]] || die "/dev/kvm not available — KVM required for aarch64 testing"
|
||
|
|
|
||
|
|
mkdir -p "$TEST_DIR"
|
||
|
|
|
||
|
|
# ──── Cleanup handler ─────────────────────────────────────────────
|
||
|
|
BASTION_PID=""
|
||
|
|
TAIL_PID=""
|
||
|
|
|
||
|
|
cleanup() {
|
||
|
|
echo ""
|
||
|
|
log "Cleaning up..."
|
||
|
|
[[ -n "$TAIL_PID" ]] && kill "$TAIL_PID" 2>/dev/null || true
|
||
|
|
[[ -n "$BASTION_PID" ]] && kill "$BASTION_PID" 2>/dev/null || true
|
||
|
|
sleep 1
|
||
|
|
ip link set "$TAP" down 2>/dev/null || true
|
||
|
|
ip link del "$TAP" 2>/dev/null || true
|
||
|
|
ip link set "$BRIDGE" down 2>/dev/null || true
|
||
|
|
ip link del "$BRIDGE" 2>/dev/null || true
|
||
|
|
log "Done. Logs: $BASTION_LOG State: $TEST_DIR/bastion/state.json"
|
||
|
|
}
|
||
|
|
trap cleanup EXIT INT TERM
|
||
|
|
|
||
|
|
# ──── Create isolated virtual network ─────────────────────────────
|
||
|
|
log "Creating virtual network ${BOLD}${BRIDGE_NET}/24${NC} ..."
|
||
|
|
|
||
|
|
# Clean up leftovers from previous runs
|
||
|
|
ip link del "$TAP" 2>/dev/null || true
|
||
|
|
ip link del "$BRIDGE" 2>/dev/null || true
|
||
|
|
|
||
|
|
ip link add "$BRIDGE" type bridge
|
||
|
|
ip addr add "$BRIDGE_CIDR" dev "$BRIDGE"
|
||
|
|
ip link set "$BRIDGE" up
|
||
|
|
|
||
|
|
ip tuntap add dev "$TAP" mode tap
|
||
|
|
ip link set "$TAP" master "$BRIDGE"
|
||
|
|
ip link set "$TAP" up
|
||
|
|
|
||
|
|
log "Bridge ${BOLD}$BRIDGE${NC} at ${BOLD}$BRIDGE_IP${NC}, tap ${BOLD}$TAP${NC}"
|
||
|
|
|
||
|
|
# ──── Start bastion ───────────────────────────────────────────────
|
||
|
|
log "Starting bastion (full DHCP mode, aarch64)..."
|
||
|
|
|
||
|
|
# Override ARCH to aarch64 for the test VM
|
||
|
|
IFACE="$BRIDGE" \
|
||
|
|
DHCP_MODE="full" \
|
||
|
|
ARCH="aarch64" \
|
||
|
|
BASTION_DIR="$TEST_DIR/bastion" \
|
||
|
|
HTTP_PORT=8080 \
|
||
|
|
bash "$SCRIPT_DIR/bastion.sh" serve > "$BASTION_LOG" 2>&1 &
|
||
|
|
BASTION_PID=$!
|
||
|
|
|
||
|
|
# Tail bastion output
|
||
|
|
sleep 1
|
||
|
|
tail -f "$BASTION_LOG" --pid=$BASTION_PID 2>/dev/null &
|
||
|
|
TAIL_PID=$!
|
||
|
|
|
||
|
|
# Wait for bastion HTTP to be ready
|
||
|
|
log "Waiting for bastion to start..."
|
||
|
|
READY=false
|
||
|
|
for i in $(seq 1 60); do
|
||
|
|
if curl -sf "http://${BRIDGE_IP}:8080/boot.ipxe" >/dev/null 2>&1; then
|
||
|
|
READY=true
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
if ! kill -0 "$BASTION_PID" 2>/dev/null; then
|
||
|
|
echo ""
|
||
|
|
log "Bastion failed to start. Last 20 lines:"
|
||
|
|
tail -20 "$BASTION_LOG"
|
||
|
|
die "Bastion exited unexpectedly"
|
||
|
|
fi
|
||
|
|
sleep 1
|
||
|
|
done
|
||
|
|
|
||
|
|
$READY || die "Bastion HTTP not responding after 60s"
|
||
|
|
log "Bastion is ready!"
|
||
|
|
|
||
|
|
# ──── Prepare UEFI vars and disk ──────────────────────────────────
|
||
|
|
if [[ ! -f "$OVMF_VARS" ]]; then
|
||
|
|
cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [[ ! -f "$DISK" ]]; then
|
||
|
|
log "Creating 20G test disk..."
|
||
|
|
qemu-img create -f qcow2 "$DISK" 20G >/dev/null
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ──── Boot QEMU VM ────────────────────────────────────────────────
|
||
|
|
echo ""
|
||
|
|
log "${BOLD}Booting QEMU VM (aarch64 + KVM — PXE network boot)${NC}"
|
||
|
|
log "UEFI firmware will attempt PXE boot automatically."
|
||
|
|
log "Watch for ${BOLD}'NEW MACHINE DISCOVERED'${NC} in bastion output."
|
||
|
|
echo ""
|
||
|
|
echo -e "${CYAN}──── QEMU console ────${NC}"
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# aarch64 UEFI PXE boot with KVM acceleration
|
||
|
|
# - virtio-net-pci for networking (UEFI has PXE driver)
|
||
|
|
# - pflash for UEFI firmware (code + vars)
|
||
|
|
# - no disk boot priority → falls through to PXE
|
||
|
|
qemu-system-aarch64 \
|
||
|
|
-machine virt,gic-version=3 \
|
||
|
|
-cpu host \
|
||
|
|
--enable-kvm \
|
||
|
|
-m 2048 \
|
||
|
|
-smp 2 \
|
||
|
|
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
||
|
|
-drive if=pflash,format=raw,file="$OVMF_VARS" \
|
||
|
|
-drive if=virtio,format=qcow2,file="$DISK" \
|
||
|
|
-netdev tap,id=net0,ifname="$TAP",script=no,downscript=no \
|
||
|
|
-device virtio-net-pci,netdev=net0 \
|
||
|
|
-boot n \
|
||
|
|
-nographic
|
||
|
|
|
||
|
|
# ──── Post-test ───────────────────────────────────────────────────
|
||
|
|
echo ""
|
||
|
|
log "QEMU exited. Checking bastion state..."
|
||
|
|
|
||
|
|
STATE=$(curl -sf "http://${BRIDGE_IP}:8080/api/machines" 2>/dev/null || echo '{}')
|
||
|
|
DISCOVERED=$(echo "$STATE" | python3 -c "
|
||
|
|
import sys, json
|
||
|
|
state = json.load(sys.stdin)
|
||
|
|
print(len(state.get('discovered', {})))
|
||
|
|
" 2>/dev/null || echo "0")
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
if [[ "$DISCOVERED" -gt 0 ]]; then
|
||
|
|
log "${GREEN}${BOLD}SUCCESS — $DISCOVERED machine(s) discovered!${NC}"
|
||
|
|
HTTP_PORT=8080 bash "$SCRIPT_DIR/bastion.sh" list 2>/dev/null || \
|
||
|
|
echo "$STATE" | python3 -m json.tool
|
||
|
|
else
|
||
|
|
warn "No machines discovered. Check bastion log: $BASTION_LOG"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ──── Install phase (if requested) ────────────────────────────────
|
||
|
|
if [[ "$MODE" == "--install" && "$DISCOVERED" -gt 0 ]]; then
|
||
|
|
MAC=$(echo "$STATE" | python3 -c "
|
||
|
|
import sys, json
|
||
|
|
state = json.load(sys.stdin)
|
||
|
|
print(list(state.get('discovered', {}).keys())[0])
|
||
|
|
" 2>/dev/null)
|
||
|
|
|
||
|
|
if [[ -n "$MAC" ]]; then
|
||
|
|
echo ""
|
||
|
|
log "Install mode: queuing ${BOLD}$MAC${NC} as ${BOLD}test-node${NC}..."
|
||
|
|
HTTP_PORT=8080 bash "$SCRIPT_DIR/bastion.sh" install "$MAC" test-node
|
||
|
|
|
||
|
|
# Reset UEFI vars so it PXE boots again (not from disk)
|
||
|
|
cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS"
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
log "Re-booting QEMU for install phase..."
|
||
|
|
echo ""
|
||
|
|
echo -e "${CYAN}──── QEMU console (install phase) ────${NC}"
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
qemu-system-aarch64 \
|
||
|
|
-machine virt,gic-version=3 \
|
||
|
|
-cpu host \
|
||
|
|
--enable-kvm \
|
||
|
|
-m 2048 \
|
||
|
|
-smp 2 \
|
||
|
|
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
||
|
|
-drive if=pflash,format=raw,file="$OVMF_VARS" \
|
||
|
|
-drive if=virtio,format=qcow2,file="$DISK" \
|
||
|
|
-netdev tap,id=net0,ifname="$TAP",script=no,downscript=no \
|
||
|
|
-device virtio-net-pci,netdev=net0 \
|
||
|
|
-boot n \
|
||
|
|
-nographic
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
log "Install phase complete."
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
log "Test finished."
|