diff options
Diffstat (limited to 'scripts/testing')
| -rw-r--r-- | scripts/testing/README.org | 379 | ||||
| -rw-r--r-- | scripts/testing/archinstall-config.json | 117 | ||||
| -rw-r--r-- | scripts/testing/archsetup-test.conf | 14 | ||||
| -rw-r--r-- | scripts/testing/archsetup-vm.conf | 11 | ||||
| -rwxr-xr-x | scripts/testing/cleanup-tests.sh | 125 | ||||
| -rwxr-xr-x | scripts/testing/create-base-vm.sh | 249 | ||||
| -rwxr-xr-x | scripts/testing/debug-vm.sh | 122 | ||||
| -rwxr-xr-x | scripts/testing/finalize-base-vm.sh | 31 | ||||
| -rw-r--r-- | scripts/testing/lib/network-diagnostics.sh | 24 | ||||
| -rw-r--r-- | scripts/testing/lib/validation.sh | 11 | ||||
| -rwxr-xr-x | scripts/testing/lib/vm-utils.sh | 471 | ||||
| -rwxr-xr-x | scripts/testing/run-test.sh | 177 | ||||
| -rwxr-xr-x | scripts/testing/setup-testing-env.sh | 104 |
13 files changed, 724 insertions, 1111 deletions
diff --git a/scripts/testing/README.org b/scripts/testing/README.org index a52dbea..e58a89e 100644 --- a/scripts/testing/README.org +++ b/scripts/testing/README.org @@ -10,20 +10,23 @@ This directory contains the complete testing infrastructure for archsetup, built *Realism over speed.* We use full VMs (not containers) to test everything archsetup does: user creation, systemd services, X11/DWM, hardware drivers, and boot process. +** Architecture + +Uses direct QEMU with user-mode networking and SSH port forwarding. No libvirt/virsh dependency. Base VM creation is fully automated via the archangel ISO (unattended Arch Linux installer). + * Quick Start ** One-Time Setup #+begin_src bash -# Install required packages and configure environment +# Install required packages (qemu-full, sshpass, edk2-ovmf, socat) ./scripts/testing/setup-testing-env.sh -# Log out and back in (for libvirt group membership) +# Copy an archangel ISO to vm-images/ +cp /path/to/archzfs-*.iso vm-images/ -# Create the base VM (minimal Arch installation) +# Create the base VM (fully automated, no manual steps) ./scripts/testing/create-base-vm.sh -# Follow on-screen instructions to complete installation -# Run finalize-base-vm.sh when done #+end_src ** Run a Test @@ -42,14 +45,11 @@ This directory contains the complete testing infrastructure for archsetup, built ** Debug Interactively #+begin_src bash -# Clone base VM for debugging -./scripts/testing/debug-vm.sh - -# Use existing test disk -./scripts/testing/debug-vm.sh test-results/20251108-143000/test.qcow2 - -# Use base VM (read-only) +# Launch base VM with graphical display (copy-on-write overlay, safe) ./scripts/testing/debug-vm.sh --base + +# Use existing test disk directly +./scripts/testing/debug-vm.sh vm-images/some-disk.qcow2 #+end_src ** Clean Up @@ -72,82 +72,44 @@ This directory contains the complete testing infrastructure for archsetup, built *Purpose:* One-time setup of testing infrastructure *What it does:* -- Installs QEMU/KVM, libvirt, and related tools -- Configures libvirt networking -- Adds user to libvirt group +- Installs QEMU/KVM, sshpass, OVMF firmware, and socat - Verifies KVM support - Creates directories for artifacts *When to run:* Once per development machine -*Usage:* -#+begin_src bash -./scripts/testing/setup-testing-env.sh -#+end_src - ** create-base-vm.sh *Purpose:* Create the "golden image" minimal Arch VM *What it does:* -- Downloads latest Arch ISO -- Creates VM and boots from ISO -- Opens virt-viewer for you to complete installation manually +- Boots an archangel ISO in QEMU with SSH port forwarding +- Waits for SSH to become available on the live ISO +- Copies archangel config file (=archsetup-test.conf=) into the VM +- Runs =archangel --config-file= for fully unattended installation +- Boots the installed system and verifies SSH/services +- Creates a "clean-install" qemu-img snapshot *When to run:* Once (or when you want to refresh base image) -*Usage:* -#+begin_src bash -./scripts/testing/create-base-vm.sh -#+end_src - -*Process:* -1. Script creates VM and boots from Arch ISO -2. virt-viewer opens automatically showing VM display -3. You complete installation manually using archinstall: - - Login as root (no password) - - Run =archinstall= - - Configure: hostname=archsetup-test, root password=archsetup - - Install packages: openssh git vim sudo - - Enable services: sshd, dhcpcd - - Configure SSH to allow root login - - Poweroff when done -4. Run =./scripts/testing/finalize-base-vm.sh= to complete - -*See also:* [[file:../../docs/base-vm-installation-checklist.org][Base VM Installation Checklist]] - -*Result:* Base VM image at =vm-images/archsetup-base.qcow2= +*Result:* Base VM image at =vm-images/archsetup-base.qcow2= with "clean-install" snapshot ** run-test.sh *Purpose:* Execute a full test run of archsetup *What it does:* -- Clones base VM (copy-on-write, fast) -- Starts test VM -- Transfers archsetup script and dotfiles +- Reverts base VM to clean-install snapshot +- Boots the VM via QEMU +- Transfers archsetup script and dotfiles via git bundle - Executes archsetup inside VM - Captures logs and results -- Runs validation checks +- Runs comprehensive validation checks - Generates test report -- Cleans up (unless =--keep=) +- Reverts to clean snapshot (unless =--keep=) *When to run:* Every time you want to test archsetup -*Usage:* -#+begin_src bash -# Test current archsetup -./scripts/testing/run-test.sh - -# Test specific version -./scripts/testing/run-test.sh --script /path/to/archsetup - -# Keep VM for debugging -./scripts/testing/run-test.sh --keep -#+end_src - -*Time:* 30-60 minutes (mostly downloading packages) - *Results:* Saved to =test-results/TIMESTAMP/= - =test.log= - Complete log output - =test-report.txt= - Summary of results @@ -159,51 +121,34 @@ This directory contains the complete testing infrastructure for archsetup, built *Purpose:* Launch VM for interactive debugging *What it does:* -- Creates VM from base image or existing test disk -- Configures console and SSH access -- Provides connection instructions +- Creates a copy-on-write overlay disk (instant, protects base image) +- Boots QEMU with GTK graphical display +- Provides SSH connection info +- Cleans up overlay when the GTK window is closed *When to run:* When you need to manually test or debug -*Usage:* -#+begin_src bash -# Clone base VM for debugging -./scripts/testing/debug-vm.sh - -# Use existing test disk -./scripts/testing/debug-vm.sh vm-images/archsetup-test-20251108-143000.qcow2 - -# Use base VM (read-only) -./scripts/testing/debug-vm.sh --base -#+end_src - *Connect via:* -- Console: =virsh console archsetup-debug-TIMESTAMP= -- SSH: =ssh root@IP_ADDRESS= (password: archsetup) -- VNC: =virt-viewer archsetup-debug-TIMESTAMP= +- GTK window (opens automatically) +- SSH: =sshpass -p 'archsetup' ssh -p 2222 root@localhost= ** cleanup-tests.sh *Purpose:* Clean up old test VMs and artifacts *What it does:* -- Lists all test VMs and destroys them -- Removes test disk images +- Stops running QEMU processes +- Kills orphaned QEMU processes +- Removes temporary disk images (overlays, test clones) - Keeps last N test results, deletes rest *When to run:* Periodically to free disk space -*Usage:* -#+begin_src bash -# Interactive cleanup -./scripts/testing/cleanup-tests.sh +** archsetup-test.conf -# Keep last 3 test results -./scripts/testing/cleanup-tests.sh --keep 3 +*Purpose:* Archangel config file for automated base VM creation -# Force without prompts -./scripts/testing/cleanup-tests.sh --force -#+end_src +Defines the base system: btrfs filesystem, no encryption, SSH enabled, root password "archsetup". Used by =create-base-vm.sh=. * Directory Structure @@ -213,27 +158,30 @@ archsetup/ │ └── testing/ │ ├── README.org # This file │ ├── setup-testing-env.sh # Setup infrastructure -│ ├── create-base-vm.sh # Create base VM +│ ├── create-base-vm.sh # Create base VM (automated) │ ├── run-test.sh # Run tests +│ ├── run-test-baremetal.sh # Bare-metal testing │ ├── debug-vm.sh # Interactive debugging │ ├── cleanup-tests.sh # Clean up -│ ├── finalize-base-vm.sh # Finalize base (generated) -│ ├── archinstall-config.json # Archinstall config +│ ├── archsetup-test.conf # Archangel config for test VMs │ └── lib/ │ ├── logging.sh # Logging utilities -│ └── vm-utils.sh # VM management +│ ├── vm-utils.sh # QEMU VM management +│ ├── validation.sh # Test validation checks +│ └── network-diagnostics.sh # Network pre-flight checks ├── vm-images/ # VM disk images (gitignored) -│ ├── archsetup-base.qcow2 # Golden image -│ ├── arch-latest.iso # Arch ISO -│ └── archsetup-test-*.qcow2 # Test VMs +│ ├── archsetup-base.qcow2 # Golden image with snapshot +│ ├── archzfs-*.iso # Archangel ISO +│ ├── OVMF_VARS.fd # UEFI variables (per-VM) +│ └── debug-overlay-*.qcow2 # Temp debug overlays ├── test-results/ # Test results (gitignored) -│ ├── TIMESTAMP/ -│ │ ├── test.log -│ │ ├── test-report.txt -│ │ └── archsetup-*.log -│ └── latest -> TIMESTAMP/ # Symlink to latest +│ └── TIMESTAMP/ +│ ├── test.log +│ ├── test-report.txt +│ └── archsetup-*.log └── docs/ - └── testing-strategy.org # Complete strategy doc + ├── testing-strategy.org # Complete strategy doc + └── archangel-vm-testing-guide.org # Archangel integration guide #+end_example * Configuration @@ -241,30 +189,27 @@ archsetup/ ** VM Specifications All test VMs use: -- *CPUs:* 2 vCPUs -- *RAM:* 4GB (matches archsetup tmpfs build directory) +- *CPUs:* 4 vCPUs +- *RAM:* 4GB - *Disk:* 50GB (thin provisioned qcow2) -- *Network:* NAT via libvirt default network -- *Boot:* UEFI (systemd-boot bootloader) -- *Display:* Serial console + VNC +- *Network:* User-mode networking with SSH port forwarding (localhost:2222) +- *Boot:* UEFI (via OVMF firmware) +- *Display:* Headless (automated) or GTK (debug) Set environment variables to customize: #+begin_src bash -VM_CPUS=4 VM_RAM=8192 ./scripts/testing/run-test.sh +VM_CPUS=8 VM_RAM=8192 ./scripts/testing/run-test.sh #+end_src ** Base VM Specifications -The base VM contains a minimal Arch installation: -- Base system packages -- Linux kernel and firmware -- OpenSSH server (for automation) -- dhcpcd (for networking) -- git, vim, sudo (essentials) +The base VM is created automatically by archangel with: +- Btrfs filesystem (no encryption) +- GRUB bootloader +- OpenSSH server (root login enabled) +- NetworkManager - Root password: "archsetup" -This matches the documented prerequisites for archsetup. - * Validation Checks Each test run performs these validation checks: @@ -274,62 +219,37 @@ Each test run performs these validation checks: 2. User 'cjennings' was created 3. Dotfiles are stowed (symlinks exist) 4. yay (AUR helper) is installed -5. DWM is built and installed +5. DWM or Hyprland is configured -** Additional (Future) +** Additional - All expected packages installed -- X11 can start - systemd services enabled - Firewall configured +- Developer tools present * Troubleshooting ** VM fails to start -Check if libvirtd is running: -#+begin_src bash -sudo systemctl status libvirtd -sudo systemctl start libvirtd -#+end_src - -** Cannot get VM IP address - -The VM may not have booted completely or networking failed: +Check if KVM is available and port 2222 is free: #+begin_src bash -# Check VM status -virsh domstate VM_NAME - -# Connect to console to debug -virsh console VM_NAME - -# Check if dhcpcd is running in VM -systemctl status dhcpcd +ls -l /dev/kvm +ss -tln | grep 2222 #+end_src ** SSH connection refused -Wait longer for VM to boot, or check if sshd is enabled: +Wait longer for VM to boot, or check serial log: #+begin_src bash -virsh console VM_NAME -# Inside VM: -systemctl status sshd -systemctl start sshd +cat vm-images/qemu-serial.log #+end_src ** KVM not available Check if virtualization is enabled in BIOS and KVM modules loaded: #+begin_src bash -# Check for /dev/kvm ls -l /dev/kvm - -# Load KVM module (Intel) -sudo modprobe kvm-intel - -# Or for AMD -sudo modprobe kvm-amd - -# Verify +sudo modprobe kvm-amd # or kvm-intel lsmod | grep kvm #+end_src @@ -338,160 +258,27 @@ lsmod | grep kvm Clean up old tests: #+begin_src bash ./scripts/testing/cleanup-tests.sh --force -#+end_src - -Check disk usage: -#+begin_src bash du -sh vm-images/ test-results/ #+end_src -** Base VM Installation Issues - -*** Firewall blocking VM network - -If the VM cannot reach the internet (100% packet loss when pinging), check the host firewall: - -#+begin_src bash -# Check UFW status on host -sudo ufw status - -# Check libvirt NAT rules on host -sudo iptables -t nat -L -n -v | grep -i libvirt -#+end_src - -*Solution:* Temporarily disable UFW during base VM creation: -#+begin_src bash -sudo ufw disable -# Create base VM -sudo ufw enable -#+end_src - -Or add libvirt rules to UFW: -#+begin_src bash -sudo ufw allow in on virbr0 -sudo ufw allow out on virbr0 -#+end_src - -*** VM network not working after boot - -If =dhcpcd= isn't running or network isn't configured: - -#+begin_src bash -# In the VM - restart network from scratch -killall dhcpcd - -# Bring interface down and up (replace enp1s0 with your interface) -ip link set enp1s0 down -ip link set enp1s0 up - -# Start dhcpcd -dhcpcd enp1s0 - -# Wait and verify -sleep 3 -ip addr show enp1s0 -ip route - -# Test connectivity -ping -c 3 192.168.122.1 # Gateway -ping -c 3 8.8.8.8 # Google DNS -#+end_src - -*** DNS not working (127.0.0.53 in resolv.conf) - -The Live ISO uses systemd-resolved stub resolver which may not work: - -#+begin_src bash -# In the VM - set real DNS servers -echo "nameserver 8.8.8.8" > /etc/resolv.conf -echo "nameserver 1.1.1.1" >> /etc/resolv.conf - -# Test -ping -c 3 archlinux.org -#+end_src - -*** Cannot paste into virt-viewer terminal - -Clipboard integration doesn't work well with virt-viewer. Use HTTP server instead: - -#+begin_src bash -# On host - serve the installation script -cd vm-images -python -m http.server 8000 - -# In VM - download and run -curl http://192.168.122.1:8000/auto-install.sh | bash - -# Or download first to review -curl http://192.168.122.1:8000/auto-install.sh -o install.sh -cat install.sh -bash install.sh -#+end_src - -*** Partitions "in use" error - -If re-running installation after a failed attempt: +** Port 2222 already in use +Another QEMU instance may be running: #+begin_src bash -# In the VM - unmount and wipe partitions -mount | grep vda -umount /mnt/boot 2>/dev/null -umount /mnt 2>/dev/null -umount -l /mnt # Lazy unmount if still busy - -# Wipe partition table completely -wipefs -a /dev/vda - -# Run install script again -bash install.sh -#+end_src - -*** Alternative: Use archinstall - -Instead of the auto-install.sh script, you can use Arch's built-in installer: - -#+begin_src bash -# In the VM -archinstall -#+end_src - -*Recommended settings:* -- Disk: =/dev/vda= -- Filesystem: =ext4= -- Bootloader: =systemd-boot= -- Hostname: =archsetup-test= -- Root password: =archsetup= -- Profile: =minimal= -- Additional packages: =openssh dhcpcd git vim sudo= -- Network: =NetworkManager= or =systemd-networkd= - -*After installation, before rebooting:* -#+begin_src bash -# Chroot into new system -arch-chroot /mnt - -# Enable services -systemctl enable sshd -systemctl enable dhcpcd # or NetworkManager - -# Allow root SSH login -sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config -sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config +# Find what's using the port +ss -tlnp | grep 2222 -# Exit and poweroff -exit -poweroff +# Kill orphaned QEMU processes +pkill -f "qemu-system.*archsetup-test" #+end_src * Future Enhancements ** Planned Improvements -- [ ] Fully automated base VM creation (expect-based console automation) -- [ ] Parallel test execution (multiple VMs) -- [ ] Screenshot capture of X11 desktop +- [ ] Parallel test execution (multiple VMs on different ports) +- [ ] Screenshot capture of X11/Wayland desktop - [ ] CI/CD integration (GitHub Actions / GitLab CI) - [ ] Performance benchmarking over time -- [ ] Cloud-init based base image (faster provisioning) ** Test Scenarios - [ ] Idempotency test (run archsetup twice) @@ -502,7 +289,7 @@ poweroff * References - [[file:../../docs/testing-strategy.org][Testing Strategy Document]] -- [[https://wiki.archlinux.org/title/Libvirt][Arch Wiki: libvirt]] +- [[file:../../docs/archangel-vm-testing-guide.org][Archangel VM Testing Guide]] - [[https://wiki.archlinux.org/title/QEMU][Arch Wiki: QEMU]] - [[file:../../archsetup][Main archsetup script]] -- [[file:../../TODO.org][Project TODO]] +- [[file:../../todo.org][Project TODO]] diff --git a/scripts/testing/archinstall-config.json b/scripts/testing/archinstall-config.json deleted file mode 100644 index a55e2a1..0000000 --- a/scripts/testing/archinstall-config.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "!users": { - "0": { - "!password": "archsetup", - "username": "root", - "sudo": false - } - }, - "archinstall-language": "English", - "audio_config": null, - "bootloader": "systemd-bootctl", - "config_version": "2.8.0", - "debug": false, - "disk_config": { - "config_type": "default_layout", - "device_modifications": [ - { - "device": "/dev/vda", - "partitions": [ - { - "btrfs": [], - "flags": [ - "Boot" - ], - "fs_type": "fat32", - "length": { - "sector_size": null, - "total_size": null, - "unit": "MiB", - "value": 512 - }, - "mount_options": [], - "mountpoint": "/boot", - "obj_id": "boot_partition", - "start": { - "sector_size": null, - "total_size": null, - "unit": "MiB", - "value": 1 - }, - "status": "create", - "type": "primary" - }, - { - "btrfs": [], - "flags": [], - "fs_type": "ext4", - "length": { - "sector_size": null, - "total_size": null, - "unit": "MiB", - "value": 100 - }, - "mount_options": [], - "mountpoint": "/", - "obj_id": "root_partition", - "start": { - "sector_size": null, - "total_size": null, - "unit": "MiB", - "value": 513 - }, - "status": "create", - "type": "primary" - } - ], - "wipe": true - } - ] - }, - "disk_encryption": null, - "hostname": "archsetup-test", - "kernels": [ - "linux" - ], - "locale_config": { - "kb_layout": "us", - "sys_enc": "UTF-8", - "sys_lang": "en_US" - }, - "mirror_config": { - "custom_mirrors": [], - "mirror_regions": { - "United States": [ - "https://mirror.rackspace.com/archlinux/$repo/os/$arch", - "https://mirror.leaseweb.com/archlinux/$repo/os/$arch" - ] - } - }, - "network_config": { - "type": "nm" - }, - "no_pkg_lookups": false, - "ntp": true, - "offline": false, - "packages": [ - "openssh", - "dhcpcd", - "git", - "vim" - ], - "parallel downloads": 5, - "profile_config": { - "gfx_driver": "All open-source", - "greeter": null, - "profile": { - "custom_settings": {}, - "details": [], - "main": "Minimal" - } - }, - "script": "guided", - "silent": false, - "swap": false, - "timezone": "America/Chicago", - "version": "2.8.0" -} diff --git a/scripts/testing/archsetup-test.conf b/scripts/testing/archsetup-test.conf new file mode 100644 index 0000000..d6a8d69 --- /dev/null +++ b/scripts/testing/archsetup-test.conf @@ -0,0 +1,14 @@ +# archsetup-test.conf - Archangel config for archsetup test VMs +# Used by create-base-vm.sh for fully automated base VM creation +# +# Usage: archangel --config-file /root/archsetup-test.conf + +FILESYSTEM=btrfs +HOSTNAME=archsetup-test +TIMEZONE=America/Chicago +LOCALE=en_US.UTF-8 +KEYMAP=us +DISKS=/dev/vda +NO_ENCRYPT=yes +ROOT_PASSWORD=archsetup +ENABLE_SSH=yes diff --git a/scripts/testing/archsetup-vm.conf b/scripts/testing/archsetup-vm.conf new file mode 100644 index 0000000..4117278 --- /dev/null +++ b/scripts/testing/archsetup-vm.conf @@ -0,0 +1,11 @@ +# archsetup-vm.conf - Config for running archsetup in test VMs +# Used by run-test.sh for unattended archsetup execution +# +# Usage: ./archsetup --config-file /tmp/archsetup-test/archsetup-vm.conf + +USERNAME=cjennings +PASSWORD=archsetup +LOCALE=en_US.UTF-8 +DESKTOP_ENV=hyprland +NO_GPU_DRIVERS=yes +AUTOLOGIN=yes diff --git a/scripts/testing/cleanup-tests.sh b/scripts/testing/cleanup-tests.sh index e4289a7..fd2f8de 100755 --- a/scripts/testing/cleanup-tests.sh +++ b/scripts/testing/cleanup-tests.sh @@ -36,85 +36,86 @@ while [[ $# -gt 0 ]]; do esac done -# Initialize logging +# Initialize logging and VM paths LOGFILE="/tmp/cleanup-tests-$(date +'%Y%m%d-%H%M%S').log" init_logging "$LOGFILE" +init_vm_paths "$PROJECT_ROOT/vm-images" section "Cleaning Up Test Artifacts" -# Find all test VMs -step "Finding test VMs" -TEST_VMS=$(virsh --connect qemu:///system list --all | grep "archsetup-test-" | awk '{print $2}' || true) +# Find and stop running QEMU processes +step "Checking for running QEMU processes" -if [ -z "$TEST_VMS" ]; then - info "No test VMs found" -else - VM_COUNT=$(echo "$TEST_VMS" | wc -l) - info "Found $VM_COUNT test VM(s)" - - if ! $FORCE; then - echo "" - echo "$TEST_VMS" - echo "" - read -p "Destroy these VMs? [y/N] " -n 1 -r +if vm_is_running; then + info "Found running QEMU test VM (PID: $(cat "$PID_FILE"))" + if $FORCE; then + stop_qemu + else + read -p "Stop running VM? [y/N] " -n 1 -r echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - info "Skipping VM cleanup" + if [[ $REPLY =~ ^[Yy]$ ]]; then + stop_qemu else - for vm in $TEST_VMS; do - step "Destroying VM: $vm" - if vm_is_running "$vm"; then - virsh --connect qemu:///system destroy "$vm" >> "$LOGFILE" 2>&1 - fi - virsh --connect qemu:///system undefine "$vm" --nvram >> "$LOGFILE" 2>&1 || true - success "VM destroyed: $vm" - done + info "Skipping VM shutdown" fi + fi +else + info "No running VM found" +fi + +# Check for orphaned QEMU processes +QEMU_PIDS=$(pgrep -f "qemu-system.*archsetup-test" 2>/dev/null || true) +if [ -n "$QEMU_PIDS" ]; then + info "Found orphaned QEMU processes: $QEMU_PIDS" + if $FORCE; then + echo "$QEMU_PIDS" | xargs kill -9 2>/dev/null || true + success "Orphaned processes killed" else - for vm in $TEST_VMS; do - step "Destroying VM: $vm" - if vm_is_running "$vm"; then - virsh --connect qemu:///system destroy "$vm" >> "$LOGFILE" 2>&1 - fi - virsh --connect qemu:///system undefine "$vm" --nvram >> "$LOGFILE" 2>&1 || true - success "VM destroyed: $vm" - done + read -p "Kill orphaned QEMU processes? [y/N] " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "$QEMU_PIDS" | xargs kill -9 2>/dev/null || true + success "Orphaned processes killed" + fi fi fi -# Clean up test disk images +# Clean up QEMU runtime files +rm -f "$PID_FILE" "$MONITOR_SOCK" + +# Clean up debug overlay and test disk images section "Cleaning Up Disk Images" -step "Finding test disk images" +step "Finding temporary disk images" if [ -d "$PROJECT_ROOT/vm-images" ]; then - TEST_DISKS=$(find "$PROJECT_ROOT/vm-images" -name "archsetup-test-*.qcow2" 2>/dev/null || true) + TEMP_DISKS=$(find "$PROJECT_ROOT/vm-images" -name "debug-overlay-*.qcow2" -o -name "archsetup-test-*.qcow2" 2>/dev/null || true) - if [ -z "$TEST_DISKS" ]; then - info "No test disk images found" + if [ -z "$TEMP_DISKS" ]; then + info "No temporary disk images found" else - DISK_COUNT=$(echo "$TEST_DISKS" | wc -l) - DISK_SIZE=$(du -ch $TEST_DISKS | tail -1 | awk '{print $1}') - info "Found $DISK_COUNT test disk image(s) totaling $DISK_SIZE" + DISK_COUNT=$(echo "$TEMP_DISKS" | wc -l) + DISK_SIZE=$(du -ch $TEMP_DISKS 2>/dev/null | tail -1 | awk '{print $1}') + info "Found $DISK_COUNT temporary disk image(s) totaling $DISK_SIZE" - if ! $FORCE; then + if $FORCE; then + echo "$TEMP_DISKS" | while read disk; do + rm -f "$disk" + done + success "Temporary disk images deleted" + else echo "" - echo "$TEST_DISKS" + echo "$TEMP_DISKS" echo "" read -p "Delete these disk images? [y/N] " -n 1 -r echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - info "Skipping disk cleanup" - else - echo "$TEST_DISKS" | while read disk; do + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "$TEMP_DISKS" | while read disk; do rm -f "$disk" done - success "Test disk images deleted" + success "Temporary disk images deleted" + else + info "Skipping disk cleanup" fi - else - echo "$TEST_DISKS" | while read disk; do - rm -f "$disk" - done - success "Test disk images deleted" fi fi fi @@ -141,26 +142,26 @@ else DELETE_COUNT=$(echo "$TO_DELETE" | wc -l) info "Keeping last $KEEP_LAST, deleting $DELETE_COUNT old result(s)" - if ! $FORCE; then + if $FORCE; then + echo "$TO_DELETE" | while read dir; do + rm -rf "$dir" + done + success "Old test results deleted" + else echo "" echo "Will delete:" echo "$TO_DELETE" echo "" read -p "Delete these test results? [y/N] " -n 1 -r echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - info "Skipping results cleanup" - else + if [[ $REPLY =~ ^[Yy]$ ]]; then echo "$TO_DELETE" | while read dir; do rm -rf "$dir" done success "Old test results deleted" + else + info "Skipping results cleanup" fi - else - echo "$TO_DELETE" | while read dir; do - rm -rf "$dir" - done - success "Old test results deleted" fi fi fi diff --git a/scripts/testing/create-base-vm.sh b/scripts/testing/create-base-vm.sh index 03409fe..7979bd2 100755 --- a/scripts/testing/create-base-vm.sh +++ b/scripts/testing/create-base-vm.sh @@ -1,10 +1,11 @@ #!/bin/bash -# Create base VM for archsetup testing - Manual Installation +# Create base VM for archsetup testing - Automated via Archangel ISO # Author: Craig Jennings <craigmartinjennings@gmail.com> # License: GNU GPLv3 # -# This script creates a VM booted from Arch ISO, then waits for you to -# manually install Arch using archinstall. +# This script boots an archangel ISO in QEMU, copies a config file into the +# live environment via SSH, and runs a fully unattended Arch Linux installation. +# The result is a base VM disk with a "clean-install" snapshot ready for testing. set -e @@ -17,157 +18,139 @@ source "$SCRIPT_DIR/lib/logging.sh" source "$SCRIPT_DIR/lib/vm-utils.sh" # Configuration -VM_NAME="archsetup-base" -VM_CPUS="${VM_CPUS:-4}" -VM_RAM="${VM_RAM:-8192}" # MB -VM_DISK="${VM_DISK:-50}" # GB VM_IMAGES_DIR="$PROJECT_ROOT/vm-images" -ISO_URL="https://mirrors.kernel.org/archlinux/iso/latest/archlinux-x86_64.iso" -ISO_PATH="$VM_IMAGES_DIR/arch-latest.iso" -DISK_PATH="$VM_IMAGES_DIR/archsetup-base.qcow2" +CONFIG_FILE="$SCRIPT_DIR/archsetup-test.conf" +LIVE_ISO_PASSWORD="archzfs" +SNAPSHOT_NAME="clean-install" # Initialize logging +mkdir -p "$PROJECT_ROOT/test-results" LOGFILE="$PROJECT_ROOT/test-results/create-base-vm-$(date +'%Y%m%d-%H%M%S').log" init_logging "$LOGFILE" +init_vm_paths "$VM_IMAGES_DIR" section "Creating Base VM for ArchSetup Testing" -# Verify prerequisites +# ─── Prerequisites ──────────────────────────────────────────────────── + step "Checking prerequisites" -check_libvirt || fatal "libvirt not running" -check_libvirt_group || fatal "User not in libvirt group" -check_kvm || fatal "KVM not available" +check_prerequisites || fatal "Missing prerequisites" success "Prerequisites satisfied" -# Create vm-images directory -mkdir -p "$VM_IMAGES_DIR" +# Verify config file exists +if [ ! -f "$CONFIG_FILE" ]; then + fatal "Config file not found: $CONFIG_FILE" +fi + +# Find archangel ISO in vm-images/ +ISO_PATH=$(find "$VM_IMAGES_DIR" -maxdepth 1 -name "archzfs-*.iso" -type f 2>/dev/null | sort -V | tail -1) +if [ -z "$ISO_PATH" ]; then + fatal "No archangel ISO found in $VM_IMAGES_DIR/" + info "Copy an archzfs-*.iso file to: $VM_IMAGES_DIR/" +fi +info "Using ISO: $(basename "$ISO_PATH")" -# Download Arch ISO if needed -section "Preparing Arch Linux ISO" +# ─── Prepare Disk ───────────────────────────────────────────────────── -if [ -f "$ISO_PATH" ]; then - info "Arch ISO exists: $ISO_PATH" +section "Preparing VM Disk" - # Check if ISO is older than 30 days - if [ $(find "$ISO_PATH" -mtime +30 | wc -l) -gt 0 ]; then - warn "ISO is older than 30 days" - info "Downloading latest version..." - rm -f "$ISO_PATH" - else - success "Using existing ISO" - fi +# Remove old disk and OVMF vars if they exist +if [ -f "$DISK_PATH" ]; then + warn "Removing existing disk: $DISK_PATH" + rm -f "$DISK_PATH" fi +rm -f "$OVMF_VARS" + +# Create fresh disk +step "Creating ${VM_DISK_SIZE}G qcow2 disk" +if qemu-img create -f qcow2 "$DISK_PATH" "${VM_DISK_SIZE}G" >> "$LOGFILE" 2>&1; then + success "Disk created: $DISK_PATH" +else + fatal "Failed to create disk image" +fi + +# ─── Phase 1: Install from ISO ─────────────────────────────────────── + +section "Phase 1: Archangel Installation" -if [ ! -f "$ISO_PATH" ]; then - step "Downloading latest Arch ISO" - info "URL: $ISO_URL" - info "This may take several minutes..." +start_timer "install" - if wget --progress=dot:giga -O "$ISO_PATH" "$ISO_URL" 2>&1 | tee -a "$LOGFILE"; then - success "ISO downloaded" - else - fatal "ISO download failed" - fi +# Boot from ISO +start_qemu "$DISK_PATH" "iso" "$ISO_PATH" "none" || fatal "Failed to start QEMU" + +# Wait for live ISO SSH +wait_for_ssh "$LIVE_ISO_PASSWORD" 120 || fatal "Live ISO SSH not available" + +# Copy config file into VM +copy_to_vm "$CONFIG_FILE" "/root/archsetup-test.conf" "$LIVE_ISO_PASSWORD" || \ + fatal "Failed to copy config to VM" + +# Run archangel installer (synchronous - typically 5-10 minutes) +step "Running archangel installer (unattended)..." +info "This will partition, install, and configure the base system" + +if vm_exec "$LIVE_ISO_PASSWORD" "archangel --config-file /root/archsetup-test.conf"; then + success "Archangel installation completed" +else + error "Archangel installation failed" + step "Capturing serial log for debugging" + info "Serial log: $SERIAL_LOG" + fatal "Base VM installation failed - check logs" fi -# Remove existing VM and disk -if vm_exists "$VM_NAME"; then - warn "VM $VM_NAME already exists - destroying it" - if vm_is_running "$VM_NAME"; then - virsh destroy "$VM_NAME" >> "$LOGFILE" 2>&1 - fi - virsh undefine "$VM_NAME" --nvram >> "$LOGFILE" 2>&1 || true +# Power off +stop_qemu +stop_timer "install" + +# ─── Phase 2: Verify Installation ──────────────────────────────────── + +section "Phase 2: Verifying Installation" + +start_timer "verify" + +# Boot from installed disk +start_qemu "$DISK_PATH" "disk" "" "none" || fatal "Failed to boot installed system" + +# Wait for SSH on installed system +wait_for_ssh "$ROOT_PASSWORD" 120 || fatal "Installed system SSH not available" + +# Basic verification +step "Verifying base system" + +KERNEL=$(vm_exec "$ROOT_PASSWORD" "uname -r" 2>/dev/null) +success "Kernel: $KERNEL" + +if vm_exec "$ROOT_PASSWORD" "systemctl is-active sshd" &>/dev/null; then + success "SSH service active" +else + warn "SSH service not active" fi -[ -f "$DISK_PATH" ] && rm -f "$DISK_PATH" - -# Create and start VM -section "Creating and Starting VM" - -info "Creating VM: $VM_NAME" -info " CPUs: $VM_CPUS | RAM: ${VM_RAM}MB | Disk: ${VM_DISK}GB" - -virt-install \ - --connect qemu:///system \ - --name "$VM_NAME" \ - --memory "$VM_RAM" \ - --vcpus "$VM_CPUS" \ - --disk path="$DISK_PATH",size="$VM_DISK",format=qcow2,bus=virtio \ - --cdrom "$ISO_PATH" \ - --os-variant archlinux \ - --network network=default,model=virtio \ - --graphics vnc,listen=127.0.0.1 \ - --console pty,target.type=serial \ - --boot uefi \ - --noreboot \ - --check path_in_use=off \ - --filesystem type=mount,mode=mapped,source="$PROJECT_ROOT/scripts",target=host-scripts \ - >> "$LOGFILE" 2>&1 & - -VIRT_INSTALL_PID=$! - -progress "Waiting for VM to boot from ISO" -sleep 30 - -# Check if VM started -if ! vm_is_running "$VM_NAME"; then - wait $VIRT_INSTALL_PID - EXIT_CODE=$? - fatal "VM failed to start (exit code: $EXIT_CODE)" +if vm_exec "$ROOT_PASSWORD" "systemctl is-active NetworkManager" &>/dev/null; then + success "NetworkManager active" +else + warn "NetworkManager not active" fi -success "VM started successfully" - -# Display manual installation instructions -section "Manual Installation Required" - -cat << 'EOF' - -[i] -[i] Base VM is running from Arch ISO -[i] -[i] NEXT STEPS - Complete installation manually: -[i] -[i] 1. Open virt-viewer (should already be open): -[i] virt-viewer --connect qemu:///system archsetup-base -[i] -[i] 2. Login as 'root' (no password) -[i] -[i] 3. Run: archinstall -[i] -[i] 4. Configure with these settings: -[i] - Hostname: archsetup-test -[i] - Root password: archsetup -[i] - Profile: minimal -[i] - Network: dhcpcd (or NetworkManager) -[i] - Additional packages: openssh git vim sudo iperf3 mtr traceroute bind net-tools sshfs -[i] - Enable: sshd, dhcpcd (or NetworkManager) -[i] -[i] 5. After archinstall completes: -[i] - Chroot into /mnt: arch-chroot /mnt -[i] - Edit /etc/ssh/sshd_config: -[i] sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config -[i] sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config -[i] - Set up shared folder mount (9p filesystem): -[i] mkdir -p /mnt/host-scripts -[i] echo 'host-scripts /mnt/host-scripts 9p trans=virtio,version=9p2000.L,rw 0 0' >> /etc/fstab -[i] - Exit chroot: exit -[i] - Poweroff: poweroff -[i] -[i] 6. After VM powers off, run: -[i] ./scripts/testing/finalize-base-vm.sh -[i] -[i] Log file: $LOGFILE -[i] - -EOF - -info "Waiting for VM to power off..." -info "(This script will exit when you manually power off the VM)" - -# Wait for virt-install to finish (VM powers off) -wait $VIRT_INSTALL_PID || true - -success "VM has powered off" +# Power off for snapshot +stop_qemu +stop_timer "verify" + +# ─── Phase 3: Create Snapshot ──────────────────────────────────────── + +section "Phase 3: Creating Clean-Install Snapshot" + +create_snapshot "$DISK_PATH" "$SNAPSHOT_NAME" || fatal "Failed to create snapshot" + +# ─── Done ───────────────────────────────────────────────────────────── + +section "Base VM Created Successfully" + +info "" +info " Disk: $DISK_PATH" +info " Snapshot: $SNAPSHOT_NAME" +info " Config: $(basename "$CONFIG_FILE")" +info " Log: $LOGFILE" +info "" +info "Next step: Run ./scripts/testing/run-test.sh" info "" -info "Next step: Run ./scripts/testing/finalize-base-vm.sh" diff --git a/scripts/testing/debug-vm.sh b/scripts/testing/debug-vm.sh index a442850..5b2b197 100755 --- a/scripts/testing/debug-vm.sh +++ b/scripts/testing/debug-vm.sh @@ -2,6 +2,9 @@ # Launch VM for interactive debugging # Author: Craig Jennings <craigmartinjennings@gmail.com> # License: GNU GPLv3 +# +# Launches a QEMU VM with a graphical display for interactive debugging. +# Uses a copy-on-write overlay when using --base to protect the base image. set -e @@ -27,21 +30,23 @@ else echo "Usage: $0 [disk-image.qcow2 | --base]" echo "" echo "Options:" - echo " --base Use base VM (read-only, safe for testing)" - echo " disk-image.qcow2 Use existing test disk image" - echo " (no args) Clone base VM for debugging" + echo " --base Use base VM via copy-on-write overlay (safe)" + echo " disk-image.qcow2 Use existing test disk image directly" + echo " (no args) Same as --base" exit 1 fi # Configuration TIMESTAMP=$(date +'%Y%m%d-%H%M%S') -DEBUG_VM_NAME="archsetup-debug-$TIMESTAMP" -BASE_DISK="$PROJECT_ROOT/vm-images/archsetup-base.qcow2" +VM_IMAGES_DIR="$PROJECT_ROOT/vm-images" +BASE_DISK="$VM_IMAGES_DIR/archsetup-base.qcow2" ROOT_PASSWORD="archsetup" +OVERLAY_DISK="" -# Initialize logging +# Initialize logging and VM paths LOGFILE="/tmp/debug-vm-$TIMESTAMP.log" init_logging "$LOGFILE" +init_vm_paths "$VM_IMAGES_DIR" section "Launching Debug VM" @@ -50,84 +55,55 @@ if $USE_BASE; then if [ ! -f "$BASE_DISK" ]; then fatal "Base disk not found: $BASE_DISK" fi - VM_DISK="$BASE_DISK" - info "Using base VM (read-only snapshot mode)" -else - if [ -z "$VM_DISK" ]; then - # Clone base VM - VM_DISK="$PROJECT_ROOT/vm-images/$DEBUG_VM_NAME.qcow2" - step "Cloning base VM for debugging" - clone_disk "$BASE_DISK" "$VM_DISK" || fatal "Failed to clone base VM" - success "Debug disk created: $VM_DISK" + + # Create a copy-on-write overlay (instant, protects base image) + OVERLAY_DISK="$VM_IMAGES_DIR/debug-overlay-$TIMESTAMP.qcow2" + step "Creating copy-on-write overlay" + if qemu-img create -f qcow2 -b "$BASE_DISK" -F qcow2 "$OVERLAY_DISK" >> "$LOGFILE" 2>&1; then + success "Overlay created: $(basename "$OVERLAY_DISK")" else - info "Using existing disk: $VM_DISK" + fatal "Failed to create overlay disk" fi + VM_DISK="$OVERLAY_DISK" +else + info "Using existing disk: $VM_DISK" fi -# Create debug VM -step "Creating debug VM: $DEBUG_VM_NAME" -virt-install \ - --connect qemu:///system \ - --name "$DEBUG_VM_NAME" \ - --memory 4096 \ - --vcpus 2 \ - --disk path="$VM_DISK",format=qcow2,bus=virtio \ - --os-variant archlinux \ - --network network=default,model=virtio \ - --graphics vnc,listen=127.0.0.1 \ - --console pty,target.type=serial \ - --boot uefi \ - --import \ - --noautoconsole \ - >> "$LOGFILE" 2>&1 - -success "Debug VM created" - -# Wait for boot -step "Waiting for VM to boot..." -sleep 20 - -# Get VM IP -VM_IP=$(get_vm_ip "$DEBUG_VM_NAME" 2>/dev/null || true) +# If snapshot exists, restore it first (only for non-overlay disks) +if [ -z "$OVERLAY_DISK" ] && snapshot_exists "$VM_DISK" "clean-install"; then + step "Restoring clean-install snapshot" + restore_snapshot "$VM_DISK" "clean-install" +fi + +# Launch QEMU with graphical display +step "Starting QEMU with graphical display" +start_qemu "$VM_DISK" "disk" "" "gtk" || fatal "Failed to start QEMU" # Display connection information section "Debug VM Ready" info "" -info "VM Name: $DEBUG_VM_NAME" -if [ -n "$VM_IP" ]; then - info "IP Address: $VM_IP" -fi -info "Disk: $VM_DISK" +info " Disk: $(basename "$VM_DISK")" +info " SSH: sshpass -p '$ROOT_PASSWORD' ssh -p $SSH_PORT root@localhost" +info " Root password: $ROOT_PASSWORD" info "" -info "Connect via:" -info " Console: virsh console $DEBUG_VM_NAME" -if [ -n "$VM_IP" ]; then - info " SSH: ssh root@$VM_IP" -fi -info " VNC: virt-viewer $DEBUG_VM_NAME" -info "" -info "Root password: $ROOT_PASSWORD" -info "" -info "When done debugging:" -info " virsh destroy $DEBUG_VM_NAME" -info " virsh undefine $DEBUG_VM_NAME" -if [ ! "$VM_DISK" = "$BASE_DISK" ] && [ -z "$1" ]; then - info " rm $VM_DISK" -fi -info "" -info "Log file: $LOGFILE" +info " The GTK window should be open. Close it to stop the VM." +info " Log file: $LOGFILE" info "" -# Offer to connect to console -read -p "Connect to console now? [Y/n] " -n 1 -r -echo "" -if [[ $REPLY =~ ^[Nn]$ ]]; then - info "VM is running in background" - info "Connect later with: virsh console $DEBUG_VM_NAME" -else - info "Connecting to console..." - info "Press Ctrl+] to disconnect from console" +# Wait for QEMU to exit (user closes GTK window) +step "Waiting for VM to exit..." +while vm_is_running; do sleep 2 - virsh console "$DEBUG_VM_NAME" +done +success "VM has stopped" + +# Clean up overlay disk +if [ -n "$OVERLAY_DISK" ] && [ -f "$OVERLAY_DISK" ]; then + step "Removing overlay disk" + rm -f "$OVERLAY_DISK" + success "Overlay cleaned up" fi + +_cleanup_qemu_files +info "Debug session complete" diff --git a/scripts/testing/finalize-base-vm.sh b/scripts/testing/finalize-base-vm.sh deleted file mode 100755 index 225ffae..0000000 --- a/scripts/testing/finalize-base-vm.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# Finalize base VM after installation -VM_NAME="archsetup-base" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -BASE_DISK="$PROJECT_ROOT/vm-images/archsetup-base.qcow2" - -echo "[i] Removing ISO from VM..." -virsh --connect qemu:///system change-media $VM_NAME sda --eject 2>/dev/null || true -virsh --connect qemu:///system change-media $VM_NAME hda --eject 2>/dev/null || true -echo "[✓] ISO removed" - -echo "[i] Fixing base disk permissions..." -sudo chown $USER:$USER "$BASE_DISK" -sudo chmod 644 "$BASE_DISK" -echo "[✓] Permissions fixed" - -echo "[i] Starting VM from installed system..." -virsh --connect qemu:///system start $VM_NAME -echo "[i] Waiting for boot..." -sleep 30 -IP=$(virsh --connect qemu:///system domifaddr $VM_NAME 2>/dev/null | grep -oP '(\d+\.){3}\d+' | head -1) -echo "[✓] Base VM is ready!" -echo "" -echo "Connect via:" -echo " Console: virsh console $VM_NAME" -echo " SSH: ssh root@$IP" -echo " Password: archsetup" -echo "" -echo "To create a test clone:" -echo " ./scripts/testing/run-test.sh" diff --git a/scripts/testing/lib/network-diagnostics.sh b/scripts/testing/lib/network-diagnostics.sh index 3f9735b..f7cae11 100644 --- a/scripts/testing/lib/network-diagnostics.sh +++ b/scripts/testing/lib/network-diagnostics.sh @@ -3,27 +3,30 @@ # Author: Craig Jennings <craigmartinjennings@gmail.com> # License: GNU GPLv3 -# Note: logging.sh should already be sourced by the calling script +# Note: logging.sh and vm-utils.sh should already be sourced by the calling script +# Uses globals: ROOT_PASSWORD, SSH_PORT, SSH_OPTS, VM_IP (from vm-utils.sh or calling script) # Run quick network diagnostics -# Args: $1 = VM IP address or hostname run_network_diagnostics() { - local vm_host="$1" + local password="${ROOT_PASSWORD:-archsetup}" + local port="${SSH_PORT:-22}" + local host="${VM_IP:-localhost}" + local ssh_base="sshpass -p $password ssh $SSH_OPTS -p $port root@$host" section "Pre-flight Network Diagnostics" - # Test 1: Basic connectivity + # Test 1: Basic connectivity (use curl instead of ping - SLIRP may not handle ICMP) step "Testing internet connectivity" - if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "ping -c 3 8.8.8.8 >/dev/null 2>&1"; then + if $ssh_base "curl -s --connect-timeout 5 -o /dev/null http://archlinux.org" 2>/dev/null; then success "Internet connectivity OK" else error "No internet connectivity" return 1 fi - # Test 2: DNS resolution + # Test 2: DNS resolution (use getent which is always available, unlike nslookup/dig) step "Testing DNS resolution" - if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "nslookup archlinux.org >/dev/null 2>&1"; then + if $ssh_base "getent hosts archlinux.org >/dev/null 2>&1" 2>/dev/null; then success "DNS resolution OK" else error "DNS resolution failed" @@ -32,7 +35,7 @@ run_network_diagnostics() { # Test 3: Arch mirror accessibility step "Testing Arch mirror access" - if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "curl -s -I https://mirrors.kernel.org/archlinux/ | head -1 | grep -qE '(200|301)'"; then + if $ssh_base "curl -s -I https://mirrors.kernel.org/archlinux/ | head -1 | grep -qE '(200|301)'" 2>/dev/null; then success "Arch mirrors accessible" else error "Cannot reach Arch mirrors" @@ -41,7 +44,7 @@ run_network_diagnostics() { # Test 4: AUR accessibility step "Testing AUR access" - if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'"; then + if $ssh_base "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'" 2>/dev/null; then success "AUR accessible" else error "Cannot reach AUR" @@ -50,8 +53,7 @@ run_network_diagnostics() { # Show network info info "Network configuration:" - sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host \ - "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while read line; do + $ssh_base "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while read line; do info " $line" done diff --git a/scripts/testing/lib/validation.sh b/scripts/testing/lib/validation.sh index 8c4787e..3191c64 100644 --- a/scripts/testing/lib/validation.sh +++ b/scripts/testing/lib/validation.sh @@ -20,7 +20,7 @@ declare -a UNKNOWN_ISSUES # SSH helper (uses globals: VM_IP, ROOT_PASSWORD) ssh_cmd() { sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ConnectTimeout=10 "root@$VM_IP" "$@" 2>/dev/null + -o ConnectTimeout=10 -p "${SSH_PORT:-22}" "root@$VM_IP" "$@" 2>/dev/null } # Validation result helpers @@ -458,17 +458,12 @@ validate_hyprland_tools() { local missing="" # Check core Hyprland packages - for pkg in hyprland hypridle hyprlock waybar wofi swww grim slurp gammastep; do + for pkg in hyprland hypridle hyprlock waybar fuzzel swww grim slurp gammastep foot; do if ! ssh_cmd "pacman -Q $pkg &>/dev/null"; then missing="$missing $pkg" fi done - # st should still be installed (via XWayland) - if ! ssh_cmd "test -f /usr/local/bin/st"; then - missing="$missing st" - fi - if [ -z "$missing" ]; then validation_pass "All Hyprland tools installed" else @@ -483,7 +478,7 @@ validate_hyprland_config() { for config in ".config/hypr/hyprland.conf" ".config/hypr/hypridle.conf" \ ".config/hypr/hyprlock.conf" ".config/waybar/config" \ - ".config/wofi/config" ".config/gammastep/config.ini"; do + ".config/fuzzel/fuzzel.ini" ".config/gammastep/config.ini"; do if ! ssh_cmd "test -f /home/cjennings/$config"; then missing="$missing $config" fi diff --git a/scripts/testing/lib/vm-utils.sh b/scripts/testing/lib/vm-utils.sh index 81aec33..47bd391 100755 --- a/scripts/testing/lib/vm-utils.sh +++ b/scripts/testing/lib/vm-utils.sh @@ -1,39 +1,52 @@ #!/bin/bash -# VM management utilities for archsetup testing +# VM management utilities for archsetup testing (direct QEMU) # Author: Craig Jennings <craigmartinjennings@gmail.com> # License: GNU GPLv3 +# +# Manages QEMU VMs directly without libvirt. Uses user-mode networking +# with port forwarding for SSH access and qemu-img for snapshots. # Note: logging.sh should already be sourced by the calling script # VM configuration defaults VM_CPUS="${VM_CPUS:-4}" -VM_RAM="${VM_RAM:-8192}" # MB -VM_DISK="${VM_DISK:-50}" # GB -VM_NETWORK="${VM_NETWORK:-default}" -LIBVIRT_URI="qemu:///system" # Use system session, not user session - -# Check if libvirt is running -check_libvirt() { - if ! systemctl is-active --quiet libvirtd; then - error "libvirtd service is not running" - info "Start it with: sudo systemctl start libvirtd" - return 1 - fi - return 0 +VM_RAM="${VM_RAM:-4096}" # MB +VM_DISK_SIZE="${VM_DISK_SIZE:-50}" # GB + +# SSH configuration +SSH_PORT="${SSH_PORT:-2222}" +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10" +ROOT_PASSWORD="${ROOT_PASSWORD:-archsetup}" + +# OVMF firmware paths +OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd" +OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd" + +# VM runtime paths (set by init_vm_paths) +VM_IMAGES_DIR="" +DISK_PATH="" +OVMF_VARS="" +PID_FILE="" +MONITOR_SOCK="" +SERIAL_LOG="" + +# Initialize all VM paths from images directory +# Must be called before any other vm-utils function +init_vm_paths() { + local images_dir="${1:-$VM_IMAGES_DIR}" + [ -z "$images_dir" ] && fatal "VM_IMAGES_DIR not set" + + VM_IMAGES_DIR="$images_dir" + DISK_PATH="$VM_IMAGES_DIR/archsetup-base.qcow2" + OVMF_VARS="$VM_IMAGES_DIR/OVMF_VARS.fd" + PID_FILE="$VM_IMAGES_DIR/qemu.pid" + MONITOR_SOCK="$VM_IMAGES_DIR/qemu-monitor.sock" + SERIAL_LOG="$VM_IMAGES_DIR/qemu-serial.log" + mkdir -p "$VM_IMAGES_DIR" } -# Check if user is in libvirt group -check_libvirt_group() { - if ! groups | grep -q libvirt; then - warn "Current user is not in libvirt group" - info "Add yourself with: sudo usermod -a -G libvirt $USER" - info "Then log out and back in for changes to take effect" - return 1 - fi - return 0 -} +# ─── Prerequisite Checks ───────────────────────────────────────────── -# Check if KVM is available check_kvm() { if [ ! -e /dev/kvm ]; then error "KVM is not available" @@ -44,249 +57,311 @@ check_kvm() { return 0 } -# Wait for VM to boot (check for SSH or serial console) -wait_for_vm() { - local vm_name="$1" - local timeout="${2:-300}" # 5 minutes default - local elapsed=0 +check_qemu() { + if ! command -v qemu-system-x86_64 &>/dev/null; then + error "qemu-system-x86_64 not found" + info "Install with: sudo pacman -S qemu-full" + return 1 + fi + return 0 +} - progress "Waiting for VM $vm_name to boot..." +check_ovmf() { + if [ ! -f "$OVMF_CODE" ]; then + error "OVMF firmware not found: $OVMF_CODE" + info "Install with: sudo pacman -S edk2-ovmf" + return 1 + fi + return 0 +} - while [ $elapsed -lt $timeout ]; do - if virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null | grep -q "running"; then - sleep 5 - complete "VM $vm_name is running" - return 0 - fi - sleep 2 - elapsed=$((elapsed + 2)) - done +check_sshpass() { + if ! command -v sshpass &>/dev/null; then + error "sshpass not found" + info "Install with: sudo pacman -S sshpass" + return 1 + fi + return 0 +} - error "Timeout waiting for VM $vm_name to boot" - return 1 +check_socat() { + if ! command -v socat &>/dev/null; then + error "socat not found" + info "Install with: sudo pacman -S socat" + return 1 + fi + return 0 } -# Check if VM exists -vm_exists() { - local vm_name="$1" - virsh --connect "$LIBVIRT_URI" dominfo "$vm_name" &>/dev/null - return $? +check_prerequisites() { + local failed=0 + check_kvm || failed=1 + check_qemu || failed=1 + check_ovmf || failed=1 + check_sshpass || failed=1 + check_socat || failed=1 + return $failed } -# Check if VM is running +# ─── VM Lifecycle ───────────────────────────────────────────────────── + +# Check if a QEMU VM is running via PID file vm_is_running() { - local vm_name="$1" - [ "$(virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null)" = "running" ] - return $? -} + [ -f "$PID_FILE" ] || return 1 -# Start VM -start_vm() { - local vm_name="$1" + local pid + pid=$(cat "$PID_FILE" 2>/dev/null) || return 1 - if vm_is_running "$vm_name"; then - warn "VM $vm_name is already running" + if kill -0 "$pid" 2>/dev/null && grep -q "qemu" "/proc/$pid/cmdline" 2>/dev/null; then return 0 fi - step "Starting VM: $vm_name" - if virsh --connect "$LIBVIRT_URI" start "$vm_name" >> "$LOGFILE" 2>&1; then - success "VM $vm_name started" - return 0 - else - error "Failed to start VM $vm_name" + # Stale PID file + rm -f "$PID_FILE" + return 1 +} + +# Start a QEMU VM +# Args: $1 = disk path +# $2 = boot mode: "iso" or "disk" (default: disk) +# $3 = ISO path (required if mode=iso) +# $4 = display: "none" (headless) or "gtk" (graphical, default: none) +start_qemu() { + local disk="$1" + local mode="${2:-disk}" + local iso_path="${3:-}" + local display="${4:-none}" + + # Stop any existing instance + stop_qemu 2>/dev/null || true + + # Check port availability + if ss -tln 2>/dev/null | grep -q ":${SSH_PORT} "; then + error "Port $SSH_PORT is already in use" + info "Another QEMU instance or service may be running" return 1 fi -} -# Stop VM gracefully -stop_vm() { - local vm_name="$1" - local timeout="${2:-60}" + # Ensure OVMF_VARS exists + if [ ! -f "$OVMF_VARS" ]; then + cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS" + fi + + # Truncate serial log + : > "$SERIAL_LOG" + + # Build QEMU command + local qemu_cmd=( + qemu-system-x86_64 + -name "archsetup-test" + -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=$disk,format=qcow2,if=virtio" + -netdev "user,id=net0,hostfwd=tcp::${SSH_PORT}-:22" + -device "virtio-net-pci,netdev=net0" + -monitor "unix:$MONITOR_SOCK,server,nowait" + -pidfile "$PID_FILE" + -serial "file:$SERIAL_LOG" + -usb + -device usb-tablet + ) + + # Boot mode + if [ "$mode" = "iso" ]; then + [ -z "$iso_path" ] && fatal "ISO path required for iso boot mode" + qemu_cmd+=(-cdrom "$iso_path" -boot d) + else + qemu_cmd+=(-boot c) + fi - if ! vm_is_running "$vm_name"; then - info "VM $vm_name is not running" - return 0 + # Display mode + if [ "$display" = "gtk" ]; then + qemu_cmd+=(-device virtio-vga-gl -display "gtk,gl=on") + else + qemu_cmd+=(-display none) fi - step "Shutting down VM: $vm_name" - if virsh --connect "$LIBVIRT_URI" shutdown "$vm_name" >> "$LOGFILE" 2>&1; then - # Wait for graceful shutdown - local elapsed=0 - while [ $elapsed -lt $timeout ]; do - if ! vm_is_running "$vm_name"; then - success "VM $vm_name stopped gracefully" - return 0 - fi - sleep 2 - elapsed=$((elapsed + 2)) - done - - warn "VM $vm_name did not stop gracefully, forcing..." - virsh --connect "$LIBVIRT_URI" destroy "$vm_name" >> "$LOGFILE" 2>&1 + step "Starting QEMU (mode=$mode, display=$display)" + + # Launch in background + "${qemu_cmd[@]}" &>> "$LOGFILE" & + + # Wait for PID file to appear + local wait=0 + while [ ! -f "$PID_FILE" ] && [ $wait -lt 10 ]; do + sleep 1 + wait=$((wait + 1)) + done + + if ! vm_is_running; then + error "QEMU failed to start" + return 1 fi - success "VM $vm_name stopped" + success "QEMU started (PID: $(cat "$PID_FILE"))" return 0 } -# Destroy VM (force stop) -destroy_vm() { - local vm_name="$1" +# Stop VM gracefully via ACPI powerdown, fallback to kill +stop_qemu() { + local timeout="${1:-60}" - if ! vm_exists "$vm_name"; then - info "VM $vm_name does not exist" + if ! vm_is_running; then return 0 fi - step "Destroying VM: $vm_name" - if vm_is_running "$vm_name"; then - virsh --connect "$LIBVIRT_URI" destroy "$vm_name" >> "$LOGFILE" 2>&1 + step "Sending shutdown signal to VM" + + # Send ACPI powerdown via monitor socket + if [ -S "$MONITOR_SOCK" ]; then + echo "system_powerdown" | socat - "UNIX-CONNECT:$MONITOR_SOCK" >> "$LOGFILE" 2>&1 || true fi - virsh --connect "$LIBVIRT_URI" undefine "$vm_name" --nvram >> "$LOGFILE" 2>&1 - success "VM $vm_name destroyed" + # Wait for graceful shutdown + local elapsed=0 + while [ $elapsed -lt $timeout ]; do + if ! vm_is_running; then + success "VM stopped gracefully" + _cleanup_qemu_files + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + + # Force kill + warn "VM did not stop gracefully after ${timeout}s, force killing" + kill_qemu return 0 } -# Create snapshot +# Force kill VM immediately +kill_qemu() { + if [ -f "$PID_FILE" ]; then + local pid + pid=$(cat "$PID_FILE" 2>/dev/null) + if [ -n "$pid" ]; then + kill -9 "$pid" 2>/dev/null || true + fi + fi + _cleanup_qemu_files +} + +# Clean up runtime files +_cleanup_qemu_files() { + rm -f "$PID_FILE" "$MONITOR_SOCK" +} + +# ─── Snapshot Operations (qemu-img) ────────────────────────────────── +# All snapshot operations require the VM to be stopped. + create_snapshot() { - local vm_name="$1" - local snapshot_name="$2" + local disk="${1:-$DISK_PATH}" + local snapshot_name="${2:-clean-install}" + + if vm_is_running; then + error "Cannot create snapshot while VM is running" + return 1 + fi step "Creating snapshot: $snapshot_name" - if virsh --connect "$LIBVIRT_URI" snapshot-create-as "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then - success "Snapshot $snapshot_name created" + if qemu-img snapshot -c "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then + success "Snapshot '$snapshot_name' created" return 0 else - error "Failed to create snapshot $snapshot_name" + error "Failed to create snapshot '$snapshot_name'" return 1 fi } -# Restore snapshot restore_snapshot() { - local vm_name="$1" - local snapshot_name="$2" + local disk="${1:-$DISK_PATH}" + local snapshot_name="${2:-clean-install}" + + if vm_is_running; then + error "Cannot restore snapshot while VM is running" + return 1 + fi step "Restoring snapshot: $snapshot_name" - if virsh --connect "$LIBVIRT_URI" snapshot-revert "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then - success "Snapshot $snapshot_name restored" + if qemu-img snapshot -a "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then + success "Snapshot '$snapshot_name' restored" return 0 else - error "Failed to restore snapshot $snapshot_name" + error "Failed to restore snapshot '$snapshot_name'" return 1 fi } -# Delete snapshot delete_snapshot() { - local vm_name="$1" - local snapshot_name="$2" + local disk="${1:-$DISK_PATH}" + local snapshot_name="${2:-clean-install}" step "Deleting snapshot: $snapshot_name" - if virsh --connect "$LIBVIRT_URI" snapshot-delete "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then - success "Snapshot $snapshot_name deleted" + if qemu-img snapshot -d "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then + success "Snapshot '$snapshot_name' deleted" return 0 else - error "Failed to delete snapshot $snapshot_name" + error "Failed to delete snapshot '$snapshot_name'" return 1 fi } -# Clone disk image (copy-on-write) -clone_disk() { - local base_image="$1" - local new_image="$2" - - if [ ! -f "$base_image" ]; then - error "Base image not found: $base_image" - return 1 - fi - - step "Cloning disk image (full copy)" - if qemu-img convert -f qcow2 -O qcow2 "$base_image" "$new_image" >> "$LOGFILE" 2>&1; then - success "Disk cloned: $new_image" - else - error "Failed to clone disk" - return 1 - fi - - # Truncate machine-id so systemd generates a new one on boot (avoids DHCP conflicts) - step "Clearing machine-id for unique network identity" - if guestfish -a "$new_image" -i truncate /etc/machine-id >> "$LOGFILE" 2>&1; then - success "Machine-ID cleared (will regenerate on boot)" - return 0 - else - warn "Failed to clear machine-ID (guestfish failed)" - info "Network may conflict with base VM if both run simultaneously" - return 0 # Don't fail the whole operation - fi +list_snapshots() { + local disk="${1:-$DISK_PATH}" + qemu-img snapshot -l "$disk" 2>/dev/null } -# Get VM IP address (requires guest agent or DHCP lease) -get_vm_ip() { - local vm_name="$1" +snapshot_exists() { + local disk="${1:-$DISK_PATH}" + local snapshot_name="${2:-clean-install}" + qemu-img snapshot -l "$disk" 2>/dev/null | grep -q "$snapshot_name" +} - # Try guest agent first - local ip - ip=$(virsh --connect "$LIBVIRT_URI" domifaddr "$vm_name" 2>/dev/null | grep -oP '(\d+\.){3}\d+' | head -1) +# ─── SSH Operations ─────────────────────────────────────────────────── - if [ -n "$ip" ]; then - echo "$ip" - return 0 - fi - - # Fall back to DHCP leases - local mac - mac=$(virsh --connect "$LIBVIRT_URI" domiflist "$vm_name" | grep -oP '([0-9a-f]{2}:){5}[0-9a-f]{2}' | head -1) +# Wait for SSH to become available on localhost:$SSH_PORT +wait_for_ssh() { + local password="${1:-$ROOT_PASSWORD}" + local timeout="${2:-120}" + local elapsed=0 - if [ -n "$mac" ]; then - ip=$(grep "$mac" /var/lib/libvirt/dnsmasq/default.leases 2>/dev/null | awk '{print $3}') - if [ -n "$ip" ]; then - echo "$ip" + progress "Waiting for SSH on localhost:$SSH_PORT..." + while [ $elapsed -lt $timeout ]; do + if sshpass -p "$password" ssh $SSH_OPTS -p "$SSH_PORT" root@localhost true 2>/dev/null; then + success "SSH is available" return 0 fi - fi + sleep 5 + elapsed=$((elapsed + 5)) + done + error "SSH timeout after ${timeout}s" return 1 } # Execute command in VM via SSH vm_exec() { - local vm_name="$1" + local password="${1:-$ROOT_PASSWORD}" shift - local cmd="$*" - - local ip - ip=$(get_vm_ip "$vm_name") - - if [ -z "$ip" ]; then - error "Could not get IP address for VM $vm_name" - return 1 - fi - - ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$ip" "$cmd" 2>> "$LOGFILE" + sshpass -p "$password" ssh $SSH_OPTS \ + -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ + -p "$SSH_PORT" root@localhost "$@" 2>> "$LOGFILE" } # Copy file to VM copy_to_vm() { - local vm_name="$1" - local local_file="$2" - local remote_path="$3" - - local ip - ip=$(get_vm_ip "$vm_name") - - if [ -z "$ip" ]; then - error "Could not get IP address for VM $vm_name" - return 1 - fi + local local_file="$1" + local remote_path="$2" + local password="${3:-$ROOT_PASSWORD}" - step "Copying $local_file to VM" - if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "$local_file" "root@$ip:$remote_path" >> "$LOGFILE" 2>&1; then + step "Copying $(basename "$local_file") to VM:$remote_path" + if sshpass -p "$password" scp $SSH_OPTS -P "$SSH_PORT" \ + "$local_file" "root@localhost:$remote_path" >> "$LOGFILE" 2>&1; then success "File copied to VM" return 0 else @@ -297,21 +372,13 @@ copy_to_vm() { # Copy file from VM copy_from_vm() { - local vm_name="$1" - local remote_file="$2" - local local_path="$3" - - local ip - ip=$(get_vm_ip "$vm_name") - - if [ -z "$ip" ]; then - error "Could not get IP address for VM $vm_name" - return 1 - fi + local remote_file="$1" + local local_path="$2" + local password="${3:-$ROOT_PASSWORD}" step "Copying $remote_file from VM" - if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$ip:$remote_file" "$local_path" >> "$LOGFILE" 2>&1; then + if sshpass -p "$password" scp $SSH_OPTS -P "$SSH_PORT" \ + "root@localhost:$remote_file" "$local_path" >> "$LOGFILE" 2>&1; then success "File copied from VM" return 0 else diff --git a/scripts/testing/run-test.sh b/scripts/testing/run-test.sh index 4c41cc3..c65961d 100755 --- a/scripts/testing/run-test.sh +++ b/scripts/testing/run-test.sh @@ -5,7 +5,7 @@ # # This script: # 1. Reverts base VM to clean snapshot -# 2. Starts the base VM +# 2. Boots the VM via QEMU # 3. Transfers archsetup and dotfiles # 4. Executes archsetup in the VM # 5. Captures logs and validates results @@ -55,15 +55,20 @@ done # Configuration TIMESTAMP=$(date +'%Y%m%d-%H%M%S') -VM_NAME="archsetup-base" +VM_IMAGES_DIR="$PROJECT_ROOT/vm-images" TEST_RESULTS_DIR="$PROJECT_ROOT/test-results/$TIMESTAMP" ROOT_PASSWORD="archsetup" ARCHZFS_INBOX="$HOME/code/archzfs/inbox" +ARCHSETUP_VM_CONF="$SCRIPT_DIR/archsetup-vm.conf" -# Initialize logging +# Set VM_IP for validation.sh backward compatibility +VM_IP="localhost" + +# Initialize logging and VM paths mkdir -p "$TEST_RESULTS_DIR" LOGFILE="$TEST_RESULTS_DIR/test.log" init_logging "$LOGFILE" +init_vm_paths "$VM_IMAGES_DIR" section "ArchSetup Test Run: $TIMESTAMP" @@ -72,9 +77,9 @@ if [ ! -f "$ARCHSETUP_SCRIPT" ]; then fatal "ArchSetup script not found: $ARCHSETUP_SCRIPT" fi -# Check if VM exists -if ! vm_exists "$VM_NAME"; then - fatal "Base VM not found: $VM_NAME" +# Check disk exists +if [ ! -f "$DISK_PATH" ]; then + fatal "Base disk not found: $DISK_PATH" info "Create it first: ./scripts/testing/create-base-vm.sh" fi @@ -82,72 +87,34 @@ fi section "Preparing Test Environment" step "Checking for snapshot: $SNAPSHOT_NAME" -if ! virsh --connect "$LIBVIRT_URI" snapshot-list "$VM_NAME" --name 2>/dev/null | grep -q "^$SNAPSHOT_NAME$"; then - fatal "Snapshot '$SNAPSHOT_NAME' not found on VM $VM_NAME" +if ! snapshot_exists "$DISK_PATH" "$SNAPSHOT_NAME"; then + fatal "Snapshot '$SNAPSHOT_NAME' not found on $DISK_PATH" info "Available snapshots:" - virsh --connect "$LIBVIRT_URI" snapshot-list "$VM_NAME" 2>/dev/null || info " (none)" + list_snapshots "$DISK_PATH" info "" - info "Create snapshot with:" - info " virsh snapshot-create-as $VM_NAME $SNAPSHOT_NAME --description 'Clean Arch install'" + info "Create base VM with: ./scripts/testing/create-base-vm.sh" fi success "Snapshot $SNAPSHOT_NAME exists" -# Shut down VM if running -if vm_is_running "$VM_NAME"; then - warn "VM $VM_NAME is currently running - shutting down for snapshot revert" - stop_vm "$VM_NAME" -fi +# Stop VM if running and restore snapshot +stop_qemu 2>/dev/null || true -# Revert to clean snapshot step "Reverting to snapshot: $SNAPSHOT_NAME" -if restore_snapshot "$VM_NAME" "$SNAPSHOT_NAME"; then +if restore_snapshot "$DISK_PATH" "$SNAPSHOT_NAME"; then success "Reverted to clean state" else fatal "Failed to revert snapshot" fi -# Start VM +# Start VM and wait for SSH start_timer "boot" step "Starting VM and waiting for SSH..." -if ! start_vm "$VM_NAME"; then - fatal "Failed to start VM" -fi - -sleep 10 # Give VM time to boot - -# Get VM IP address -VM_IP="" -for i in {1..30}; do - VM_IP=$(get_vm_ip "$VM_NAME" 2>/dev/null || true) - if [ -n "$VM_IP" ]; then - break - fi - sleep 2 -done - -if [ -z "$VM_IP" ]; then - error "Could not get VM IP address" - info "VM may not have booted correctly" - fatal "VM boot failed" -fi - -success "VM is running at $VM_IP" +start_qemu "$DISK_PATH" "disk" "" "none" || fatal "Failed to start VM" +wait_for_ssh "$ROOT_PASSWORD" 120 || fatal "VM SSH not available" stop_timer "boot" -# Wait for SSH -step "Waiting for SSH to become available..." -for i in {1..60}; do - if sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ConnectTimeout=2 "root@$VM_IP" "echo connected" &>/dev/null; then - break - fi - sleep 2 -done - -success "SSH is available" - # Run network diagnostics -if ! run_network_diagnostics "$VM_IP"; then +if ! run_network_diagnostics; then fatal "Network diagnostics failed - aborting test" fi @@ -165,39 +132,59 @@ BUNDLE_FILE=$(mktemp) git bundle create "$BUNDLE_FILE" HEAD >> "$LOGFILE" 2>&1 # Transfer bundle and extract on VM -sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP" "rm -rf /tmp/archsetup-test && mkdir -p /tmp/archsetup-test" >> "$LOGFILE" 2>&1 +vm_exec "$ROOT_PASSWORD" "rm -rf /tmp/archsetup-test && mkdir -p /tmp/archsetup-test" >> "$LOGFILE" 2>&1 -sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "$BUNDLE_FILE" "root@$VM_IP:/tmp/archsetup.bundle" >> "$LOGFILE" 2>&1 +copy_to_vm "$BUNDLE_FILE" "/tmp/archsetup.bundle" "$ROOT_PASSWORD" # Clone from bundle on VM (simulates git clone) -sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP" "cd /tmp && git clone --depth 1 /tmp/archsetup.bundle archsetup-test && rm /tmp/archsetup.bundle" >> "$LOGFILE" 2>&1 +vm_exec "$ROOT_PASSWORD" \ + "cd /tmp && git clone --depth 1 /tmp/archsetup.bundle archsetup-test && rm /tmp/archsetup.bundle" \ + >> "$LOGFILE" 2>&1 rm -f "$BUNDLE_FILE" success "Repository cloned to VM (simulating git clone --depth 1)" +# Transfer archsetup VM config file +step "Copying archsetup VM config" +copy_to_vm "$ARCHSETUP_VM_CONF" "/tmp/archsetup-test/archsetup-vm.conf" "$ROOT_PASSWORD" + +# Pre-archsetup VM setup +section "Pre-ArchSetup VM Setup" + +step "Importing archzfs PGP key" +vm_exec "$ROOT_PASSWORD" \ + "curl -sL https://archzfs.com/archzfs.gpg | pacman-key --add - && pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76" \ + >> "$LOGFILE" 2>&1 && success "archzfs PGP key imported" || warn "Failed to import archzfs key (may already be present)" + +step "Syncing package databases" +vm_exec "$ROOT_PASSWORD" "pacman -Sy --noconfirm" >> "$LOGFILE" 2>&1 && \ + success "Package databases synced" || warn "Package database sync had issues" + # Execute archsetup section "Executing ArchSetup" start_timer "archsetup" step "Starting archsetup script in detached session on VM..." -info "This will take 30-60 minutes depending on network speed" info "Log file: $LOGFILE" -# Start archsetup in a detached session on the VM (resilient to SSH disconnections) +# Start archsetup fully detached. +# Use ssh -T -n to prevent PTY allocation and stdin forwarding, which allows +# the SSH session to close immediately after the remote shell exits. REMOTE_LOG="/tmp/archsetup-test/archsetup-output.log" -sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP" "cd /tmp/archsetup-test && nohup bash archsetup > $REMOTE_LOG 2>&1 & echo \$!" \ + +sshpass -p "$ROOT_PASSWORD" ssh -T -n $SSH_OPTS \ + -p "$SSH_PORT" root@localhost \ + "setsid bash -c 'cd /tmp/archsetup-test && bash archsetup --config-file /tmp/archsetup-test/archsetup-vm.conf > $REMOTE_LOG 2>&1' < /dev/null > /dev/null 2>&1 &" \ >> "$LOGFILE" 2>&1 -if [ $? -ne 0 ]; then - fatal "Failed to start archsetup on VM" +# Verify the process started +sleep 3 +if vm_exec "$ROOT_PASSWORD" "pgrep -f 'bash archsetup'" >> "$LOGFILE" 2>/dev/null; then + success "ArchSetup started in background on VM" +else + fatal "ArchSetup process not found after launch" fi -success "ArchSetup started in background on VM" - # Poll for completion step "Monitoring archsetup progress (polling every 30 seconds)..." POLL_COUNT=0 @@ -205,9 +192,7 @@ MAX_POLLS=180 # 90 minutes max (180 * 30 seconds) while [ $POLL_COUNT -lt $MAX_POLLS ]; do # Check if archsetup process is still running - # Use ps to avoid pgrep matching its own SSH command - if sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP" "ps aux | grep '[b]ash archsetup' > /dev/null" 2>/dev/null; then + if vm_exec "$ROOT_PASSWORD" "ps aux | grep '[b]ash archsetup' > /dev/null" 2>/dev/null; then # Still running, wait and continue sleep 30 POLL_COUNT=$((POLL_COUNT + 1)) @@ -229,8 +214,9 @@ if [ $POLL_COUNT -ge $MAX_POLLS ]; then else # Get exit code from the remote log step "Retrieving archsetup exit status..." - ARCHSETUP_EXIT_CODE=$(sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP" "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null && echo 0 || echo 1" 2>/dev/null) + ARCHSETUP_EXIT_CODE=$(vm_exec "$ROOT_PASSWORD" \ + "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null && echo 0 || echo 1" \ + 2>/dev/null) if [ "$ARCHSETUP_EXIT_CODE" = "0" ]; then success "ArchSetup completed successfully" @@ -241,8 +227,7 @@ fi # Copy the remote output log step "Retrieving archsetup output from VM..." -sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP:$REMOTE_LOG" "$TEST_RESULTS_DIR/archsetup-output.log" 2>> "$LOGFILE" || \ +copy_from_vm "$REMOTE_LOG" "$TEST_RESULTS_DIR/archsetup-output.log" "$ROOT_PASSWORD" || \ warn "Could not copy remote output log" # Append remote output to main test log @@ -256,15 +241,17 @@ stop_timer "archsetup" section "Capturing Test Artifacts" step "Copying archsetup log from VM" -sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP:/var/log/archsetup-*.log" "$TEST_RESULTS_DIR/" 2>> "$LOGFILE" || \ +copy_from_vm "/var/log/archsetup-*.log" "$TEST_RESULTS_DIR/" "$ROOT_PASSWORD" || \ warn "Could not copy archsetup log" step "Copying package lists from VM" -sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "root@$VM_IP:/root/.local/src/archsetup-*.txt" "$TEST_RESULTS_DIR/" 2>> "$LOGFILE" || \ +copy_from_vm "/var/log/archsetup-*-package-list.txt" "$TEST_RESULTS_DIR/" "$ROOT_PASSWORD" || \ warn "Could not copy package lists" +step "Copying installed packages list" +copy_from_vm "/var/log/archsetup-installed-packages.txt" "$TEST_RESULTS_DIR/" "$ROOT_PASSWORD" || \ + warn "Could not copy installed packages list" + # Capture post-install state capture_post_install_state "$TEST_RESULTS_DIR" @@ -282,9 +269,9 @@ generate_issue_report "$TEST_RESULTS_DIR" "$ARCHZFS_INBOX" # Set validation result based on failure count if [ $VALIDATION_FAILED -eq 0 ]; then - VALIDATION_PASSED=true + TEST_PASSED=true else - VALIDATION_PASSED=false + TEST_PASSED=false fi # Generate test report @@ -298,16 +285,16 @@ ArchSetup Test Report Test ID: $TIMESTAMP Date: $(date +'%Y-%m-%d %H:%M:%S') -Test Method: Snapshot-based +Test Method: QEMU snapshot-based VM Configuration: - Name: $VM_NAME - IP: $VM_IP + Disk: $DISK_PATH Snapshot: $SNAPSHOT_NAME + SSH: localhost:$SSH_PORT Results: ArchSetup Exit Code: $ARCHSETUP_EXIT_CODE - Validation: $(if $VALIDATION_PASSED; then echo "PASSED"; else echo "FAILED"; fi) + Validation: $(if $TEST_PASSED; then echo "PASSED"; else echo "FAILED"; fi) Validation Summary: Passed: $VALIDATION_PASSED @@ -332,22 +319,24 @@ EOFREPORT info "Test report saved: $REPORT_FILE" +# Display report to terminal +echo "" +cat "$REPORT_FILE" + # Cleanup or keep VM section "Cleanup" if $KEEP_VM; then info "VM is still running in post-test state (--keep flag was used)" info "Connect with:" - info " Console: virsh console $VM_NAME" - info " SSH: ssh root@$VM_IP" + info " SSH: sshpass -p '$ROOT_PASSWORD' ssh -p $SSH_PORT root@localhost" info "" - info "To revert to clean state when done:" - info " virsh shutdown $VM_NAME" - info " virsh snapshot-revert $VM_NAME $SNAPSHOT_NAME" + info "To stop VM: kill \$(cat $PID_FILE)" + info "To revert: qemu-img snapshot -a $SNAPSHOT_NAME $DISK_PATH" else step "Shutting down VM and reverting to clean snapshot" - stop_vm "$VM_NAME" - if restore_snapshot "$VM_NAME" "$SNAPSHOT_NAME"; then + stop_qemu + if restore_snapshot "$DISK_PATH" "$SNAPSHOT_NAME"; then success "VM reverted to clean state" else warn "Failed to revert snapshot - VM may be in modified state" @@ -357,7 +346,7 @@ fi # Final summary section "Test Complete" -if [ $ARCHSETUP_EXIT_CODE -eq 0 ] && $VALIDATION_PASSED; then +if [ "$ARCHSETUP_EXIT_CODE" = "0" ] && $TEST_PASSED; then success "TEST PASSED" exit 0 else diff --git a/scripts/testing/setup-testing-env.sh b/scripts/testing/setup-testing-env.sh index e682553..f0e63aa 100755 --- a/scripts/testing/setup-testing-env.sh +++ b/scripts/testing/setup-testing-env.sh @@ -4,9 +4,7 @@ # License: GNU GPLv3 # # This script performs one-time setup of the testing infrastructure: -# - Installs QEMU/KVM and libvirt -# - Configures libvirt networking -# - Adds user to libvirt group +# - Installs QEMU/KVM, sshpass, OVMF firmware, and socat # - Verifies KVM support # - Creates directories for test artifacts @@ -20,6 +18,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" source "$SCRIPT_DIR/lib/logging.sh" # Initialize logging +mkdir -p "$PROJECT_ROOT/test-results" LOGFILE="$PROJECT_ROOT/test-results/setup-$(date +'%Y%m%d-%H%M%S').log" init_logging "$LOGFILE" @@ -41,13 +40,9 @@ section "Installing Required Packages" PACKAGES=( qemu-full - libvirt - virt-manager - dnsmasq - bridge-utils - iptables - virt-install - libguestfs + sshpass + edk2-ovmf + socat ) for pkg in "${PACKAGES[@]}"; do @@ -64,46 +59,6 @@ for pkg in "${PACKAGES[@]}"; do fi done -# Enable and start libvirt service -section "Configuring libvirt Service" - -step "Enabling libvirtd service" -if sudo systemctl enable libvirtd.service >> "$LOGFILE" 2>&1; then - success "libvirtd service enabled" -else - warn "Failed to enable libvirtd service (may already be enabled)" -fi - -step "Starting libvirtd service" -if sudo systemctl start libvirtd.service >> "$LOGFILE" 2>&1; then - success "libvirtd service started" -else - if sudo systemctl is-active --quiet libvirtd.service; then - info "libvirtd service is already running" - else - error "Failed to start libvirtd service" - fatal "Service startup failed" - fi -fi - -# Add user to libvirt group -section "Configuring User Permissions" - -if groups | grep -q libvirt; then - success "User $USER is already in libvirt group" -else - step "Adding user $USER to libvirt group" - if sudo usermod -a -G libvirt "$USER" >> "$LOGFILE" 2>&1; then - success "User added to libvirt group" - warn "You must log out and back in for group membership to take effect" - warn "After logging back in, re-run this script to continue" - exit 0 - else - error "Failed to add user to libvirt group" - fatal "User configuration failed" - fi -fi - # Verify KVM support section "Verifying KVM Support" @@ -126,6 +81,18 @@ else info "Load with: sudo modprobe kvm-intel (or kvm-amd)" fi +# Verify OVMF firmware +section "Verifying OVMF Firmware" + +OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd" +OVMF_VARS="/usr/share/edk2/x64/OVMF_VARS.4m.fd" + +if [ -f "$OVMF_CODE" ] && [ -f "$OVMF_VARS" ]; then + success "OVMF firmware files present" +else + fatal "OVMF firmware files not found (expected at $OVMF_CODE)" +fi + # Create directory structure section "Creating Directory Structure" @@ -147,45 +114,14 @@ for dir in "${DIRS[@]}"; do fi done -# Configure default libvirt network -section "Configuring libvirt Network" - -if virsh net-info default &>/dev/null; then - info "Default network exists" - - if virsh net-info default | grep -q "Active:.*yes"; then - success "Default network is active" - else - step "Starting default network" - if virsh net-start default >> "$LOGFILE" 2>&1; then - success "Default network started" - else - error "Failed to start default network" - fi - fi - - if virsh net-info default | grep -q "Autostart:.*yes"; then - info "Default network autostart is enabled" - else - step "Enabling default network autostart" - if virsh net-autostart default >> "$LOGFILE" 2>&1; then - success "Default network autostart enabled" - else - warn "Failed to enable default network autostart" - fi - fi -else - error "Default network not found" - info "This is unusual - libvirt should create it automatically" -fi - # Summary section "Setup Complete" success "Testing environment is ready" info "" info "Next steps:" -info " 1. Create base VM: ./scripts/testing/create-base-vm.sh" -info " 2. Run a test: ./scripts/testing/run-test.sh" +info " 1. Copy an archangel ISO to: $PROJECT_ROOT/vm-images/" +info " 2. Create base VM: ./scripts/testing/create-base-vm.sh" +info " 3. Run a test: ./scripts/testing/run-test.sh" info "" info "Log file: $LOGFILE" |
