aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-01-18 13:25:00 -0600
committerCraig Jennings <c@cjennings.net>2026-01-18 13:25:00 -0600
commit3e3d24e12750a5a79925ae36b5e48598116d5636 (patch)
tree6bcfed700cbc9fff0e35c54f8b26d5defd205aee /scripts
parent3ecf4e25cb52ca1ca57c386821f66e6a3b894848 (diff)
downloadarchangel-3e3d24e12750a5a79925ae36b5e48598116d5636.tar.gz
archangel-3e3d24e12750a5a79925ae36b5e48598116d5636.zip
Add CI/CD test infrastructure
- Add Makefile with targets: all, test, test-unit, test-install, build, release, clean, lint - Add test-install.sh for automated VM installation testing - Add test configs: single-disk, mirror, raidz1, no-ssh, custom-locale - Add test-logs/ to .gitignore - Uses sshpass for SSH authentication to live ISO - Copies latest install-archzfs to VM before testing (allows testing without rebuild) - Supports --list to show available configs
Diffstat (limited to 'scripts')
-rw-r--r--scripts/test-configs/custom-locale.conf13
-rw-r--r--scripts/test-configs/mirror.conf14
-rw-r--r--scripts/test-configs/no-ssh.conf13
-rw-r--r--scripts/test-configs/raidz1.conf14
-rw-r--r--scripts/test-configs/single-disk.conf13
-rwxr-xr-xscripts/test-install.sh459
6 files changed, 526 insertions, 0 deletions
diff --git a/scripts/test-configs/custom-locale.conf b/scripts/test-configs/custom-locale.conf
new file mode 100644
index 0000000..f0cda9b
--- /dev/null
+++ b/scripts/test-configs/custom-locale.conf
@@ -0,0 +1,13 @@
+# Test config: Non-US locale (tests locale handling)
+
+HOSTNAME=test-locale
+TIMEZONE=Europe/Berlin
+LOCALE=de_DE.UTF-8
+KEYMAP=de
+
+DISKS=/dev/vda
+
+NO_ENCRYPT=yes
+ROOT_PASSWORD=testpass
+
+ENABLE_SSH=yes
diff --git a/scripts/test-configs/mirror.conf b/scripts/test-configs/mirror.conf
new file mode 100644
index 0000000..82cfa91
--- /dev/null
+++ b/scripts/test-configs/mirror.conf
@@ -0,0 +1,14 @@
+# Test config: 2-disk mirror (tests EFI redundancy)
+
+HOSTNAME=test-mirror
+TIMEZONE=UTC
+LOCALE=en_US.UTF-8
+KEYMAP=us
+
+DISKS=/dev/vda,/dev/vdb
+RAID_LEVEL=mirror
+
+NO_ENCRYPT=yes
+ROOT_PASSWORD=testpass
+
+ENABLE_SSH=yes
diff --git a/scripts/test-configs/no-ssh.conf b/scripts/test-configs/no-ssh.conf
new file mode 100644
index 0000000..7b6ff63
--- /dev/null
+++ b/scripts/test-configs/no-ssh.conf
@@ -0,0 +1,13 @@
+# Test config: SSH disabled (tests console-only verification)
+
+HOSTNAME=test-nossh
+TIMEZONE=UTC
+LOCALE=en_US.UTF-8
+KEYMAP=us
+
+DISKS=/dev/vda
+
+NO_ENCRYPT=yes
+ROOT_PASSWORD=testpass
+
+ENABLE_SSH=no
diff --git a/scripts/test-configs/raidz1.conf b/scripts/test-configs/raidz1.conf
new file mode 100644
index 0000000..4de8a78
--- /dev/null
+++ b/scripts/test-configs/raidz1.conf
@@ -0,0 +1,14 @@
+# Test config: 3-disk RAIDZ1 (tests RAID parity)
+
+HOSTNAME=test-raidz1
+TIMEZONE=UTC
+LOCALE=en_US.UTF-8
+KEYMAP=us
+
+DISKS=/dev/vda,/dev/vdb,/dev/vdc
+RAID_LEVEL=raidz1
+
+NO_ENCRYPT=yes
+ROOT_PASSWORD=testpass
+
+ENABLE_SSH=yes
diff --git a/scripts/test-configs/single-disk.conf b/scripts/test-configs/single-disk.conf
new file mode 100644
index 0000000..d540198
--- /dev/null
+++ b/scripts/test-configs/single-disk.conf
@@ -0,0 +1,13 @@
+# Test config: Single disk install (most common case)
+
+HOSTNAME=test-single
+TIMEZONE=UTC
+LOCALE=en_US.UTF-8
+KEYMAP=us
+
+DISKS=/dev/vda
+
+NO_ENCRYPT=yes
+ROOT_PASSWORD=testpass
+
+ENABLE_SSH=yes
diff --git a/scripts/test-install.sh b/scripts/test-install.sh
new file mode 100755
index 0000000..2f9c62d
--- /dev/null
+++ b/scripts/test-install.sh
@@ -0,0 +1,459 @@
+#!/bin/bash
+# test-install.sh - Automated installation testing for archzfs
+#
+# Runs unattended installs in VMs using test config files.
+# Verifies installation success via SSH (when enabled) or console.
+#
+# Usage:
+# ./test-install.sh # Run all test configs
+# ./test-install.sh single-disk # Run specific config
+# ./test-install.sh --list # List available configs
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+export PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+CONFIG_DIR="$SCRIPT_DIR/test-configs"
+LOG_DIR="$PROJECT_DIR/test-logs"
+VM_DIR="$PROJECT_DIR/vm"
+
+# VM settings
+VM_RAM="4096"
+VM_CPUS="4"
+VM_DISK_SIZE="20G"
+export SSH_PORT="2222"
+export SSH_PASSWORD="archzfs"
+SERIAL_LOG="$LOG_DIR/serial.log"
+
+# Timeouts (seconds)
+BOOT_TIMEOUT=120
+INSTALL_TIMEOUT=600
+SSH_TIMEOUT=30
+VERIFY_TIMEOUT=60
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+info() { echo -e "${GREEN}[INFO]${NC} $1"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; }
+step() { echo -e "${BLUE}[STEP]${NC} $1"; }
+
+# UEFI firmware
+OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
+OVMF_VARS_ORIG="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
+
+# Track test results
+TESTS_RUN=0
+TESTS_PASSED=0
+TESTS_FAILED=0
+FAILED_TESTS=()
+
+usage() {
+ cat <<EOF
+Usage: $0 [OPTIONS] [CONFIG_NAME...]
+
+Run automated installation tests in VMs.
+
+Options:
+ --list List available test configs
+ --help, -h Show this help
+
+Examples:
+ $0 # Run all tests
+ $0 single-disk # Run single test
+ $0 single-disk mirror # Run specific tests
+
+Available configs:
+$(ls "$CONFIG_DIR"/*.conf 2>/dev/null | xargs -n1 basename | sed 's/.conf$//' | sed 's/^/ /')
+EOF
+}
+
+list_configs() {
+ echo "Available test configs:"
+ for conf in "$CONFIG_DIR"/*.conf; do
+ [[ -f "$conf" ]] || continue
+ name=$(basename "$conf" .conf)
+ desc=$(grep "^# Test config:" "$conf" | sed 's/^# Test config: //' || echo "")
+ printf " %-20s %s\n" "$name" "$desc"
+ done
+}
+
+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/"
+ error "Build the ISO first with: make build"
+ exit 1
+ fi
+ info "Using ISO: $(basename "$ISO_FILE")"
+}
+
+# Get number of disks needed for a config
+get_disk_count() {
+ local config="$1"
+ local disks
+ disks=$(grep "^DISKS=" "$config" | cut -d= -f2 | tr ',' '\n' | wc -l)
+ echo "$disks"
+}
+
+# Create VM disks
+create_disks() {
+ local count="$1"
+ local test_name="$2"
+
+ mkdir -p "$VM_DIR"
+
+ for ((i=1; i<=count; i++)); do
+ local disk="$VM_DIR/test-${test_name}-disk${i}.qcow2"
+ if [[ -f "$disk" ]]; then
+ rm -f "$disk"
+ fi
+ qemu-img create -f qcow2 "$disk" "$VM_DISK_SIZE" >/dev/null
+ done
+}
+
+# Build QEMU disk arguments
+get_disk_args() {
+ local count="$1"
+ local test_name="$2"
+ local args=""
+
+ for ((i=1; i<=count; i++)); do
+ local disk="$VM_DIR/test-${test_name}-disk${i}.qcow2"
+ args="$args -drive file=$disk,format=qcow2,if=virtio"
+ done
+ echo "$args"
+}
+
+# Clean up VM disks
+cleanup_disks() {
+ local test_name="$1"
+ rm -f "$VM_DIR"/test-"${test_name}"-disk*.qcow2
+}
+
+# Start VM and return PID
+start_vm() {
+ local test_name="$1"
+ local disk_count="$2"
+ local disk_args
+ disk_args=$(get_disk_args "$disk_count" "$test_name")
+
+ # Copy OVMF vars for this test
+ local ovmf_vars="$VM_DIR/OVMF_VARS_${test_name}.fd"
+ cp "$OVMF_VARS_ORIG" "$ovmf_vars"
+
+ # Start VM with serial console logging
+ qemu-system-x86_64 \
+ -name "archzfs-test-$test_name" \
+ -machine type=q35,accel=kvm \
+ -cpu host \
+ -m "$VM_RAM" \
+ -smp "$VM_CPUS" \
+ -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
+ -drive if=pflash,format=raw,file="$ovmf_vars" \
+ $disk_args \
+ -cdrom "$ISO_FILE" \
+ -boot d \
+ -netdev user,id=net0,hostfwd=tcp::"$SSH_PORT"-:22 \
+ -device virtio-net-pci,netdev=net0 \
+ -serial file:"$SERIAL_LOG" \
+ -display none \
+ -daemonize \
+ -pidfile "$VM_DIR/qemu-${test_name}.pid" \
+ 2>/dev/null
+
+ sleep 2 # Give QEMU time to start
+ cat "$VM_DIR/qemu-${test_name}.pid" 2>/dev/null
+}
+
+# Stop VM
+stop_vm() {
+ local test_name="$1"
+ local pid_file="$VM_DIR/qemu-${test_name}.pid"
+
+ if [[ -f "$pid_file" ]]; then
+ local pid
+ pid=$(cat "$pid_file")
+ if kill -0 "$pid" 2>/dev/null; then
+ kill "$pid" 2>/dev/null || true
+ sleep 2
+ kill -9 "$pid" 2>/dev/null || true
+ fi
+ rm -f "$pid_file"
+ fi
+
+ # Also clean up OVMF vars
+ rm -f "$VM_DIR/OVMF_VARS_${test_name}.fd"
+}
+
+# Wait for SSH to be available
+wait_for_ssh() {
+ local timeout="$1"
+ local start_time
+ start_time=$(date +%s)
+
+ while true; do
+ if sshpass -p "$SSH_PASSWORD" ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ -p "$SSH_PORT" root@localhost "echo ok" 2>/dev/null | grep -q ok; then
+ return 0
+ fi
+
+ local elapsed=$(($(date +%s) - start_time))
+ if [[ $elapsed -ge $timeout ]]; then
+ return 1
+ fi
+
+ sleep 5
+ done
+}
+
+# Run SSH command
+ssh_cmd() {
+ sshpass -p "$SSH_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ -p "$SSH_PORT" root@localhost "$@" 2>/dev/null
+}
+
+# Copy config to VM and run install
+run_install() {
+ local config="$1"
+ local config_name
+ config_name=$(basename "$config" .conf)
+
+ # Copy latest install-archzfs script to VM (in case ISO is outdated)
+ sshpass -p "$SSH_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ -P "$SSH_PORT" "$PROJECT_DIR/custom/install-archzfs" root@localhost:/usr/local/bin/install-archzfs 2>/dev/null
+
+ # Copy config file to VM
+ sshpass -p "$SSH_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ -P "$SSH_PORT" "$config" root@localhost:/root/test.conf 2>/dev/null
+
+ # Run the installer
+ ssh_cmd "install-archzfs --config-file /root/test.conf --no-encrypt" || return 1
+
+ return 0
+}
+
+# Verify installation
+verify_install() {
+ local config="$1"
+ local enable_ssh
+ enable_ssh=$(grep "^ENABLE_SSH=" "$config" | cut -d= -f2)
+
+ # Basic checks via SSH (if enabled)
+ if [[ "$enable_ssh" == "yes" ]]; then
+ # Check if we can still SSH (install script reboots, so this won't work)
+ # Instead, check the install log for success indicators
+ if ssh_cmd "grep -q 'Installation complete' /tmp/install-archzfs.log"; then
+ info "Install log shows success"
+ else
+ warn "Could not verify install log"
+ fi
+
+ # Check pool was created
+ if ssh_cmd "zpool list zroot" >/dev/null 2>&1; then
+ info "ZFS pool 'zroot' exists"
+ else
+ error "ZFS pool 'zroot' not found"
+ return 1
+ fi
+
+ # Check genesis snapshot
+ if ssh_cmd "zfs list -t snapshot | grep -q genesis"; then
+ info "Genesis snapshot exists"
+ else
+ warn "Genesis snapshot not found (may not have completed)"
+ fi
+ else
+ # For no-SSH tests, check serial console output
+ if grep -q "Installation complete" "$SERIAL_LOG" 2>/dev/null; then
+ info "Serial console shows installation complete"
+ else
+ warn "Could not verify installation via serial console"
+ fi
+ fi
+
+ return 0
+}
+
+# Run a single test
+run_test() {
+ local config="$1"
+ local config_name
+ config_name=$(basename "$config" .conf)
+
+ TESTS_RUN=$((TESTS_RUN + 1))
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ step "Testing: $config_name"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+
+ local disk_count
+ disk_count=$(get_disk_count "$config")
+ info "Disk count: $disk_count"
+
+ # Setup
+ mkdir -p "$LOG_DIR"
+ : > "$SERIAL_LOG"
+
+ step "Creating VM disks..."
+ create_disks "$disk_count" "$config_name"
+
+ step "Starting VM..."
+ local vm_pid
+ vm_pid=$(start_vm "$config_name" "$disk_count")
+
+ if [[ -z "$vm_pid" ]]; then
+ error "Failed to start VM"
+ cleanup_disks "$config_name"
+ TESTS_FAILED=$((TESTS_FAILED + 1))
+ FAILED_TESTS+=("$config_name")
+ return 1
+ fi
+ info "VM started (PID: $vm_pid)"
+
+ # Wait for boot
+ step "Waiting for VM to boot (timeout: ${BOOT_TIMEOUT}s)..."
+ if ! wait_for_ssh "$BOOT_TIMEOUT"; then
+ error "VM did not become accessible via SSH"
+ stop_vm "$config_name"
+ cleanup_disks "$config_name"
+
+ # Save logs
+ cp "$SERIAL_LOG" "$LOG_DIR/${config_name}-serial.log" 2>/dev/null || true
+ error "Serial log saved to: $LOG_DIR/${config_name}-serial.log"
+
+ TESTS_FAILED=$((TESTS_FAILED + 1))
+ FAILED_TESTS+=("$config_name")
+ return 1
+ fi
+ info "VM is accessible via SSH"
+
+ # Run install
+ step "Running installation (timeout: ${INSTALL_TIMEOUT}s)..."
+ if timeout "$INSTALL_TIMEOUT" bash -c "$(declare -f ssh_cmd run_install); run_install '$config'"; then
+ info "Installation completed"
+ else
+ error "Installation failed or timed out"
+ stop_vm "$config_name"
+
+ # Save logs
+ ssh_cmd "cat /tmp/install-archzfs.log" > "$LOG_DIR/${config_name}-install.log" 2>/dev/null || true
+ cp "$SERIAL_LOG" "$LOG_DIR/${config_name}-serial.log" 2>/dev/null || true
+
+ cleanup_disks "$config_name"
+ TESTS_FAILED=$((TESTS_FAILED + 1))
+ FAILED_TESTS+=("$config_name")
+ return 1
+ fi
+
+ # Verify
+ step "Verifying installation..."
+ if verify_install "$config"; then
+ info "Verification passed"
+ else
+ warn "Verification had issues (may be expected if install rebooted)"
+ fi
+
+ # Cleanup
+ step "Cleaning up..."
+ stop_vm "$config_name"
+ cleanup_disks "$config_name"
+
+ TESTS_PASSED=$((TESTS_PASSED + 1))
+ echo -e "${GREEN}TEST PASSED: $config_name${NC}"
+ return 0
+}
+
+# Main
+main() {
+ # Parse args
+ local configs=()
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --list)
+ list_configs
+ exit 0
+ ;;
+ --help|-h)
+ usage
+ exit 0
+ ;;
+ *)
+ configs+=("$1")
+ shift
+ ;;
+ esac
+ done
+
+ # Check dependencies
+ command -v qemu-system-x86_64 >/dev/null 2>&1 || { error "qemu not found"; exit 1; }
+ command -v sshpass >/dev/null 2>&1 || { error "sshpass not found"; exit 1; }
+ [[ -f "$OVMF_CODE" ]] || { error "OVMF not found: $OVMF_CODE"; exit 1; }
+
+ # Find ISO
+ find_iso
+
+ # Determine which configs to run
+ if [[ ${#configs[@]} -eq 0 ]]; then
+ # Run all configs
+ for conf in "$CONFIG_DIR"/*.conf; do
+ [[ -f "$conf" ]] && configs+=("$(basename "$conf" .conf)")
+ done
+ fi
+
+ if [[ ${#configs[@]} -eq 0 ]]; then
+ error "No test configs found in $CONFIG_DIR"
+ exit 1
+ fi
+
+ info "Running ${#configs[@]} test(s): ${configs[*]}"
+ echo ""
+
+ # Run tests
+ for config_name in "${configs[@]}"; do
+ local config="$CONFIG_DIR/${config_name}.conf"
+ if [[ ! -f "$config" ]]; then
+ error "Config not found: $config"
+ TESTS_FAILED=$((TESTS_FAILED + 1))
+ FAILED_TESTS+=("$config_name")
+ continue
+ fi
+
+ run_test "$config" || true
+ done
+
+ # Summary
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "TEST SUMMARY"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo -e "Tests run: $TESTS_RUN"
+ echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
+ echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
+
+ if [[ ${#FAILED_TESTS[@]} -gt 0 ]]; then
+ echo ""
+ echo "Failed tests:"
+ for t in "${FAILED_TESTS[@]}"; do
+ echo -e " ${RED}✗${NC} $t"
+ done
+ fi
+
+ echo ""
+ if [[ $TESTS_FAILED -eq 0 ]]; then
+ echo -e "${GREEN}All tests passed!${NC}"
+ exit 0
+ else
+ echo -e "${RED}Some tests failed.${NC}"
+ echo "Check logs in: $LOG_DIR"
+ exit 1
+ fi
+}
+
+main "$@"