From 150d68be118798defc2616ec5f01cf230255d1db Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 19 Jan 2026 10:59:21 -0600 Subject: Add automated sanity test for ISO verification New scripts/sanity-test.sh: - Boots ISO in headless QEMU - Waits for SSH availability - Runs 13 automated verification tests: - ZFS module loaded and working - Custom scripts present (zfsrollback, zfssnapshot, etc.) - fzf installed - LTS kernel running - archsetup directory present - Reports pass/fail with summary - Fully automated - no human input required Updated build-release to use automated sanity test instead of manual verification prompt. --- scripts/build-release | 25 +--- scripts/sanity-test.sh | 331 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 20 deletions(-) create mode 100755 scripts/sanity-test.sh (limited to 'scripts') diff --git a/scripts/build-release b/scripts/build-release index 0fc3693..f1cbe1e 100755 --- a/scripts/build-release +++ b/scripts/build-release @@ -79,28 +79,13 @@ build_iso() { find_iso } -# Run sanity test in QEMU +# Run sanity test in QEMU (automated) sanity_test() { - step "Sanity Test" - info "Booting ISO in QEMU for verification..." - info "Please verify:" - echo " 1. System boots to login prompt" - echo " 2. ZFS module loads (run 'zpool status' or 'lsmod | grep zfs')" - echo " 3. Custom scripts are present (ls /usr/local/bin/)" - echo "" - - # Start QEMU in background - "$SCRIPT_DIR/test-vm.sh" & - QEMU_PID=$! - - echo "" - read -p "Press Enter once you've verified the ISO works (or Ctrl+C to abort)... " + step "Sanity Test (Automated)" - # Kill QEMU if still running - if kill -0 $QEMU_PID 2>/dev/null; then - info "Shutting down test VM..." - kill $QEMU_PID 2>/dev/null || true - wait $QEMU_PID 2>/dev/null || true + if ! "$SCRIPT_DIR/sanity-test.sh"; then + error "Sanity test failed!" + exit 1 fi info "Sanity test passed!" diff --git a/scripts/sanity-test.sh b/scripts/sanity-test.sh new file mode 100755 index 0000000..977f018 --- /dev/null +++ b/scripts/sanity-test.sh @@ -0,0 +1,331 @@ +#!/bin/bash +# sanity-test.sh - Automated sanity test for archzfs ISO +# +# Boots the ISO in a headless QEMU VM, waits for SSH, runs verification +# commands, and reports pass/fail. Fully automated - no human input required. +# +# Usage: +# ./scripts/sanity-test.sh # Run sanity test +# ./scripts/sanity-test.sh --verbose # Show detailed output +# +# Exit codes: +# 0 - All tests passed +# 1 - One or more tests failed +# 2 - Setup/infrastructure error (QEMU, SSH, etc.) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# VM Configuration +VM_DIR="$PROJECT_DIR/vm" +VM_DISK="$VM_DIR/sanity-test.qcow2" +VM_DISK_SIZE="10G" +VM_RAM="2048" +VM_CPUS="2" +VM_NAME="archzfs-sanity" + +# UEFI firmware +OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd" +OVMF_VARS_ORIG="/usr/share/edk2/x64/OVMF_VARS.4m.fd" +OVMF_VARS="$VM_DIR/sanity-test-OVMF_VARS.fd" + +# SSH settings +SSH_PORT=2223 # Different port to avoid conflicts with test-vm.sh +SSH_USER="root" +SSH_PASS="archzfs" +SSH_TIMEOUT=180 # Max seconds to wait for SSH +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +# State +QEMU_PID="" +VERBOSE=false +TESTS_PASSED=0 +TESTS_FAILED=0 + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } +pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)); } +fail() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)); } + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --verbose|-v) VERBOSE=true; shift ;; + -h|--help) + echo "Usage: $0 [--verbose]" + echo "" + echo "Automated sanity test for archzfs ISO." + echo "Boots ISO in headless QEMU, verifies via SSH, reports results." + exit 0 + ;; + *) error "Unknown option: $1"; exit 2 ;; + esac +done + +# Find the ISO +find_iso() { + ISO_FILE=$(ls -t "$PROJECT_DIR/out/"*.iso 2>/dev/null | head -1) + if [[ -z "$ISO_FILE" ]]; then + error "No ISO found in $PROJECT_DIR/out/" + exit 2 + fi + info "Testing ISO: $(basename "$ISO_FILE")" +} + +# Setup VM resources +setup_vm() { + mkdir -p "$VM_DIR" + + # Create a fresh disk for sanity testing + if [[ -f "$VM_DISK" ]]; then + rm -f "$VM_DISK" + fi + qemu-img create -f qcow2 "$VM_DISK" "$VM_DISK_SIZE" >/dev/null 2>&1 + + # Copy OVMF vars + cp "$OVMF_VARS_ORIG" "$OVMF_VARS" +} + +# Cleanup on exit +cleanup() { + if [[ -n "$QEMU_PID" ]] && kill -0 "$QEMU_PID" 2>/dev/null; then + info "Shutting down VM..." + kill "$QEMU_PID" 2>/dev/null || true + wait "$QEMU_PID" 2>/dev/null || true + fi + # Clean up sanity test disk (leave main test disk alone) + rm -f "$VM_DISK" "$OVMF_VARS" 2>/dev/null || true +} +trap cleanup EXIT + +# Start QEMU in headless mode +start_vm() { + info "Starting headless VM..." + + qemu-system-x86_64 \ + -name "$VM_NAME" \ + -machine q35,accel=kvm \ + -cpu host \ + -smp "$VM_CPUS" \ + -m "$VM_RAM" \ + -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \ + -drive if=pflash,format=raw,file="$OVMF_VARS" \ + -drive "file=$VM_DISK,format=qcow2,if=virtio" \ + -cdrom "$ISO_FILE" \ + -boot d \ + -netdev user,id=net0,hostfwd=tcp::${SSH_PORT}-:22 \ + -device virtio-net-pci,netdev=net0 \ + -display none \ + -serial null \ + -daemonize \ + -pidfile "$VM_DIR/sanity-test.pid" + + sleep 1 + if [[ -f "$VM_DIR/sanity-test.pid" ]]; then + QEMU_PID=$(cat "$VM_DIR/sanity-test.pid") + if kill -0 "$QEMU_PID" 2>/dev/null; then + info "VM started (PID: $QEMU_PID)" + else + error "VM failed to start" + exit 2 + fi + else + error "VM failed to start - no PID file" + exit 2 + fi +} + +# Wait for SSH to become available +wait_for_ssh() { + info "Waiting for SSH (timeout: ${SSH_TIMEOUT}s)..." + local elapsed=0 + local interval=5 + + while [[ $elapsed -lt $SSH_TIMEOUT ]]; do + if sshpass -p "$SSH_PASS" ssh $SSH_OPTS -p "$SSH_PORT" "$SSH_USER@localhost" "true" 2>/dev/null; then + info "SSH available after ${elapsed}s" + return 0 + fi + sleep $interval + ((elapsed += interval)) + if $VERBOSE; then + echo -n "." + fi + done + + error "SSH timeout after ${SSH_TIMEOUT}s" + return 1 +} + +# Run a test command via SSH +run_test() { + local name="$1" + local cmd="$2" + local expect_output="$3" # Optional: string that should be in output + + if $VERBOSE; then + echo -e "${CYAN}Testing:${NC} $name" + echo -e "${CYAN}Command:${NC} $cmd" + fi + + local output + output=$(sshpass -p "$SSH_PASS" ssh $SSH_OPTS -p "$SSH_PORT" "$SSH_USER@localhost" "$cmd" 2>&1) || { + fail "$name (command failed)" + if $VERBOSE; then + echo " Output: $output" + fi + return 1 + } + + if [[ -n "$expect_output" ]]; then + if echo "$output" | grep -q "$expect_output"; then + pass "$name" + if $VERBOSE; then + echo " Output: $output" + fi + return 0 + else + fail "$name (expected '$expect_output' not found)" + if $VERBOSE; then + echo " Output: $output" + fi + return 1 + fi + else + pass "$name" + if $VERBOSE; then + echo " Output: $output" + fi + return 0 + fi +} + +# Run all sanity tests +run_sanity_tests() { + echo "" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${CYAN} SANITY TESTS${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + + # Test 1: ZFS kernel module loaded + run_test "ZFS kernel module loaded" \ + "lsmod | grep -q '^zfs' && echo 'zfs module loaded'" \ + "zfs module loaded" + + # Test 2: ZFS commands work + run_test "ZFS version command works" \ + "zfs version | head -1" \ + "zfs-" + + # Test 3: zpool command works + run_test "zpool command works" \ + "zpool version | head -1" \ + "zfs-" + + # Test 4: Custom scripts present + run_test "install-archzfs script present" \ + "test -x /usr/local/bin/install-archzfs && echo 'exists'" \ + "exists" + + run_test "zfsrollback script present" \ + "test -x /usr/local/bin/zfsrollback && echo 'exists'" \ + "exists" + + run_test "zfssnapshot script present" \ + "test -x /usr/local/bin/zfssnapshot && echo 'exists'" \ + "exists" + + run_test "grub-zfs-snap script present" \ + "test -x /usr/local/bin/grub-zfs-snap && echo 'exists'" \ + "exists" + + run_test "zfs-snap-prune script present" \ + "test -x /usr/local/bin/zfs-snap-prune && echo 'exists'" \ + "exists" + + # Test 5: fzf installed (required by zfsrollback) + run_test "fzf installed" \ + "command -v fzf && echo 'found'" \ + "found" + + # Test 6: SSH is working (implicit - we're connected) + pass "SSH connectivity" + + # Test 7: Network manager available + run_test "NetworkManager available" \ + "systemctl is-enabled NetworkManager 2>/dev/null || echo 'available'" \ + "" + + # Test 8: Kernel version (LTS) + run_test "Running LTS kernel" \ + "uname -r" \ + "lts" + + # Test 9: archsetup directory present + run_test "archsetup directory present" \ + "test -d /code/archsetup && echo 'exists'" \ + "exists" + + echo "" +} + +# Print summary +print_summary() { + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${CYAN} SUMMARY${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e " Tests passed: ${GREEN}$TESTS_PASSED${NC}" + echo -e " Tests failed: ${RED}$TESTS_FAILED${NC}" + echo "" + + if [[ $TESTS_FAILED -eq 0 ]]; then + echo -e "${GREEN}All sanity tests passed!${NC}" + return 0 + else + echo -e "${RED}Some tests failed.${NC}" + return 1 + fi +} + +# Main +main() { + echo "" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${CYAN} ARCHZFS ISO SANITY TEST${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + + # Check dependencies + command -v qemu-system-x86_64 >/dev/null || { error "qemu-system-x86_64 not found"; exit 2; } + command -v sshpass >/dev/null || { error "sshpass not found"; exit 2; } + [[ -f "$OVMF_CODE" ]] || { error "OVMF firmware not found at $OVMF_CODE"; exit 2; } + + find_iso + setup_vm + start_vm + + if ! wait_for_ssh; then + error "Could not connect to VM via SSH" + exit 2 + fi + + run_sanity_tests + + if print_summary; then + exit 0 + else + exit 1 + fi +} + +main "$@" -- cgit v1.2.3