From 7d0f90da66985b402c6a25eb3eca8cc9e6060ced Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 24 Jan 2026 18:52:34 -0600 Subject: fix(testing): remove obsolete --skip-slow-packages option This flag was removed from archsetup but remained in test scripts. --- scripts/create-archiso-zfs.sh | 33 + scripts/games.sh | 33 + scripts/gitrepos.sh | 6 + scripts/post-install.sh | 58 ++ scripts/protonmail-bridge.sh | 17 + scripts/testing/README.org | 508 +++++++++++++ scripts/testing/archinstall-config.json | 117 +++ scripts/testing/cleanup-tests.sh | 171 +++++ scripts/testing/create-base-vm.sh | 173 +++++ scripts/testing/debug-vm.sh | 133 ++++ scripts/testing/finalize-base-vm.sh | 31 + scripts/testing/lib/finalize-base-vm.sh | 21 + scripts/testing/lib/logging.sh | 151 ++++ scripts/testing/lib/network-diagnostics.sh | 60 ++ scripts/testing/lib/validation.sh | 1080 ++++++++++++++++++++++++++++ scripts/testing/lib/vm-utils.sh | 321 +++++++++ scripts/testing/run-test-baremetal.sh | 310 ++++++++ scripts/testing/run-test.sh | 367 ++++++++++ scripts/testing/setup-testing-env.sh | 191 +++++ scripts/wip-bootcandy.sh | 17 + scripts/wireguard-proton.sh | 13 + scripts/zfs-replicate | 72 ++ 22 files changed, 3883 insertions(+) create mode 100644 scripts/create-archiso-zfs.sh create mode 100755 scripts/games.sh create mode 100755 scripts/gitrepos.sh create mode 100755 scripts/post-install.sh create mode 100644 scripts/protonmail-bridge.sh create mode 100644 scripts/testing/README.org create mode 100644 scripts/testing/archinstall-config.json create mode 100755 scripts/testing/cleanup-tests.sh create mode 100755 scripts/testing/create-base-vm.sh create mode 100755 scripts/testing/debug-vm.sh create mode 100755 scripts/testing/finalize-base-vm.sh create mode 100755 scripts/testing/lib/finalize-base-vm.sh create mode 100755 scripts/testing/lib/logging.sh create mode 100644 scripts/testing/lib/network-diagnostics.sh create mode 100644 scripts/testing/lib/validation.sh create mode 100755 scripts/testing/lib/vm-utils.sh create mode 100755 scripts/testing/run-test-baremetal.sh create mode 100755 scripts/testing/run-test.sh create mode 100755 scripts/testing/setup-testing-env.sh create mode 100644 scripts/wip-bootcandy.sh create mode 100755 scripts/wireguard-proton.sh create mode 100755 scripts/zfs-replicate (limited to 'scripts') diff --git a/scripts/create-archiso-zfs.sh b/scripts/create-archiso-zfs.sh new file mode 100644 index 0000000..3028468 --- /dev/null +++ b/scripts/create-archiso-zfs.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# create-archiso-zfs.sh +# Craig Jennings +# Creates an arch linux iso with zfs support. +# This script must be run as root. + +pacman-key -r DDF7DB817396A49B2A2723F7403BD972F75D9D76 +pacman-key --lsign-key DDF7DB817396A49B2A2723F7403BD972F75D9D76 + +pacman -Syu --noconfirm archiso +mkdir ~/iso + +# copies the releng profile +cp -r /usr/share/archiso/configs/releng/* ~/iso + +# add the archzfs repository to the pacman configuration +echo -e ' +[archzfs] +Server = https://archzfs.com/$repo/$arch +SigLevel = Optional TrustAll' >> ~/iso/pacman.conf + +# tell archiso to install the zfs dkms module and zfs utils to resulting iso +echo -e ' +linux-headers +archzfs-dkms +zfs-utils' >> ~/iso/packages.x86_64 + +# build the iso. this takes some time +mkarchiso -vo ~/iso/out ~/iso + + + diff --git a/scripts/games.sh b/scripts/games.sh new file mode 100755 index 0000000..518a2cb --- /dev/null +++ b/scripts/games.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# games installations via flatpak + +# Make sure we have flatpak already installed +if ! [ -x "$(command -v flatpak)" ]; then + echo 'Error: flatpak is not installed.' >&2 + echo 'Please install it with "sudo pacman -S flatpak" and reboot your system.' >&2 + exit 1 +fi + +flatpak install --noninteractive --assumeyes --user flathub app.drey.Blurble +flatpak install --noninteractive --assumeyes --user flathub com.github.avojak.paint-spill +flatpak install --noninteractive --assumeyes --user flathub flathub org.gnome.Crosswords +flatpak install --noninteractive --assumeyes --user flathub io.github.garglk.Gargoyle +flatpak install --noninteractive --assumeyes --user flathub io.github.nokse22.ultimate-tic-tac-toe +flatpak install --noninteractive --assumeyes --user flathub io.github.swordpuffin.hunt +flatpak install --noninteractive --assumeyes --user flathub io.github.swordpuffin.splices +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Aisleriot +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Chess +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Four-in-a-row +flatpak install --noninteractive --assumeyes --user flathub org.gnome.LightsOff +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Mahjongg +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Mines +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Quadrapassel +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Sudoku +flatpak install --noninteractive --assumeyes --user flathub org.gnome.SwellFoop +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Taquin +flatpak install --noninteractive --assumeyes --user flathub org.gnome.Tetravex +flatpak install --noninteractive --assumeyes --user flathub org.gnome.TwentyFortyEight +flatpak install --noninteractive --assumeyes --user flathub org.gnome.five-or-more +flatpak install --noninteractive --assumeyes --user flathub org.gnome.gbrainy +flatpak install --noninteractive --assumeyes --user flathub org.gottcode.Peg-E +flatpak install --noninteractive --assumeyes --user flathub org.gottcode.Tanglet diff --git a/scripts/gitrepos.sh b/scripts/gitrepos.sh new file mode 100755 index 0000000..a3df4d4 --- /dev/null +++ b/scripts/gitrepos.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# cjennings +# replaces original http remote repositories with git ones + +cd ~/.emacs.d && git remote remove origin && git remote add origin git@cjennings.net:dotemacs.git && git pull --set-upstream origin main +cd ~/.dotfiles && git remote remove origin && git remote add origin git@cjennings.net:dotfiles.git && git pull --set-upstream origin main diff --git a/scripts/post-install.sh b/scripts/post-install.sh new file mode 100755 index 0000000..611f5c3 --- /dev/null +++ b/scripts/post-install.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +logfile="$HOME/post-install.log" +touch $logfile + +echo "fixing perms on gpg directory" +{ + chown -R $(whoami) ~/.gnupg/ + find ~/.gnupg -type f -exec chmod 600 {} \; + find ~/.gnupg -type d -exec chmod 700 {} \; +} >> $logfile 2>&1 + +echo "fixing remote repositories" +{ + cd ~/.dotfiles && \ + git remote remove origin && \ + git remote add origin git@cjennings.net:dotfiles.git + + cd ~/.emacs.d && \ + git remote remove origin && \ + git remote add origin git@cjennings.net:dotemacs.git +} + +echo "cloning git repos" +{ + git clone cjennings@cjennings.net:git/org.git ~/sync/org + git clone --depth 1 cjennings@cjennings.net:git/wallpaper.git ~/pictures/wallpaper + git clone git@cjennings.net:dwm.git ~/code/dwm + git clone git@cjennings.net:dmenu.git ~/code/dmenu + git clone git@cjennings.net:st.git ~/code/st + git clone git@cjennings.net:slock.git ~/code/slock + git clone git@cjennings.net:pinentry-dmenu.git ~/code/pinentry-dmenu + + git clone cjennings@cjennings.net:git/bsdsetup.git ~/code/bsdsetup + git clone git@cjennings.net:git/archsetup.git ~/code/archsetup + git clone git@cjennings.net:dotemacs.git ~/code/dotemacs + + git clone cjennings@cjennings.net:git/wttrin.git ~/code/wttrin.git + git clone cjennings@cjennings.net:git/rsyncshot.git ~/code/rsyncshot.git + + git clone cjennings@cjennings.net:git/exercism.git ~/code/exercism + git clone cjennings@cjennings.net:git/elisp.git ~/code/elisp + git clone cjennings@cjennings.net:git/clisp.git ~/code/clisp + git clone cjennings@cjennings.net:git/lcthw.git ~/code/lcthw + git clone cjennings@cjennings.net:git/100dayspython.git ~/code/100dayspython + + git clone cjennings@cjennings.net:git/documents.git ~/projects/documents + git clone cjennings@cjennings.net:git/kit.git ~/projects/kit + git clone cjennings@cjennings.net:git/clipper.git ~/projects/clipper + git clone cjennings@cjennings.net:git/finances.git ~/projects/finances + git clone cjennings@cjennings.net:git/nasbuild.git ~/projects/nasbuild + git clone cjennings@cjennings.net:git/nextjob.git ~/projects/nextjob + git clone cjennings@cjennings.net:git/elibrary.git ~/projects/elibrary + git clone cjennings@cjennings.net:git/danneel-hoa.git ~/projects/danneel-hoa + git clone cjennings@cjennings.net:git/danneel-remodel.git ~/projects/danneel-remodel +} >> $logfile 2>&1 + +printf "\n\nDone.\n\n" diff --git a/scripts/protonmail-bridge.sh b/scripts/protonmail-bridge.sh new file mode 100644 index 0000000..7dd8ad6 --- /dev/null +++ b/scripts/protonmail-bridge.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# build proton bridge +mkdir $HOME/protonbridgebuild && cd $HOME/protonbridgebuild +wget https://proton.me/download/bridge/PKGBUILD +makepkg -s --install + +# init pass with a passphrase-free GPG key +gpg --batch --passphrase '' --quick-gen-key 'ProtonMailBridge' default default never +pass init "ProtonMailBridge" + +# launch protonmail-bridge +cd $HOME +protonmail-bridge & + +# cleanup +rm -rf $HOME/protonbridgebuild diff --git a/scripts/testing/README.org b/scripts/testing/README.org new file mode 100644 index 0000000..a52dbea --- /dev/null +++ b/scripts/testing/README.org @@ -0,0 +1,508 @@ +#+TITLE: ArchSetup Testing Infrastructure +#+AUTHOR: Craig Jennings +#+DATE: 2025-11-08 + +* Overview + +This directory contains the complete testing infrastructure for archsetup, built on QEMU/KVM virtualization. It provides automated, reproducible testing of the archsetup installation script in isolated virtual machines. + +** Philosophy + +*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. + +* Quick Start + +** One-Time Setup + +#+begin_src bash +# Install required packages and configure environment +./scripts/testing/setup-testing-env.sh + +# Log out and back in (for libvirt group membership) + +# Create the base VM (minimal Arch installation) +./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 + +#+begin_src bash +# Test the current archsetup script +./scripts/testing/run-test.sh + +# Test a specific version +./scripts/testing/run-test.sh --script /path/to/archsetup + +# Keep VM running for debugging +./scripts/testing/run-test.sh --keep +#+end_src + +** 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) +./scripts/testing/debug-vm.sh --base +#+end_src + +** Clean Up + +#+begin_src bash +# Interactive cleanup (prompts for confirmation) +./scripts/testing/cleanup-tests.sh + +# Keep last 10 test results +./scripts/testing/cleanup-tests.sh --keep 10 + +# Force cleanup without prompts +./scripts/testing/cleanup-tests.sh --force +#+end_src + +* Scripts + +** setup-testing-env.sh + +*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 +- 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 + +*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= + +** 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 +- Executes archsetup inside VM +- Captures logs and results +- Runs validation checks +- Generates test report +- Cleans up (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 +- =archsetup-*.log= - Log from archsetup script +- =*.txt= - Package lists from VM + +** debug-vm.sh + +*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 + +*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= + +** 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 +- 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 + +# Keep last 3 test results +./scripts/testing/cleanup-tests.sh --keep 3 + +# Force without prompts +./scripts/testing/cleanup-tests.sh --force +#+end_src + +* Directory Structure + +#+begin_example +archsetup/ +├── scripts/ +│ └── testing/ +│ ├── README.org # This file +│ ├── setup-testing-env.sh # Setup infrastructure +│ ├── create-base-vm.sh # Create base VM +│ ├── run-test.sh # Run tests +│ ├── debug-vm.sh # Interactive debugging +│ ├── cleanup-tests.sh # Clean up +│ ├── finalize-base-vm.sh # Finalize base (generated) +│ ├── archinstall-config.json # Archinstall config +│ └── lib/ +│ ├── logging.sh # Logging utilities +│ └── vm-utils.sh # VM management +├── vm-images/ # VM disk images (gitignored) +│ ├── archsetup-base.qcow2 # Golden image +│ ├── arch-latest.iso # Arch ISO +│ └── archsetup-test-*.qcow2 # Test VMs +├── test-results/ # Test results (gitignored) +│ ├── TIMESTAMP/ +│ │ ├── test.log +│ │ ├── test-report.txt +│ │ └── archsetup-*.log +│ └── latest -> TIMESTAMP/ # Symlink to latest +└── docs/ + └── testing-strategy.org # Complete strategy doc +#+end_example + +* Configuration + +** VM Specifications + +All test VMs use: +- *CPUs:* 2 vCPUs +- *RAM:* 4GB (matches archsetup tmpfs build directory) +- *Disk:* 50GB (thin provisioned qcow2) +- *Network:* NAT via libvirt default network +- *Boot:* UEFI (systemd-boot bootloader) +- *Display:* Serial console + VNC + +Set environment variables to customize: +#+begin_src bash +VM_CPUS=4 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) +- Root password: "archsetup" + +This matches the documented prerequisites for archsetup. + +* Validation Checks + +Each test run performs these validation checks: + +** Critical (Must Pass) +1. archsetup exits with code 0 +2. User 'cjennings' was created +3. Dotfiles are stowed (symlinks exist) +4. yay (AUR helper) is installed +5. DWM is built and installed + +** Additional (Future) +- All expected packages installed +- X11 can start +- systemd services enabled +- Firewall configured + +* 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: +#+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 +#+end_src + +** SSH connection refused + +Wait longer for VM to boot, or check if sshd is enabled: +#+begin_src bash +virsh console VM_NAME +# Inside VM: +systemctl status sshd +systemctl start sshd +#+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 +lsmod | grep kvm +#+end_src + +** Disk space issues + +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: + +#+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 + +# Exit and poweroff +exit +poweroff +#+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 +- [ ] 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) +- [ ] Network failure recovery +- [ ] Offline installation (local package cache) +- [ ] Different hardware profiles (CPU/RAM variations) + +* References + +- [[file:../../docs/testing-strategy.org][Testing Strategy Document]] +- [[https://wiki.archlinux.org/title/Libvirt][Arch Wiki: libvirt]] +- [[https://wiki.archlinux.org/title/QEMU][Arch Wiki: QEMU]] +- [[file:../../archsetup][Main archsetup script]] +- [[file:../../TODO.org][Project TODO]] diff --git a/scripts/testing/archinstall-config.json b/scripts/testing/archinstall-config.json new file mode 100644 index 0000000..a55e2a1 --- /dev/null +++ b/scripts/testing/archinstall-config.json @@ -0,0 +1,117 @@ +{ + "!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/cleanup-tests.sh b/scripts/testing/cleanup-tests.sh new file mode 100755 index 0000000..e4289a7 --- /dev/null +++ b/scripts/testing/cleanup-tests.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# Clean up old test VMs and artifacts +# Author: Craig Jennings +# License: GNU GPLv3 + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source utilities +source "$SCRIPT_DIR/lib/logging.sh" +source "$SCRIPT_DIR/lib/vm-utils.sh" + +# Parse arguments +KEEP_LAST=5 +FORCE=false + +while [[ $# -gt 0 ]]; do + case $1 in + --keep) + KEEP_LAST="$2" + shift 2 + ;; + --force) + FORCE=true + shift + ;; + *) + echo "Usage: $0 [--keep N] [--force]" + echo " --keep N Keep last N test results (default: 5)" + echo " --force Skip confirmation prompts" + exit 1 + ;; + esac +done + +# Initialize logging +LOGFILE="/tmp/cleanup-tests-$(date +'%Y%m%d-%H%M%S').log" +init_logging "$LOGFILE" + +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) + +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 + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + info "Skipping VM cleanup" + 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 + fi + 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 + fi +fi + +# Clean up test disk images +section "Cleaning Up Disk Images" + +step "Finding test disk images" +if [ -d "$PROJECT_ROOT/vm-images" ]; then + TEST_DISKS=$(find "$PROJECT_ROOT/vm-images" -name "archsetup-test-*.qcow2" 2>/dev/null || true) + + if [ -z "$TEST_DISKS" ]; then + info "No test 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" + + if ! $FORCE; then + echo "" + echo "$TEST_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 + rm -f "$disk" + done + success "Test disk images deleted" + fi + else + echo "$TEST_DISKS" | while read disk; do + rm -f "$disk" + done + success "Test disk images deleted" + fi + fi +fi + +# Clean up old test results +section "Cleaning Up Test Results" + +if [ ! -d "$PROJECT_ROOT/test-results" ]; then + info "No test results directory" +else + step "Finding test result directories" + TEST_RESULTS=$(find "$PROJECT_ROOT/test-results" -maxdepth 1 -type d -name "20*" 2>/dev/null | sort -r || true) + + if [ -z "$TEST_RESULTS" ]; then + info "No test results found" + else + RESULT_COUNT=$(echo "$TEST_RESULTS" | wc -l) + info "Found $RESULT_COUNT test result directory(ies)" + + if [ $RESULT_COUNT -le $KEEP_LAST ]; then + info "Keeping all results (count <= $KEEP_LAST)" + else + TO_DELETE=$(echo "$TEST_RESULTS" | tail -n +$((KEEP_LAST + 1))) + DELETE_COUNT=$(echo "$TO_DELETE" | wc -l) + info "Keeping last $KEEP_LAST, deleting $DELETE_COUNT old result(s)" + + if ! $FORCE; then + 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 + echo "$TO_DELETE" | while read dir; do + rm -rf "$dir" + done + success "Old test results deleted" + fi + else + echo "$TO_DELETE" | while read dir; do + rm -rf "$dir" + done + success "Old test results deleted" + fi + fi + fi +fi + +section "Cleanup Complete" + +info "Log file: $LOGFILE" diff --git a/scripts/testing/create-base-vm.sh b/scripts/testing/create-base-vm.sh new file mode 100755 index 0000000..03409fe --- /dev/null +++ b/scripts/testing/create-base-vm.sh @@ -0,0 +1,173 @@ +#!/bin/bash +# Create base VM for archsetup testing - Manual Installation +# Author: Craig Jennings +# License: GNU GPLv3 +# +# This script creates a VM booted from Arch ISO, then waits for you to +# manually install Arch using archinstall. + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source utilities +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" + +# Initialize logging +LOGFILE="$PROJECT_ROOT/test-results/create-base-vm-$(date +'%Y%m%d-%H%M%S').log" +init_logging "$LOGFILE" + +section "Creating Base VM for ArchSetup Testing" + +# Verify 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" +success "Prerequisites satisfied" + +# Create vm-images directory +mkdir -p "$VM_IMAGES_DIR" + +# Download Arch ISO if needed +section "Preparing Arch Linux ISO" + +if [ -f "$ISO_PATH" ]; then + info "Arch ISO exists: $ISO_PATH" + + # 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 +fi + +if [ ! -f "$ISO_PATH" ]; then + step "Downloading latest Arch ISO" + info "URL: $ISO_URL" + info "This may take several minutes..." + + 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 +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 +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)" +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" +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 new file mode 100755 index 0000000..a442850 --- /dev/null +++ b/scripts/testing/debug-vm.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Launch VM for interactive debugging +# Author: Craig Jennings +# License: GNU GPLv3 + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source utilities +source "$SCRIPT_DIR/lib/logging.sh" +source "$SCRIPT_DIR/lib/vm-utils.sh" + +# Parse arguments +VM_DISK="" +USE_BASE=false + +if [ $# -eq 0 ]; then + USE_BASE=true +elif [ "$1" = "--base" ]; then + USE_BASE=true +elif [ -f "$1" ]; then + VM_DISK="$1" +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" + 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" +ROOT_PASSWORD="archsetup" + +# Initialize logging +LOGFILE="/tmp/debug-vm-$TIMESTAMP.log" +init_logging "$LOGFILE" + +section "Launching Debug VM" + +# Determine which disk to use +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" + else + info "Using existing disk: $VM_DISK" + fi +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) + +# 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 "" +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 "" + +# 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" + sleep 2 + virsh console "$DEBUG_VM_NAME" +fi diff --git a/scripts/testing/finalize-base-vm.sh b/scripts/testing/finalize-base-vm.sh new file mode 100755 index 0000000..225ffae --- /dev/null +++ b/scripts/testing/finalize-base-vm.sh @@ -0,0 +1,31 @@ +#!/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/finalize-base-vm.sh b/scripts/testing/lib/finalize-base-vm.sh new file mode 100755 index 0000000..e3913ea --- /dev/null +++ b/scripts/testing/lib/finalize-base-vm.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Finalize base VM after installation +VM_NAME="archsetup-base" +echo "[i] Removing ISO from VM..." +virsh change-media $VM_NAME sda --eject 2>/dev/null || true +virsh change-media $VM_NAME hda --eject 2>/dev/null || true +echo "[✓] ISO removed" +echo "[i] Starting VM from installed system..." +virsh start $VM_NAME +echo "[i] Waiting for boot..." +sleep 30 +IP=$(virsh 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/logging.sh b/scripts/testing/lib/logging.sh new file mode 100755 index 0000000..eda9eb1 --- /dev/null +++ b/scripts/testing/lib/logging.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# Logging utilities for archsetup testing +# Author: Craig Jennings +# License: GNU GPLv3 + +# Global log file (set by calling script) +LOGFILE="${LOGFILE:-/tmp/archsetup-test.log}" + +# Initialize logging +init_logging() { + local logfile="$1" + LOGFILE="$logfile" + + # Create log directory if it doesn't exist + mkdir -p "$(dirname "$LOGFILE")" + + # Initialize log file + echo "=== Test Log Started: $(date +'%Y-%m-%d %H:%M:%S') ===" > "$LOGFILE" + echo "" >> "$LOGFILE" +} + +# Log message (to file and optionally stdout) +log() { + local message="$1" + local timestamp + timestamp=$(date +'%Y-%m-%d %H:%M:%S') + echo "[$timestamp] $message" >> "$LOGFILE" +} + +# Info message +info() { + local message="$1" + echo "[i] $message" + log "INFO: $message" +} + +# Success message +success() { + local message="$1" + echo "[✓] $message" + log "SUCCESS: $message" +} + +# Warning message +warn() { + local message="$1" + echo "[!] $message" + log "WARNING: $message" +} + +# Error message +error() { + local message="$1" + echo "[✗] $message" >&2 + log "ERROR: $message" +} + +# Fatal error (exits script) +fatal() { + local message="$1" + local exit_code="${2:-1}" + echo "[✗] FATAL: $message" >&2 + log "FATAL: $message (exit code: $exit_code)" + exit "$exit_code" +} + +# Section header +section() { + local title="$1" + echo "" + echo "=== $title ===" + log "=== $title ===" +} + +# Step message +step() { + local message="$1" + echo " -> $message" + log " STEP: $message" +} + +# Progress indicator (for long-running operations) +progress() { + local message="$1" + echo " ... $message" + log " PROGRESS: $message" +} + +# Clear progress line and show completion +complete() { + local message="$1" + echo " [✓] $message" + log " COMPLETE: $message" +} + +# Show command being executed (useful for debugging) +show_cmd() { + local cmd="$1" + echo "$ $cmd" + log "CMD: $cmd" +} + +# Separator line +separator() { + echo "----------------------------------------" +} + +# Summary statistics +summary() { + local passed="$1" + local failed="$2" + local total=$((passed + failed)) + + echo "" + separator + section "Test Summary" + echo " Total: $total" + echo " Passed: $passed" + echo " Failed: $failed" + separator + echo "" + + log "=== Test Summary ===" + log "Total: $total, Passed: $passed, Failed: $failed" +} + +# Timer utilities +declare -A TIMERS + +start_timer() { + local name="${1:-default}" + TIMERS[$name]=$(date +%s) + log "TIMER START: $name" +} + +stop_timer() { + local name="${1:-default}" + local start=${TIMERS[$name]} + local end=$(date +%s) + local duration=$((end - start)) + local mins=$((duration / 60)) + local secs=$((duration % 60)) + + if [ $mins -gt 0 ]; then + echo " Time: ${mins}m ${secs}s" + log "TIMER STOP: $name (${mins}m ${secs}s)" + else + echo " Time: ${secs}s" + log "TIMER STOP: $name (${secs}s)" + fi +} diff --git a/scripts/testing/lib/network-diagnostics.sh b/scripts/testing/lib/network-diagnostics.sh new file mode 100644 index 0000000..3f9735b --- /dev/null +++ b/scripts/testing/lib/network-diagnostics.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Network diagnostics for VM testing +# Author: Craig Jennings +# License: GNU GPLv3 + +# Note: logging.sh should already be sourced by the calling script + +# Run quick network diagnostics +# Args: $1 = VM IP address or hostname +run_network_diagnostics() { + local vm_host="$1" + + section "Pre-flight Network Diagnostics" + + # Test 1: Basic connectivity + 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 + success "Internet connectivity OK" + else + error "No internet connectivity" + return 1 + fi + + # Test 2: DNS resolution + 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 + success "DNS resolution OK" + else + error "DNS resolution failed" + return 1 + fi + + # 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 + success "Arch mirrors accessible" + else + error "Cannot reach Arch mirrors" + return 1 + fi + + # 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 + success "AUR accessible" + else + error "Cannot reach AUR" + return 1 + fi + + # 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 + info " $line" + done + + success "Network diagnostics complete" + return 0 +} diff --git a/scripts/testing/lib/validation.sh b/scripts/testing/lib/validation.sh new file mode 100644 index 0000000..c0a5d43 --- /dev/null +++ b/scripts/testing/lib/validation.sh @@ -0,0 +1,1080 @@ +#!/bin/bash +# Validation utilities for archsetup testing +# Author: Craig Jennings +# License: GNU GPLv3 +# +# This module provides comprehensive validation checks for archsetup installations. +# It captures pre-install state, runs post-install validations, and attributes +# issues to either archsetup or the base install (archzfs/vanilla Arch). + +# Validation counters +VALIDATION_PASSED=0 +VALIDATION_FAILED=0 +VALIDATION_WARNINGS=0 + +# Arrays to track issues +declare -a ARCHSETUP_ISSUES +declare -a BASE_INSTALL_ISSUES +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 +} + +# Validation result helpers +validation_pass() { + local test_name="$1" + success "$test_name" + ((VALIDATION_PASSED++)) || true +} + +validation_fail() { + local test_name="$1" + local details="${2:-}" + error "$test_name" + [ -n "$details" ] && info " Details: $details" + ((VALIDATION_FAILED++)) || true +} + +validation_warn() { + local test_name="$1" + local details="${2:-}" + warn "$test_name" + [ -n "$details" ] && info " Details: $details" + ((VALIDATION_WARNINGS++)) || true +} + +# Attribute an issue to archsetup or base install +attribute_issue() { + local issue="$1" + local source="$2" # "archsetup", "base", or "unknown" + + case "$source" in + archsetup) + ARCHSETUP_ISSUES+=("$issue") + ;; + base) + BASE_INSTALL_ISSUES+=("$issue") + ;; + *) + UNKNOWN_ISSUES+=("$issue") + ;; + esac +} + +#============================================================================= +# PRE-INSTALL LOG CAPTURE +#============================================================================= + +capture_pre_install_state() { + local output_dir="$1" + + section "Capturing Pre-Install State" + mkdir -p "$output_dir/pre-install" + + step "Capturing system logs before archsetup" + + # Capture journal + ssh_cmd "journalctl -b --no-pager" > "$output_dir/pre-install/journal.log" 2>&1 || true + + # Capture dmesg + ssh_cmd "dmesg" > "$output_dir/pre-install/dmesg.log" 2>&1 || true + + # Capture package list + ssh_cmd "pacman -Q" > "$output_dir/pre-install/packages.txt" 2>&1 || true + + # Capture service status + ssh_cmd "systemctl list-units --type=service --all" > "$output_dir/pre-install/services.txt" 2>&1 || true + + # Capture failed services + ssh_cmd "systemctl --failed" > "$output_dir/pre-install/failed-services.txt" 2>&1 || true + + # Capture existing errors in logs + ssh_cmd "journalctl -b -p err --no-pager" > "$output_dir/pre-install/errors.log" 2>&1 || true + + # Count pre-existing errors + PRE_INSTALL_ERROR_COUNT=$(wc -l < "$output_dir/pre-install/errors.log" 2>/dev/null || echo 0) + + success "Pre-install state captured ($PRE_INSTALL_ERROR_COUNT pre-existing error lines)" +} + +#============================================================================= +# POST-INSTALL LOG CAPTURE +#============================================================================= + +capture_post_install_state() { + local output_dir="$1" + + section "Capturing Post-Install State" + mkdir -p "$output_dir/post-install" + + step "Capturing system logs after archsetup" + + # Capture journal + ssh_cmd "journalctl -b --no-pager" > "$output_dir/post-install/journal.log" 2>&1 || true + + # Capture dmesg + ssh_cmd "dmesg" > "$output_dir/post-install/dmesg.log" 2>&1 || true + + # Capture package list + ssh_cmd "pacman -Q" > "$output_dir/post-install/packages.txt" 2>&1 || true + + # Capture service status + ssh_cmd "systemctl list-units --type=service --all" > "$output_dir/post-install/services.txt" 2>&1 || true + + # Capture failed services + ssh_cmd "systemctl --failed" > "$output_dir/post-install/failed-services.txt" 2>&1 || true + + # Capture all errors + ssh_cmd "journalctl -b -p err --no-pager" > "$output_dir/post-install/errors.log" 2>&1 || true + + # Capture archsetup log + ssh_cmd "cat /var/log/archsetup-*.log 2>/dev/null" > "$output_dir/post-install/archsetup.log" 2>&1 || true + + success "Post-install state captured" +} + +#============================================================================= +# LOG DIFF ANALYSIS +#============================================================================= + +analyze_log_diff() { + local output_dir="$1" + + section "Analyzing Log Differences" + mkdir -p "$output_dir/analysis" + + step "Comparing pre and post install errors" + + # Find new errors (in post but not in pre) + if [ -f "$output_dir/pre-install/errors.log" ] && [ -f "$output_dir/post-install/errors.log" ]; then + comm -13 <(sort "$output_dir/pre-install/errors.log") <(sort "$output_dir/post-install/errors.log") \ + > "$output_dir/analysis/new-errors.log" 2>/dev/null || true + + NEW_ERROR_COUNT=$(wc -l < "$output_dir/analysis/new-errors.log" 2>/dev/null || echo 0) + + if [ "$NEW_ERROR_COUNT" -gt 0 ]; then + warn "Found $NEW_ERROR_COUNT new error lines after archsetup" + # Categorize errors + categorize_errors "$output_dir/analysis/new-errors.log" "$output_dir/analysis" + else + success "No new errors introduced by archsetup" + fi + fi + + step "Checking for new failed services" + + # Compare failed services + if [ -f "$output_dir/pre-install/failed-services.txt" ] && [ -f "$output_dir/post-install/failed-services.txt" ]; then + local pre_failed post_failed + pre_failed=$(grep -c "failed" "$output_dir/pre-install/failed-services.txt" 2>/dev/null | tr -d '[:space:]') + post_failed=$(grep -c "failed" "$output_dir/post-install/failed-services.txt" 2>/dev/null | tr -d '[:space:]') + # Default to 0 if empty + pre_failed=${pre_failed:-0} + post_failed=${post_failed:-0} + + if [ "$post_failed" -gt "$pre_failed" ]; then + warn "New failed services detected (before: $pre_failed, after: $post_failed)" + diff "$output_dir/pre-install/failed-services.txt" "$output_dir/post-install/failed-services.txt" \ + > "$output_dir/analysis/failed-services-diff.txt" 2>/dev/null || true + else + success "No new service failures" + fi + fi + + step "Counting new packages installed" + + if [ -f "$output_dir/pre-install/packages.txt" ] && [ -f "$output_dir/post-install/packages.txt" ]; then + comm -13 <(sort "$output_dir/pre-install/packages.txt") <(sort "$output_dir/post-install/packages.txt") \ + > "$output_dir/analysis/new-packages.txt" 2>/dev/null || true + + local new_pkg_count=$(wc -l < "$output_dir/analysis/new-packages.txt" 2>/dev/null || echo 0) + info "Installed $new_pkg_count new packages" + fi +} + +categorize_errors() { + local error_log="$1" + local output_dir="$2" + + # Known benign errors/warnings to ignore + local -a BENIGN_PATTERNS=( + "SPL:.*module verification failed" + "ZFS:.*module verification failed" + "tainting kernel" + "RAS:.*Correctable Errors" + "ACPI.*AE_NOT_FOUND" + "firmware.*regulatory" + "Invalid user name.*in service file" # dbus-broker timing during package install + ) + + # Patterns that indicate archsetup issues + local -a ARCHSETUP_PATTERNS=( + "archsetup" + "stow" + "yay" + "makepkg" + "pacman.*error" + ) + + # Filter and categorize + while IFS= read -r line; do + local is_benign=false + local is_archsetup=false + + # Check if benign + for pattern in "${BENIGN_PATTERNS[@]}"; do + if echo "$line" | grep -qiE "$pattern"; then + is_benign=true + break + fi + done + + if $is_benign; then + echo "$line" >> "$output_dir/benign-errors.log" + continue + fi + + # Check if archsetup-related + for pattern in "${ARCHSETUP_PATTERNS[@]}"; do + if echo "$line" | grep -qiE "$pattern"; then + is_archsetup=true + break + fi + done + + if $is_archsetup; then + echo "$line" >> "$output_dir/archsetup-errors.log" + attribute_issue "$line" "archsetup" + else + echo "$line" >> "$output_dir/base-install-errors.log" + attribute_issue "$line" "base" + fi + done < "$error_log" +} + +#============================================================================= +# VALIDATION CHECKS +#============================================================================= + +run_all_validations() { + section "Running Validation Checks" + + # User & Authentication + validate_user_created + validate_user_shell + validate_user_groups + + # Dotfiles + validate_dotfiles + + # Package Managers + validate_yay_installed + validate_pacman_working + + # Window Manager (detects DWM or Hyprland automatically) + validate_window_manager + + # Essential Services + validate_firewall + validate_dns_config + validate_avahi + validate_fail2ban + validate_networkmanager + + # Developer Tools + validate_emacs + validate_git_config + validate_dev_tools + + # System Configuration + validate_zfs_config + validate_boot_config + validate_autologin_config + validate_gnome_keyring_setup + + # Boot & Initramfs (critical for ZFS systems) + validate_terminus_font + validate_mkinitcpio_hooks + validate_initramfs_consolefont + validate_nvme_module + + # Archsetup Specific + validate_archsetup_log + validate_state_markers +} + +#----------------------------------------------------------------------------- +# User & Authentication Validations +#----------------------------------------------------------------------------- + +validate_user_created() { + step "Checking if user 'cjennings' exists" + if ssh_cmd "id cjennings" &>> "$LOGFILE"; then + validation_pass "User cjennings exists" + else + validation_fail "User cjennings not found" + attribute_issue "User cjennings not created" "archsetup" + fi +} + +validate_user_shell() { + step "Checking if ZSH is default shell" + local shell=$(ssh_cmd "getent passwd cjennings | cut -d: -f7") + if [ "$shell" = "/bin/zsh" ] || [ "$shell" = "/usr/bin/zsh" ]; then + validation_pass "ZSH is default shell" + else + validation_fail "ZSH not default shell (got: $shell)" + attribute_issue "ZSH not set as default shell" "archsetup" + fi +} + +validate_user_groups() { + step "Checking user group memberships" + # Groups added by archsetup: + # - wheel (useradd -G wheel) + # - sys,adm,network,scanner,power,uucp,audio,lp,rfkill,video,storage,optical,users (usermod -aG) + # - docker (gpasswd -a, added later in developer_workstation) + local expected_groups="wheel sys adm network scanner power uucp audio lp rfkill video storage optical users docker" + local missing_groups="" + + for group in $expected_groups; do + if ! ssh_cmd "groups cjennings" | grep -q "\b$group\b"; then + missing_groups="$missing_groups $group" + fi + done + + if [ -z "$missing_groups" ]; then + validation_pass "User in all expected groups (15 groups)" + else + validation_fail "User missing groups:$missing_groups" + attribute_issue "User missing groups:$missing_groups" "archsetup" + fi +} + +#----------------------------------------------------------------------------- +# Dotfiles Validations +#----------------------------------------------------------------------------- + +validate_dotfiles() { + step "Checking dotfiles setup" + + # 1. Check if .zshrc is a symlink + if ! ssh_cmd "test -L /home/cjennings/.zshrc"; then + validation_fail "Dotfiles not stowed (.zshrc is not a symlink)" + attribute_issue "Dotfiles stow failed" "archsetup" + return 1 + fi + + # 2. Check symlink points to correct location + local target=$(ssh_cmd "readlink /home/cjennings/.zshrc") + local expected_pattern="code/archsetup/dotfiles/system/.zshrc" + + if ! echo "$target" | grep -q "$expected_pattern"; then + validation_fail "Dotfiles symlink points to wrong location: $target" + attribute_issue "Dotfiles symlink incorrect: $target" "archsetup" + return 1 + fi + + # 3. Check the target file actually exists (not a broken symlink) + if ! ssh_cmd "test -f /home/cjennings/.zshrc"; then + validation_fail "Dotfiles symlink is broken (target doesn't exist)" + ssh_cmd "ls -la /home/cjennings/.zshrc" >> "$LOGFILE" 2>&1 + attribute_issue "Dotfiles symlink broken" "archsetup" + return 1 + fi + + # 4. Check user can actually read the file (not just root) + local result=$(ssh_cmd "sudo -u cjennings cat /home/cjennings/.zshrc > /dev/null 2>&1 && echo OK || echo FAIL") + if [ "$result" != "OK" ]; then + validation_fail "Dotfiles not readable by user (permission issue)" + ssh_cmd "ls -la /home/cjennings/.zshrc" >> "$LOGFILE" 2>&1 + attribute_issue "Dotfiles not readable by user" "archsetup" + return 1 + fi + + validation_pass "Dotfiles configured correctly (symlink to $target, readable by user)" +} + +#----------------------------------------------------------------------------- +# Package Manager Validations +#----------------------------------------------------------------------------- + +validate_yay_installed() { + step "Checking if yay (AUR helper) is installed and functional" + + # Check binary exists + if ! ssh_cmd "which yay" &>> "$LOGFILE"; then + validation_fail "yay not found" + attribute_issue "yay not installed" "archsetup" + return 1 + fi + + # Check yay can query packages (functional test) + if ssh_cmd "sudo -u cjennings yay -Qi yay" &>> "$LOGFILE"; then + validation_pass "yay is installed and functional" + else + validation_fail "yay binary exists but query failed" + attribute_issue "yay not functional" "archsetup" + fi +} + +validate_pacman_working() { + step "Checking if pacman is functional" + if ssh_cmd "pacman -Qi base" &>> "$LOGFILE"; then + validation_pass "pacman is functional" + else + validation_fail "pacman query failed" + attribute_issue "pacman not functional" "unknown" + fi +} + +#----------------------------------------------------------------------------- +# Window Manager Validations +#----------------------------------------------------------------------------- + +validate_suckless_tools() { + step "Checking suckless tools (dwm, st, dmenu, slock)" + local missing="" + + for tool in dwm st dmenu slock; do + if ! ssh_cmd "test -f /usr/local/bin/$tool"; then + missing="$missing $tool" + fi + done + + if [ -z "$missing" ]; then + validation_pass "All suckless tools installed (dwm, st, dmenu, slock)" + else + validation_fail "Missing suckless tools:$missing" + attribute_issue "Missing suckless tools:$missing" "archsetup" + fi +} + +validate_hyprland_tools() { + step "Checking Hyprland tools" + local missing="" + + # Check core Hyprland packages + for pkg in hyprland hypridle hyprlock waybar wofi swww grim slurp gammastep; 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 + validation_fail "Missing Hyprland tools:$missing" + attribute_issue "Missing Hyprland tools:$missing" "archsetup" + fi +} + +validate_hyprland_config() { + step "Checking Hyprland configuration files" + local missing="" + + 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 + if ! ssh_cmd "test -f /home/cjennings/$config"; then + missing="$missing $config" + fi + done + + if [ -z "$missing" ]; then + validation_pass "All Hyprland config files present" + else + validation_fail "Missing Hyprland configs:$missing" + attribute_issue "Missing Hyprland configs:$missing" "archsetup" + fi +} + +validate_hyprland_socket() { + step "Checking Hyprland IPC socket" + # Note: This only works if Hyprland is running. Skip if no display. + if ssh_cmd "test -S /tmp/hypr/*/.socket.sock 2>/dev/null"; then + validation_pass "Hyprland socket exists" + else + validation_warn "Hyprland socket not found (Hyprland may not be running)" + fi +} + +validate_window_manager() { + # Detect which desktop environment is installed and validate accordingly + if ssh_cmd "pacman -Q hyprland &>/dev/null"; then + section "Hyprland Desktop Environment" + validate_hyprland_tools + validate_hyprland_config + validate_hyprland_socket + elif ssh_cmd "test -f /usr/local/bin/dwm"; then + section "DWM Desktop Environment" + validate_suckless_tools + else + validation_warn "No window manager detected (DESKTOP_ENV=none?)" + fi +} + +#----------------------------------------------------------------------------- +# Essential Services Validations +#----------------------------------------------------------------------------- + +validate_firewall() { + step "Checking if firewall (ufw) is enabled" + local status=$(ssh_cmd "systemctl is-enabled ufw.service 2>/dev/null || echo disabled") + if [ "$status" = "enabled" ]; then + validation_pass "UFW firewall is enabled" + else + validation_fail "UFW firewall not enabled" + attribute_issue "UFW not enabled" "archsetup" + fi +} + +validate_dns_config() { + step "Checking DNS-over-TLS configuration" + if ssh_cmd "grep -q 'DNS=.*#' /etc/systemd/resolved.conf 2>/dev/null"; then + validation_pass "DNS-over-TLS configured" + else + validation_warn "DNS-over-TLS may not be configured" + fi +} + +validate_avahi() { + step "Checking avahi-daemon status" + local status=$(ssh_cmd "systemctl is-enabled avahi-daemon.service 2>/dev/null || echo disabled") + if [ "$status" = "enabled" ]; then + validation_pass "avahi-daemon is enabled" + + # Full-stack mDNS test: ping hostname.local + local hostname=$(ssh_cmd "hostname") + if ssh_cmd "ping -c 1 -W 2 ${hostname}.local" &>> "$LOGFILE"; then + validation_pass "mDNS working (${hostname}.local responds to ping)" + else + validation_warn "mDNS ping failed (avahi may need time to propagate)" + fi + else + # This might be OK if avahi was pre-installed + validation_warn "avahi-daemon not enabled (may have been pre-configured)" + fi +} + +validate_fail2ban() { + step "Checking fail2ban status" + local status=$(ssh_cmd "systemctl is-enabled fail2ban.service 2>/dev/null || echo disabled") + if [ "$status" = "enabled" ]; then + validation_pass "fail2ban is enabled" + else + validation_fail "fail2ban not enabled" + attribute_issue "fail2ban not enabled" "archsetup" + fi +} + +validate_networkmanager() { + step "Checking NetworkManager status" + local status=$(ssh_cmd "systemctl is-enabled NetworkManager.service 2>/dev/null || echo disabled") + if [ "$status" = "enabled" ]; then + validation_pass "NetworkManager is enabled" + # Functional test + if ssh_cmd "nmcli general status" &>> "$LOGFILE"; then + validation_pass "NetworkManager is functional" + else + validation_warn "NetworkManager enabled but not responding" + fi + else + validation_fail "NetworkManager not enabled" + attribute_issue "NetworkManager not enabled" "archsetup" + fi +} + +#----------------------------------------------------------------------------- +# Service-Specific Validations +#----------------------------------------------------------------------------- + +validate_all_services() { + section "Service Validations" + + # Core services (always expected) + validate_service "sshd" "enabled" "active" + validate_service "systemd-resolved" "enabled" "active" + validate_service "ufw" "enabled" "" # VM lacks iptables modules, can't be active + validate_service "fail2ban" "enabled" "active" + validate_service "NetworkManager" "enabled" "active" + validate_service "rngd" "enabled" "active" + validate_service "cronie" "enabled" "" + validate_service "atd" "enabled" "" + + # Timer services + validate_service "reflector.timer" "enabled" "" + validate_service "paccache.timer" "enabled" "" + + # Optional services (warn if missing, don't fail) + validate_service_optional "avahi-daemon" "enabled" + validate_service_optional "bluetooth" "enabled" + validate_service_optional "cups" "enabled" + validate_service_optional "docker" "enabled" + validate_service_optional "tailscaled" "enabled" + # Syncthing uses user service (not system), check lingering is enabled + step "Checking user lingering for syncthing" + local linger_enabled=$(ssh_cmd "ls /var/lib/systemd/linger/cjennings 2>/dev/null && echo yes || echo no") + if [ "$linger_enabled" = "yes" ]; then + validation_pass "User lingering enabled for syncthing user service" + else + validation_warn "User lingering not enabled (syncthing may not autostart)" + fi + + # Filesystem-specific + validate_zfs_services + validate_btrfs_services + + # Functional tests + validate_service_functions +} + +validate_service() { + local service="$1" + local expected_enabled="$2" # "enabled" or "" + local expected_active="$3" # "active" or "" + + step "Checking $service" + + if [ -n "$expected_enabled" ]; then + local enabled=$(ssh_cmd "systemctl is-enabled $service 2>/dev/null || echo disabled") + if [ "$enabled" = "enabled" ]; then + validation_pass "$service is enabled" + else + validation_fail "$service not enabled (got: $enabled)" + attribute_issue "$service not enabled" "archsetup" + return 1 + fi + fi + + if [ -n "$expected_active" ]; then + local active=$(ssh_cmd "systemctl is-active $service 2>/dev/null || echo inactive") + if [ "$active" = "active" ]; then + validation_pass "$service is active" + else + validation_fail "$service not active (got: $active)" + attribute_issue "$service not active" "archsetup" + return 1 + fi + fi + + return 0 +} + +validate_service_optional() { + local service="$1" + local expected_enabled="$2" + + step "Checking optional service: $service" + + local enabled=$(ssh_cmd "systemctl is-enabled $service 2>/dev/null || echo disabled") + if [ "$enabled" = "enabled" ]; then + validation_pass "$service is enabled" + else + validation_warn "$service not enabled (optional)" + fi +} + +validate_zfs_services() { + # Only check if ZFS is installed + if ! ssh_cmd "which zfs" &>> "$LOGFILE"; then + return 0 + fi + + step "Checking ZFS-specific services" + + validate_service_optional "sanoid.timer" "enabled" + + # Check for zfs-scrub timer (pool name varies) + local scrub_enabled + scrub_enabled=$(ssh_cmd "systemctl list-unit-files 'zfs-scrub*' 2>/dev/null | grep -c enabled" | tr -d '[:space:]') + scrub_enabled=${scrub_enabled:-0} + if [ "$scrub_enabled" -gt 0 ]; then + validation_pass "ZFS scrub timer enabled" + else + validation_warn "ZFS scrub timer not found" + fi +} + +validate_btrfs_services() { + # Only check if btrfs root + if ! ssh_cmd "mount | grep 'on / ' | grep -q btrfs"; then + return 0 + fi + + step "Checking btrfs-specific services" + validate_service_optional "grub-btrfsd" "enabled" +} + +validate_service_functions() { + section "Service Functional Tests" + + # UFW functional test + # NOTE: VM environment lacks iptables kernel modules, so UFW cannot activate. + # We only verify it's enabled; active status requires real hardware. + step "Testing UFW functionality" + local ufw_enabled + ufw_enabled=$(ssh_cmd "systemctl is-enabled ufw.service 2>/dev/null || echo disabled") + if [ "$ufw_enabled" = "enabled" ]; then + validation_pass "UFW is enabled (activation requires iptables kernel modules)" + else + validation_fail "UFW not enabled" + attribute_issue "UFW not enabled" "archsetup" + fi + + # fail2ban functional test + step "Testing fail2ban functionality" + if ssh_cmd "fail2ban-client status" &>> "$LOGFILE"; then + validation_pass "fail2ban is responding" + else + validation_fail "fail2ban not responding" + attribute_issue "fail2ban not functioning" "archsetup" + fi + + # DNS resolution test + step "Testing DNS resolution" + if ssh_cmd "resolvectl query archlinux.org" &>> "$LOGFILE"; then + validation_pass "DNS resolution working" + else + validation_warn "DNS resolution test failed (may be network issue)" + fi + + # Docker functional test (if enabled) + if ssh_cmd "systemctl is-enabled docker" &>> "$LOGFILE"; then + step "Testing Docker functionality" + if ssh_cmd "docker info" &>> "$LOGFILE"; then + validation_pass "Docker is responding" + else + validation_warn "Docker enabled but not responding" + fi + fi +} + +#----------------------------------------------------------------------------- +# Developer Tools Validations +#----------------------------------------------------------------------------- + +validate_emacs() { + step "Checking if Emacs is installed" + if ssh_cmd "which emacs" &>> "$LOGFILE"; then + validation_pass "Emacs is installed" + + # Check if config exists + if ssh_cmd "test -d /home/cjennings/.emacs.d"; then + validation_pass "Emacs config directory exists" + + # Check user can access the directory + local result + result=$(ssh_cmd "sudo -u cjennings ls /home/cjennings/.emacs.d > /dev/null 2>&1 && echo OK || echo FAIL") + if [ "$result" = "OK" ]; then + validation_pass "Emacs config readable by user" + else + validation_fail "Emacs config not readable by user (permission issue)" + attribute_issue "Emacs .emacs.d not readable by user" "archsetup" + fi + else + validation_warn "Emacs config directory not found" + fi + else + validation_fail "Emacs not found" + attribute_issue "Emacs not installed" "archsetup" + fi +} + +validate_git_config() { + step "Checking git installation" + if ssh_cmd "which git" &>> "$LOGFILE"; then + validation_pass "git is installed" + else + validation_fail "git not found" + attribute_issue "git not installed" "archsetup" + fi +} + +validate_dev_tools() { + step "Checking developer tools" + local tools="python node npm go rustc" + local missing="" + + for tool in $tools; do + if ! ssh_cmd "which $tool" &>> "$LOGFILE"; then + missing="$missing $tool" + fi + done + + if [ -z "$missing" ]; then + validation_pass "Core dev tools installed" + else + validation_warn "Some dev tools missing:$missing" + fi +} + +#----------------------------------------------------------------------------- +# System Configuration Validations +#----------------------------------------------------------------------------- + +validate_zfs_config() { + step "Checking ZFS configuration (if applicable)" + if ssh_cmd "which zfs" &>> "$LOGFILE"; then + # ZFS is installed, check for sanoid + if ssh_cmd "which sanoid" &>> "$LOGFILE"; then + validation_pass "ZFS with sanoid detected" + else + validation_warn "ZFS detected but sanoid not installed" + fi + else + info "ZFS not installed (non-ZFS system)" + fi +} + +validate_boot_config() { + step "Checking GRUB configuration" + if ssh_cmd "test -f /boot/grub/grub.cfg" &>> "$LOGFILE"; then + validation_pass "GRUB config exists" + else + validation_warn "GRUB config not found (may use different bootloader)" + fi +} + +validate_terminus_font() { + step "Checking terminus-font installation" + if ssh_cmd "pacman -Q terminus-font" &>> "$LOGFILE"; then + validation_pass "terminus-font package installed" + else + validation_fail "terminus-font package not installed" + attribute_issue "terminus-font not installed via pacman" "archsetup" + fi +} + +validate_mkinitcpio_hooks() { + step "Checking mkinitcpio HOOKS configuration" + local hooks=$(ssh_cmd "grep '^HOOKS=' /etc/mkinitcpio.conf") + local is_zfs=$(ssh_cmd "findmnt -n -o FSTYPE / 2>/dev/null") + + if [ "$is_zfs" = "zfs" ]; then + # ZFS system: must use udev, not systemd + if echo "$hooks" | grep -q '\budev\b'; then + validation_pass "ZFS system uses udev hook (correct)" + elif echo "$hooks" | grep -q '\bsystemd\b'; then + validation_fail "ZFS system uses systemd hook (will break boot)" + attribute_issue "mkinitcpio uses systemd hook on ZFS system" "archsetup" + else + validation_warn "Could not determine init hook type" + fi + else + # Non-ZFS: systemd hook is fine + if echo "$hooks" | grep -q '\bsystemd\b'; then + validation_pass "Non-ZFS system uses systemd hook" + elif echo "$hooks" | grep -q '\budev\b'; then + validation_pass "Non-ZFS system uses udev hook" + fi + fi +} + +validate_initramfs_consolefont() { + step "Checking console font in initramfs" + local font_in_initramfs=$(ssh_cmd "lsinitcpio /boot/initramfs-linux*.img 2>/dev/null | grep -c 'consolefont.psf\\|ter-'") + + if [ "${font_in_initramfs:-0}" -gt 0 ]; then + validation_pass "Console font included in initramfs" + else + validation_warn "Console font may not be in initramfs" + fi +} + +validate_nvme_module() { + step "Checking NVMe module configuration" + local has_nvme=$(ssh_cmd "ls /dev/nvme* 2>/dev/null | head -1") + + if [ -n "$has_nvme" ]; then + # System has NVMe drives + local modules=$(ssh_cmd "grep '^MODULES=' /etc/mkinitcpio.conf") + if echo "$modules" | grep -q 'nvme'; then + validation_pass "NVMe module in mkinitcpio MODULES" + else + validation_warn "NVMe system but nvme not in MODULES (may cause slow boot)" + fi + else + info "No NVMe drives detected" + fi +} + +validate_autologin_config() { + step "Checking autologin configuration" + if ssh_cmd "test -f /etc/systemd/system/getty@tty1.service.d/autologin.conf" &>> "$LOGFILE"; then + validation_pass "Autologin configured" + else + info "Autologin not configured (may be intentional)" + fi +} + +validate_gnome_keyring_setup() { + step "Checking gnome-keyring pre-configuration" + local keyring_dir="/home/cjennings/.local/share/keyrings" + + # Check directory exists + if ! ssh_cmd "test -d $keyring_dir"; then + validation_fail "Keyring directory not created" + attribute_issue "gnome-keyring directory not pre-created" "archsetup" + return 1 + fi + + # Check directory permissions (should be 700) + local perms=$(ssh_cmd "stat -c '%a' $keyring_dir") + if [ "$perms" != "700" ]; then + validation_fail "Keyring directory has wrong permissions: $perms (expected 700)" + attribute_issue "gnome-keyring directory wrong permissions" "archsetup" + return 1 + fi + + # Check ownership + local owner=$(ssh_cmd "stat -c '%U' $keyring_dir") + if [ "$owner" != "cjennings" ]; then + validation_fail "Keyring directory owned by $owner (expected cjennings)" + attribute_issue "gnome-keyring directory wrong ownership" "archsetup" + return 1 + fi + + # Check default file exists and contains "login" + local default_keyring=$(ssh_cmd "cat $keyring_dir/default 2>/dev/null") + if [ "$default_keyring" != "login" ]; then + validation_fail "Default keyring not set to 'login' (got: '$default_keyring')" + attribute_issue "gnome-keyring default not set to login" "archsetup" + return 1 + fi + + validation_pass "gnome-keyring pre-configured (default=login, dir=700)" +} + +#----------------------------------------------------------------------------- +# Archsetup-Specific Validations +#----------------------------------------------------------------------------- + +validate_archsetup_log() { + step "Checking archsetup log for errors" + local error_count + # Use grep -h to suppress filenames, then wc -l to count total matches + error_count=$(ssh_cmd "grep -h '^Error:' /var/log/archsetup-*.log 2>/dev/null | wc -l" | tr -d '[:space:]') + error_count=${error_count:-0} + + if [ "$error_count" = "0" ]; then + validation_pass "No errors in archsetup log" + else + validation_fail "Found $error_count errors in archsetup log" + attribute_issue "Errors in archsetup log: $error_count" "archsetup" + fi +} + +validate_state_markers() { + step "Checking archsetup state markers" + local state_count=$(ssh_cmd "ls /var/lib/archsetup/state/ 2>/dev/null | wc -l") + + if [ "$state_count" -ge 12 ]; then + validation_pass "All 12 installation steps completed" + else + validation_warn "Only $state_count/12 steps completed" + fi +} + +#============================================================================= +# ISSUE REPORTING +#============================================================================= + +generate_issue_report() { + local output_dir="$1" + local archzfs_inbox="$2" + + section "Issue Attribution Report" + + local report_file="$output_dir/issue-report.txt" + + cat > "$report_file" << EOF +======================================== +Issue Attribution Report +Generated: $(date +'%Y-%m-%d %H:%M:%S') +======================================== + +VALIDATION SUMMARY +------------------ +Passed: $VALIDATION_PASSED +Failed: $VALIDATION_FAILED +Warnings: $VALIDATION_WARNINGS + +EOF + + if [ ${#ARCHSETUP_ISSUES[@]} -gt 0 ]; then + echo "ARCHSETUP ISSUES (${#ARCHSETUP_ISSUES[@]})" >> "$report_file" + echo "-------------------------------------------" >> "$report_file" + for issue in "${ARCHSETUP_ISSUES[@]}"; do + echo " - $issue" >> "$report_file" + done + echo "" >> "$report_file" + + error "Found ${#ARCHSETUP_ISSUES[@]} archsetup issues" + fi + + if [ ${#BASE_INSTALL_ISSUES[@]} -gt 0 ]; then + echo "BASE INSTALL ISSUES (${#BASE_INSTALL_ISSUES[@]})" >> "$report_file" + echo "-------------------------------------------" >> "$report_file" + for issue in "${BASE_INSTALL_ISSUES[@]}"; do + echo " - $issue" >> "$report_file" + done + echo "" >> "$report_file" + + warn "Found ${#BASE_INSTALL_ISSUES[@]} base install issues" + + # If archzfs inbox provided, create issue files + if [ -n "$archzfs_inbox" ] && [ -d "$archzfs_inbox" ]; then + local issue_file="$archzfs_inbox/$(date +'%Y-%m-%d')-test-issues.txt" + echo "Base install issues from archsetup test run:" > "$issue_file" + echo "Date: $(date +'%Y-%m-%d %H:%M:%S')" >> "$issue_file" + echo "" >> "$issue_file" + for issue in "${BASE_INSTALL_ISSUES[@]}"; do + echo "- $issue" >> "$issue_file" + done + info "Created archzfs issue file: $issue_file" + fi + fi + + if [ ${#UNKNOWN_ISSUES[@]} -gt 0 ]; then + echo "UNKNOWN/UNATTRIBUTED ISSUES (${#UNKNOWN_ISSUES[@]})" >> "$report_file" + echo "-------------------------------------------" >> "$report_file" + for issue in "${UNKNOWN_ISSUES[@]}"; do + echo " - $issue" >> "$report_file" + done + echo "" >> "$report_file" + + warn "Found ${#UNKNOWN_ISSUES[@]} unattributed issues" + fi + + if [ ${#ARCHSETUP_ISSUES[@]} -eq 0 ] && [ ${#BASE_INSTALL_ISSUES[@]} -eq 0 ] && [ ${#UNKNOWN_ISSUES[@]} -eq 0 ]; then + echo "No issues found!" >> "$report_file" + success "No issues found!" + fi + + info "Issue report saved: $report_file" +} + +#============================================================================= +# MAIN VALIDATION ENTRY POINT +#============================================================================= + +run_full_validation() { + local output_dir="$1" + local archzfs_inbox="${2:-}" + + run_all_validations + analyze_log_diff "$output_dir" + generate_issue_report "$output_dir" "$archzfs_inbox" + + # Return success if no failures + [ $VALIDATION_FAILED -eq 0 ] +} diff --git a/scripts/testing/lib/vm-utils.sh b/scripts/testing/lib/vm-utils.sh new file mode 100755 index 0000000..81aec33 --- /dev/null +++ b/scripts/testing/lib/vm-utils.sh @@ -0,0 +1,321 @@ +#!/bin/bash +# VM management utilities for archsetup testing +# Author: Craig Jennings +# License: GNU GPLv3 + +# 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 +} + +# 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 +} + +# Check if KVM is available +check_kvm() { + if [ ! -e /dev/kvm ]; then + error "KVM is not available" + info "Check if virtualization is enabled in BIOS" + info "Load kvm module: sudo modprobe kvm-intel (or kvm-amd)" + return 1 + fi + 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 + + progress "Waiting for VM $vm_name to boot..." + + 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 + + error "Timeout waiting for VM $vm_name to boot" + return 1 +} + +# Check if VM exists +vm_exists() { + local vm_name="$1" + virsh --connect "$LIBVIRT_URI" dominfo "$vm_name" &>/dev/null + return $? +} + +# Check if VM is running +vm_is_running() { + local vm_name="$1" + [ "$(virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null)" = "running" ] + return $? +} + +# Start VM +start_vm() { + local vm_name="$1" + + if vm_is_running "$vm_name"; then + warn "VM $vm_name is already running" + 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" + return 1 + fi +} + +# Stop VM gracefully +stop_vm() { + local vm_name="$1" + local timeout="${2:-60}" + + if ! vm_is_running "$vm_name"; then + info "VM $vm_name is not running" + return 0 + 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 + fi + + success "VM $vm_name stopped" + return 0 +} + +# Destroy VM (force stop) +destroy_vm() { + local vm_name="$1" + + if ! vm_exists "$vm_name"; then + info "VM $vm_name does not exist" + 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 + fi + + virsh --connect "$LIBVIRT_URI" undefine "$vm_name" --nvram >> "$LOGFILE" 2>&1 + success "VM $vm_name destroyed" + return 0 +} + +# Create snapshot +create_snapshot() { + local vm_name="$1" + local snapshot_name="$2" + + 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" + return 0 + else + error "Failed to create snapshot $snapshot_name" + return 1 + fi +} + +# Restore snapshot +restore_snapshot() { + local vm_name="$1" + local snapshot_name="$2" + + 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" + return 0 + else + error "Failed to restore snapshot $snapshot_name" + return 1 + fi +} + +# Delete snapshot +delete_snapshot() { + local vm_name="$1" + local snapshot_name="$2" + + 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" + return 0 + else + 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 +} + +# Get VM IP address (requires guest agent or DHCP lease) +get_vm_ip() { + local vm_name="$1" + + # Try guest agent first + local ip + ip=$(virsh --connect "$LIBVIRT_URI" domifaddr "$vm_name" 2>/dev/null | grep -oP '(\d+\.){3}\d+' | head -1) + + 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) + + 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" + return 0 + fi + fi + + return 1 +} + +# Execute command in VM via SSH +vm_exec() { + local vm_name="$1" + 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" +} + +# 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 + + 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 + success "File copied to VM" + return 0 + else + error "Failed to copy file to VM" + return 1 + fi +} + +# 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 + + 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 + success "File copied from VM" + return 0 + else + error "Failed to copy file from VM" + return 1 + fi +} diff --git a/scripts/testing/run-test-baremetal.sh b/scripts/testing/run-test-baremetal.sh new file mode 100755 index 0000000..c108e6f --- /dev/null +++ b/scripts/testing/run-test-baremetal.sh @@ -0,0 +1,310 @@ +#!/bin/bash +# Run archsetup test on bare metal ZFS system +# Author: Craig Jennings +# License: GNU GPLv3 +# +# This script: +# 1. Connects to bare metal ZFS system via SSH +# 2. Optionally rolls back to genesis snapshots first +# 3. Transfers archsetup +# 4. Executes archsetup +# 5. Captures logs and validates results +# 6. Can rollback to genesis if test fails + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source utilities +source "$SCRIPT_DIR/lib/logging.sh" +source "$SCRIPT_DIR/lib/validation.sh" + +# Parse arguments +ROLLBACK_FIRST=false +ROLLBACK_AFTER=false +TARGET_HOST="" +ROOT_PASSWORD="" + +usage() { + echo "Usage: $0 --host --password [options]" + echo "" + echo "Required:" + echo " --host Target bare metal host (e.g., ratio.local)" + echo " --password Root password for SSH" + echo "" + echo "Options:" + echo " --rollback-first Roll back to genesis snapshots before running" + echo " --rollback-after Roll back to genesis snapshots after test (cleanup)" + echo " --validate-only Skip archsetup, only run validation checks" + echo " --help Show this help" + exit 1 +} + +VALIDATE_ONLY=false + +while [[ $# -gt 0 ]]; do + case $1 in + --host) + TARGET_HOST="$2" + shift 2 + ;; + --password) + ROOT_PASSWORD="$2" + shift 2 + ;; + --rollback-first) + ROLLBACK_FIRST=true + shift + ;; + --rollback-after) + ROLLBACK_AFTER=true + shift + ;; + --validate-only) + VALIDATE_ONLY=true + shift + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# Validate required args +if [ -z "$TARGET_HOST" ] || [ -z "$ROOT_PASSWORD" ]; then + echo "Error: --host and --password are required" + usage +fi + +# Configuration +TIMESTAMP=$(date +'%Y%m%d-%H%M%S') +TEST_RESULTS_DIR="$PROJECT_ROOT/test-results/baremetal-$TIMESTAMP" +ARCHZFS_INBOX="$HOME/code/archzfs/inbox" + +# Override VM_IP for validation.sh ssh_cmd function +VM_IP="$TARGET_HOST" + +# Initialize logging +mkdir -p "$TEST_RESULTS_DIR" +LOGFILE="$TEST_RESULTS_DIR/test.log" +init_logging "$LOGFILE" + +section "Bare Metal Test Run: $TIMESTAMP" +info "Target: $TARGET_HOST" + +# Test SSH connectivity +step "Testing SSH connectivity to $TARGET_HOST" +if ! sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=10 "root@$TARGET_HOST" "echo connected" &>/dev/null; then + fatal "Cannot connect to $TARGET_HOST via SSH" +fi +success "SSH connection OK" + +# Check it's a ZFS system +step "Verifying ZFS root" +if ! ssh_cmd "zfs list zroot" &>> "$LOGFILE"; then + fatal "Target is not a ZFS system (no zroot pool)" +fi +success "ZFS root confirmed" + +# Rollback to genesis if requested +if $ROLLBACK_FIRST; then + section "Rolling Back to Genesis Snapshots" + + step "Getting list of datasets with genesis snapshots" + DATASETS=$(ssh_cmd "zfs list -H -o name -t snapshot | grep '@genesis$' | sed 's/@genesis$//'") + + step "Rolling back all datasets to genesis" + for ds in $DATASETS; do + info "Rolling back $ds@genesis" + if ! ssh_cmd "zfs rollback -r $ds@genesis" &>> "$LOGFILE"; then + warn "Failed to rollback $ds@genesis" + fi + done + success "Rollback complete" + + # Need to reconnect after rollback + sleep 5 + step "Reconnecting after rollback" + if ! ssh_cmd "echo reconnected" &>/dev/null; then + fatal "Lost connection after rollback" + fi + success "Reconnected" +fi + +if ! $VALIDATE_ONLY; then + # Capture pre-install state + capture_pre_install_state "$TEST_RESULTS_DIR" + + # Transfer archsetup + section "Transferring ArchSetup" + + step "Creating git bundle" + BUNDLE_FILE=$(mktemp) + git -C "$PROJECT_ROOT" bundle create "$BUNDLE_FILE" HEAD >> "$LOGFILE" 2>&1 + + step "Transferring to $TARGET_HOST" + ssh_cmd "rm -rf /tmp/archsetup-test && mkdir -p /tmp/archsetup-test" + sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "$BUNDLE_FILE" "root@$TARGET_HOST:/tmp/archsetup.bundle" >> "$LOGFILE" 2>&1 + + step "Extracting on target" + ssh_cmd "cd /tmp && git clone /tmp/archsetup.bundle archsetup-test && rm /tmp/archsetup.bundle" >> "$LOGFILE" 2>&1 + rm -f "$BUNDLE_FILE" + success "ArchSetup transferred" + + # Execute archsetup + section "Executing ArchSetup" + + start_timer "archsetup" + step "Starting archsetup on $TARGET_HOST" + info "This will take 30-60 minutes" + + REMOTE_LOG="/tmp/archsetup-test/archsetup-output.log" + + # Start archsetup in background + ssh_cmd "cd /tmp/archsetup-test && nohup bash archsetup > $REMOTE_LOG 2>&1 &" + success "ArchSetup started in background" + + # Poll for completion + step "Monitoring archsetup progress" + POLL_COUNT=0 + MAX_POLLS=180 # 90 minutes max + + while [ $POLL_COUNT -lt $MAX_POLLS ]; do + if ssh_cmd "ps aux | grep '[b]ash archsetup' > /dev/null" 2>/dev/null; then + sleep 30 + POLL_COUNT=$((POLL_COUNT + 1)) + if [ $((POLL_COUNT % 10)) -eq 0 ]; then + ELAPSED_MINS=$((POLL_COUNT / 2)) + info "Still running... ($ELAPSED_MINS minutes elapsed)" + # Show last line of progress + LAST_LINE=$(ssh_cmd "tail -1 $REMOTE_LOG 2>/dev/null" || echo "") + [ -n "$LAST_LINE" ] && info " $LAST_LINE" + fi + else + break + fi + done + + if [ $POLL_COUNT -ge $MAX_POLLS ]; then + error "ArchSetup timed out after 90 minutes" + ARCHSETUP_EXIT_CODE=124 + else + step "Retrieving archsetup exit status" + if ssh_cmd "grep -q 'ARCHSETUP_EXECUTION_COMPLETE' /var/log/archsetup-*.log 2>/dev/null"; then + ARCHSETUP_EXIT_CODE=0 + success "ArchSetup completed successfully" + else + ARCHSETUP_EXIT_CODE=1 + error "ArchSetup may have encountered errors" + fi + fi + + stop_timer "archsetup" + + # Copy logs + section "Capturing Test Artifacts" + + step "Copying archsetup log" + sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "root@$TARGET_HOST:/var/log/archsetup-*.log" "$TEST_RESULTS_DIR/" 2>> "$LOGFILE" || \ + warn "Could not copy archsetup log" + + step "Copying archsetup output" + sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "root@$TARGET_HOST:$REMOTE_LOG" "$TEST_RESULTS_DIR/archsetup-output.log" 2>> "$LOGFILE" || \ + warn "Could not copy output log" + + # Capture post-install state + capture_post_install_state "$TEST_RESULTS_DIR" +else + info "Skipping archsetup (--validate-only)" + ARCHSETUP_EXIT_CODE=0 + mkdir -p "$TEST_RESULTS_DIR/pre-install" "$TEST_RESULTS_DIR/post-install" +fi + +# Run validations +run_all_validations +validate_all_services + +# Additional ZFS-specific validations +section "ZFS-Specific Validations" +validate_zfs_services + +# Analyze logs if we ran archsetup +if ! $VALIDATE_ONLY; then + analyze_log_diff "$TEST_RESULTS_DIR" +fi + +# Generate reports +generate_issue_report "$TEST_RESULTS_DIR" "$ARCHZFS_INBOX" + +# Set validation result +if [ $VALIDATION_FAILED -eq 0 ]; then + VALIDATION_PASSED=true +else + VALIDATION_PASSED=false +fi + +# Generate test report +section "Generating Test Report" + +REPORT_FILE="$TEST_RESULTS_DIR/test-report.txt" +cat > "$REPORT_FILE" << EOFREPORT +======================================== +Bare Metal ArchSetup Test Report +======================================== + +Test ID: $TIMESTAMP +Date: $(date +'%Y-%m-%d %H:%M:%S') +Target: $TARGET_HOST +Test Method: Bare Metal ZFS + +Results: + ArchSetup Exit Code: $ARCHSETUP_EXIT_CODE + Validation: $(if $VALIDATION_PASSED; then echo "PASSED"; else echo "FAILED"; fi) + +Validation Summary: + Passed: $VALIDATION_PASSED_COUNT + Failed: $VALIDATION_FAILED + Warnings: $VALIDATION_WARNINGS + +Artifacts: + Log file: $LOGFILE + Report: $REPORT_FILE + Results: $TEST_RESULTS_DIR/ + +EOFREPORT + +info "Test report saved: $REPORT_FILE" + +# Rollback after if requested +if $ROLLBACK_AFTER; then + section "Rolling Back to Genesis (cleanup)" + + DATASETS=$(ssh_cmd "zfs list -H -o name -t snapshot | grep '@genesis$' | sed 's/@genesis$//'") + for ds in $DATASETS; do + info "Rolling back $ds@genesis" + ssh_cmd "zfs rollback -r $ds@genesis" &>> "$LOGFILE" || true + done + success "Rollback complete" +fi + +# Final summary +section "Test Complete" + +if [ $ARCHSETUP_EXIT_CODE -eq 0 ] && $VALIDATION_PASSED; then + success "TEST PASSED" + exit 0 +else + error "TEST FAILED" + info "Check logs in: $TEST_RESULTS_DIR" + exit 1 +fi diff --git a/scripts/testing/run-test.sh b/scripts/testing/run-test.sh new file mode 100755 index 0000000..4c41cc3 --- /dev/null +++ b/scripts/testing/run-test.sh @@ -0,0 +1,367 @@ +#!/bin/bash +# Run archsetup test in a VM using snapshots +# Author: Craig Jennings +# License: GNU GPLv3 +# +# This script: +# 1. Reverts base VM to clean snapshot +# 2. Starts the base VM +# 3. Transfers archsetup and dotfiles +# 4. Executes archsetup in the VM +# 5. Captures logs and validates results +# 6. Generates test report +# 7. Reverts to clean snapshot for next run + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source utilities +source "$SCRIPT_DIR/lib/logging.sh" +source "$SCRIPT_DIR/lib/vm-utils.sh" +source "$SCRIPT_DIR/lib/network-diagnostics.sh" +source "$SCRIPT_DIR/lib/validation.sh" + +# Parse arguments +KEEP_VM=false +ARCHSETUP_SCRIPT="$PROJECT_ROOT/archsetup" +SNAPSHOT_NAME="clean-install" + +while [[ $# -gt 0 ]]; do + case $1 in + --keep) + KEEP_VM=true + shift + ;; + --script) + ARCHSETUP_SCRIPT="$2" + shift 2 + ;; + --snapshot) + SNAPSHOT_NAME="$2" + shift 2 + ;; + *) + echo "Usage: $0 [--keep] [--script /path/to/archsetup] [--snapshot name]" + echo " --keep Keep VM in post-test state (for debugging)" + echo " --script Specify custom archsetup script to test" + echo " --snapshot Snapshot name to revert to (default: clean-install)" + exit 1 + ;; + esac +done + +# Configuration +TIMESTAMP=$(date +'%Y%m%d-%H%M%S') +VM_NAME="archsetup-base" +TEST_RESULTS_DIR="$PROJECT_ROOT/test-results/$TIMESTAMP" +ROOT_PASSWORD="archsetup" +ARCHZFS_INBOX="$HOME/code/archzfs/inbox" + +# Initialize logging +mkdir -p "$TEST_RESULTS_DIR" +LOGFILE="$TEST_RESULTS_DIR/test.log" +init_logging "$LOGFILE" + +section "ArchSetup Test Run: $TIMESTAMP" + +# Verify archsetup script exists +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" + info "Create it first: ./scripts/testing/create-base-vm.sh" +fi + +# Check if snapshot exists +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" + info "Available snapshots:" + virsh --connect "$LIBVIRT_URI" snapshot-list "$VM_NAME" 2>/dev/null || info " (none)" + info "" + info "Create snapshot with:" + info " virsh snapshot-create-as $VM_NAME $SNAPSHOT_NAME --description 'Clean Arch install'" +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 + +# Revert to clean snapshot +step "Reverting to snapshot: $SNAPSHOT_NAME" +if restore_snapshot "$VM_NAME" "$SNAPSHOT_NAME"; then + success "Reverted to clean state" +else + fatal "Failed to revert snapshot" +fi + +# Start VM +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" +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 + fatal "Network diagnostics failed - aborting test" +fi + +# Capture pre-install state for comparison +capture_pre_install_state "$TEST_RESULTS_DIR" + +# Transfer files to VM (simulating git clone) +section "Simulating Git Clone" + +step "Creating shallow git clone on VM" +info "This simulates: git clone --depth 1 /home/cjennings/code/archsetup" + +# Create a temporary git bundle from current repo +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 + +sshpass -p "$ROOT_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "$BUNDLE_FILE" "root@$VM_IP:/tmp/archsetup.bundle" >> "$LOGFILE" 2>&1 + +# 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 + +rm -f "$BUNDLE_FILE" +success "Repository cloned to VM (simulating git clone --depth 1)" + +# 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) +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 \$!" \ + >> "$LOGFILE" 2>&1 + +if [ $? -ne 0 ]; then + fatal "Failed to start archsetup on VM" +fi + +success "ArchSetup started in background on VM" + +# Poll for completion +step "Monitoring archsetup progress (polling every 30 seconds)..." +POLL_COUNT=0 +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 + # Still running, wait and continue + sleep 30 + POLL_COUNT=$((POLL_COUNT + 1)) + + # Show progress every 5 minutes + if [ $((POLL_COUNT % 10)) -eq 0 ]; then + ELAPSED_MINS=$((POLL_COUNT / 2)) + info "Still running... ($ELAPSED_MINS minutes elapsed)" + fi + else + # Process finished + break + fi +done + +if [ $POLL_COUNT -ge $MAX_POLLS ]; then + error "ArchSetup timed out after 90 minutes" + ARCHSETUP_EXIT_CODE=124 +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) + + if [ "$ARCHSETUP_EXIT_CODE" = "0" ]; then + success "ArchSetup completed successfully" + else + error "ArchSetup may have encountered errors (check logs)" + fi +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" || \ + warn "Could not copy remote output log" + +# Append remote output to main test log +if [ -f "$TEST_RESULTS_DIR/archsetup-output.log" ]; then + cat "$TEST_RESULTS_DIR/archsetup-output.log" >> "$LOGFILE" +fi + +stop_timer "archsetup" + +# Capture logs and artifacts from VM +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" || \ + 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" || \ + warn "Could not copy package lists" + +# Capture post-install state +capture_post_install_state "$TEST_RESULTS_DIR" + +# Run comprehensive validation +# This uses the validation.sh library for all checks +run_all_validations +validate_all_services + +# Analyze log differences (pre vs post install) +analyze_log_diff "$TEST_RESULTS_DIR" + +# Generate issue attribution report +# If base install issues found and archzfs inbox exists, create issue file +generate_issue_report "$TEST_RESULTS_DIR" "$ARCHZFS_INBOX" + +# Set validation result based on failure count +if [ $VALIDATION_FAILED -eq 0 ]; then + VALIDATION_PASSED=true +else + VALIDATION_PASSED=false +fi + +# Generate test report +section "Generating Test Report" + +REPORT_FILE="$TEST_RESULTS_DIR/test-report.txt" +cat > "$REPORT_FILE" << EOFREPORT +======================================== +ArchSetup Test Report +======================================== + +Test ID: $TIMESTAMP +Date: $(date +'%Y-%m-%d %H:%M:%S') +Test Method: Snapshot-based + +VM Configuration: + Name: $VM_NAME + IP: $VM_IP + Snapshot: $SNAPSHOT_NAME + +Results: + ArchSetup Exit Code: $ARCHSETUP_EXIT_CODE + Validation: $(if $VALIDATION_PASSED; then echo "PASSED"; else echo "FAILED"; fi) + +Validation Summary: + Passed: $VALIDATION_PASSED + Failed: $VALIDATION_FAILED + Warnings: $VALIDATION_WARNINGS + +Issue Attribution: + ArchSetup Issues: ${#ARCHSETUP_ISSUES[@]} + Base Install Issues: ${#BASE_INSTALL_ISSUES[@]} + Unknown Issues: ${#UNKNOWN_ISSUES[@]} + +Artifacts: + Log file: $LOGFILE + Report: $REPORT_FILE + Results: $TEST_RESULTS_DIR/ + Issue Report: $TEST_RESULTS_DIR/issue-report.txt + Pre-install logs: $TEST_RESULTS_DIR/pre-install/ + Post-install logs: $TEST_RESULTS_DIR/post-install/ + Analysis: $TEST_RESULTS_DIR/analysis/ + +EOFREPORT + +info "Test report saved: $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 "" + info "To revert to clean state when done:" + info " virsh shutdown $VM_NAME" + info " virsh snapshot-revert $VM_NAME $SNAPSHOT_NAME" +else + step "Shutting down VM and reverting to clean snapshot" + stop_vm "$VM_NAME" + if restore_snapshot "$VM_NAME" "$SNAPSHOT_NAME"; then + success "VM reverted to clean state" + else + warn "Failed to revert snapshot - VM may be in modified state" + fi +fi + +# Final summary +section "Test Complete" + +if [ $ARCHSETUP_EXIT_CODE -eq 0 ] && $VALIDATION_PASSED; then + success "TEST PASSED" + exit 0 +else + error "TEST FAILED" + info "Check logs in: $TEST_RESULTS_DIR" + exit 1 +fi diff --git a/scripts/testing/setup-testing-env.sh b/scripts/testing/setup-testing-env.sh new file mode 100755 index 0000000..e682553 --- /dev/null +++ b/scripts/testing/setup-testing-env.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# Setup testing environment for archsetup +# Author: Craig Jennings +# 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 +# - Verifies KVM support +# - Creates directories for test artifacts + +set -e + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Source utilities +source "$SCRIPT_DIR/lib/logging.sh" + +# Initialize logging +LOGFILE="$PROJECT_ROOT/test-results/setup-$(date +'%Y%m%d-%H%M%S').log" +init_logging "$LOGFILE" + +section "ArchSetup Testing Environment Setup" + +# Check if running on Arch Linux +if [ ! -f /etc/arch-release ]; then + fatal "This script is designed for Arch Linux" +fi + +# Check if user has sudo +if ! sudo -n true 2>/dev/null; then + warn "This script requires sudo access" + info "You may be prompted for your password" +fi + +# Install required packages +section "Installing Required Packages" + +PACKAGES=( + qemu-full + libvirt + virt-manager + dnsmasq + bridge-utils + iptables + virt-install + libguestfs +) + +for pkg in "${PACKAGES[@]}"; do + if pacman -Qi "$pkg" &>/dev/null; then + info "$pkg is already installed" + else + step "Installing $pkg" + if sudo pacman -S --noconfirm "$pkg" >> "$LOGFILE" 2>&1; then + success "$pkg installed" + else + error "Failed to install $pkg" + fatal "Package installation failed" + fi + 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" + +if [ -e /dev/kvm ]; then + success "KVM is available" +else + error "KVM is not available" + info "Check if virtualization is enabled in BIOS" + info "Load kvm module: sudo modprobe kvm-intel (or kvm-amd)" + fatal "KVM not available" +fi + +# Check which KVM module is loaded +if lsmod | grep -q kvm_intel; then + info "Using Intel KVM" +elif lsmod | grep -q kvm_amd; then + info "Using AMD KVM" +else + warn "No KVM module detected" + info "Load with: sudo modprobe kvm-intel (or kvm-amd)" +fi + +# Create directory structure +section "Creating Directory Structure" + +DIRS=( + "$PROJECT_ROOT/vm-images" + "$PROJECT_ROOT/test-results" +) + +for dir in "${DIRS[@]}"; do + if [ -d "$dir" ]; then + info "Directory exists: $dir" + else + step "Creating directory: $dir" + if mkdir -p "$dir" 2>> "$LOGFILE"; then + success "Directory created: $dir" + else + error "Failed to create directory: $dir" + fi + 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 "" +info "Log file: $LOGFILE" diff --git a/scripts/wip-bootcandy.sh b/scripts/wip-bootcandy.sh new file mode 100644 index 0000000..998cb2d --- /dev/null +++ b/scripts/wip-bootcandy.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Craig Jennings +# installs boot animation and a graphical login manager + +# +# Boot Manager +# + + +sudo pacman -S --noconfirm ly + +sudo systemctl enable ly.service + +# If you need to switch between ttys after Ly's start you also have to +# disable getty on Ly's tty to prevent "login" from spawning on top. + +sudo systemctl disable getty@tty2.service diff --git a/scripts/wireguard-proton.sh b/scripts/wireguard-proton.sh new file mode 100755 index 0000000..564a9f8 --- /dev/null +++ b/scripts/wireguard-proton.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# https://account.protonvpn.com/downloads + +sudo pacman -S --noconfirm wireguard-tools systemd-resolved + +# copy all Proton Wireguard config files +sudo cp ../assets/wireguard/*.conf /etc/wireguard/ + +sudo systemctl enable systemd-resolved +sudo systemctl start systemd-resolved + +sudo wg-quick up USGA +sudo wg diff --git a/scripts/zfs-replicate b/scripts/zfs-replicate new file mode 100755 index 0000000..cf946f1 --- /dev/null +++ b/scripts/zfs-replicate @@ -0,0 +1,72 @@ +#!/bin/bash +# zfs-replicate - Replicate ZFS datasets to TrueNAS +# +# Usage: +# zfs-replicate # Replicate all configured datasets +# zfs-replicate [dataset] # Replicate specific dataset + +set -e + +# TrueNAS Configuration +# Try local network first, fall back to tailscale +TRUENAS_LOCAL="truenas.local" +TRUENAS_TAILSCALE="truenas" +TRUENAS_USER="root" +TRUENAS_POOL="vault" +BACKUP_PATH="backups" # TODO: Configure actual path + +# Datasets to replicate +DATASETS="zroot/ROOT/default zroot/home zroot/media zroot/vms" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +command -v syncoid >/dev/null 2>&1 || error "syncoid not found. Install sanoid package." + +# Determine which host to use +determine_host() { + if ping -c 1 -W 2 "$TRUENAS_LOCAL" &>/dev/null; then + echo "$TRUENAS_LOCAL" + elif ping -c 1 -W 2 "$TRUENAS_TAILSCALE" &>/dev/null; then + echo "$TRUENAS_TAILSCALE" + else + error "Cannot reach TrueNAS at $TRUENAS_LOCAL or $TRUENAS_TAILSCALE" + fi +} + +TRUENAS_HOST=$(determine_host) +info "Using TrueNAS host: $TRUENAS_HOST" + +# Single dataset mode +if [[ -n "$1" ]]; then + dataset="$1" + dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}" + info "Replicating $dataset -> $dest" + syncoid --recursive "$dataset" "$dest" + exit 0 +fi + +# Full replication +info "Starting ZFS replication to $TRUENAS_HOST" +echo "" + +for dataset in $DATASETS; do + dest="$TRUENAS_USER@$TRUENAS_HOST:$TRUENAS_POOL/$BACKUP_PATH/${dataset#zroot/}" + info "Replicating $dataset -> $dest" + + if syncoid --recursive "$dataset" "$dest"; then + info " Success" + else + warn " Failed (will retry next run)" + fi + echo "" +done + +info "Replication complete." -- cgit v1.2.3