#!/usr/bin/env bash # Boot veilor-os ISO in KVM/QEMU under UEFI. # Usage: # ./test/run-vm.sh # boots latest ISO from build/out # ./test/run-vm.sh path/to.iso # specific ISO # SECBOOT=1 ./test/run-vm.sh # use OVMF Secure Boot firmware # FRESH=1 ./test/run-vm.sh # wipe disk + nvram, re-install from scratch # NO_INJECT=1 ./test/run-vm.sh # skip SSH-key auto-injection # # SSH-key auto-injection (chosen approach: dual — cloud-init NoCloud + QEMU # monitor sendkey fallback) # ------------------------------------------------------------------ # Goal: previously each test required logging in at the QEMU console and # running `passwd -d liveuser`, editing sshd_config, etc. before # `ssh -p 2222 liveuser@localhost` worked. This script eliminates that. # # Primary path (works for the *installed* system, not the live image): # * Detect host pubkey at ~/.ssh/id_ed25519.pub or ~/.ssh/id_rsa.pub # * Build a NoCloud cloud-init ISO (user-data + meta-data) via mkisofs/xorriso # * Mount it as a second virtual cdrom — Anaconda/cloud-init picks it up # automatically when installing because the seed has the magic # `cidata` volume label. # # Fallback path (works for the *live* image, which doesn't run cloud-init by # default — dracut-live + livesys-scripts mount squashfs read-only and skip # cloud-init.target): # * Open a QEMU monitor unix socket (-monitor unix:...). # * After ~90s (long enough for SDDM autologin → liveuser), background a # helper that pipes a sequence of `sendkey` events to the monitor: # Ctrl+Alt+F2 (drop to TTY) # "sudo passwd -d liveuser && sudo systemctl reload sshd\n" # This unblocks SSH on port 2222 without manual interaction. # # Both paths are best-effort; if the host has no pubkey, both are skipped # and the script behaves exactly as before. set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" TEST_DIR="$REPO_ROOT/test" DISK="$TEST_DIR/veilor-vm.qcow2" NVRAM="$TEST_DIR/veilor-vm.nvram" SEED_ISO="$TEST_DIR/cloud-init-seed.iso" MONITOR_SOCK="$TEST_DIR/veilor-vm.monitor.sock" ISO="${1:-$(ls -t "$REPO_ROOT"/build/out/*.iso 2>/dev/null | head -1)}" [[ -n ${ISO:-} && -f $ISO ]] || { echo "[ERR] No ISO found. Build first: ./build/build-iso.sh"; exit 1; } # OVMF firmware selection if [[ "${SECBOOT:-0}" == "1" ]]; then OVMF_CODE=/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd OVMF_VARS_SRC=/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd NVRAM="$TEST_DIR/veilor-vm.nvram.secboot" else OVMF_CODE=/usr/share/edk2/ovmf/OVMF_CODE.fd OVMF_VARS_SRC=/usr/share/edk2/ovmf/OVMF_VARS.fd fi # Reset on FRESH=1 if [[ "${FRESH:-0}" == "1" ]]; then rm -f "$DISK" "$NVRAM" "$SEED_ISO" fi # Provision disk + per-VM nvram once [[ -f $DISK ]] || qemu-img create -f qcow2 "$DISK" 40G [[ -f $NVRAM ]] || cp "$OVMF_VARS_SRC" "$NVRAM" # ── Locate host SSH pubkey (ed25519 preferred, rsa fallback) ── HOST_PUBKEY="" if [[ "${NO_INJECT:-0}" != "1" ]]; then for cand in "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_rsa.pub"; do if [[ -f $cand ]]; then HOST_PUBKEY="$(< "$cand")" echo "[INFO] using host pubkey: $cand" break fi done fi # ── Build cloud-init NoCloud seed ISO (primary path) ── SEED_ARGS=() if [[ -n $HOST_PUBKEY ]]; then SEED_DIR="$(mktemp -d)" trap 'rm -rf "$SEED_DIR"' EXIT cat > "$SEED_DIR/meta-data" < "$SEED_DIR/user-data" </dev/null 2>&1; then mkisofs -quiet -output "$SEED_ISO" \ -volid cidata -joliet -rock \ "$SEED_DIR/user-data" "$SEED_DIR/meta-data" elif command -v xorriso >/dev/null 2>&1; then xorriso -as mkisofs -quiet -output "$SEED_ISO" \ -volid cidata -joliet -rock \ "$SEED_DIR/user-data" "$SEED_DIR/meta-data" elif command -v cloud-localds >/dev/null 2>&1; then cloud-localds "$SEED_ISO" "$SEED_DIR/user-data" "$SEED_DIR/meta-data" else echo "[WARN] no mkisofs/xorriso/cloud-localds — skipping cloud-init seed" SEED_ISO="" fi if [[ -n $SEED_ISO && -f $SEED_ISO ]]; then echo "[INFO] cloud-init seed ISO: $SEED_ISO" SEED_ARGS=(-drive "file=$SEED_ISO,media=cdrom,readonly=on") fi fi # ── QEMU monitor fallback (live ISO doesn't run cloud-init) ── # Started in the background after a delay; sends keypresses through the # QEMU monitor unix socket to drop to a TTY and unblock SSH for liveuser. MONITOR_ARGS=() if [[ -n $HOST_PUBKEY ]]; then rm -f "$MONITOR_SOCK" MONITOR_ARGS=(-monitor "unix:$MONITOR_SOCK,server,nowait") ( # Wait for the VM to reach a usable login prompt (SDDM autologin → # liveuser session is the most realistic target). 90s is enough on # KVM/4 vCPUs; tune via VM_BOOT_DELAY if needed. sleep "${VM_BOOT_DELAY:-90}" [[ -S $MONITOR_SOCK ]] || exit 0 # send_chord [key2 ...] — chord released between calls send_chord() { local IFS='-' local chord="$*" printf 'sendkey %s\n' "$chord" } # send_str — only ASCII printable + space + return send_str() { local s="$1" ch local i=0 while (( i < ${#s} )); do ch="${s:i:1}" case "$ch" in ' ') printf 'sendkey spc\n' ;; [a-z0-9]) printf 'sendkey %s\n' "$ch" ;; [A-Z]) printf 'sendkey shift-%s\n' "${ch,,}" ;; '-') printf 'sendkey minus\n' ;; '_') printf 'sendkey shift-minus\n' ;; '/') printf 'sendkey slash\n' ;; '.') printf 'sendkey dot\n' ;; '&') printf 'sendkey shift-7\n' ;; esac i=$((i+1)) done } { send_chord ctrl alt f2 sleep 1 # Type: liveuser (no password by default on live) send_str "liveuser" printf 'sendkey ret\n' sleep 2 send_str "sudo passwd -d liveuser" printf 'sendkey ret\n' sleep 1 send_str "sudo systemctl reload sshd" printf 'sendkey ret\n' } | socat - "UNIX-CONNECT:$MONITOR_SOCK" 2>/dev/null || true ) & INJECT_PID=$! trap 'kill $INJECT_PID 2>/dev/null || true; rm -f "$MONITOR_SOCK"; rm -rf "${SEED_DIR:-}"' EXIT fi echo "════════════════════════════════════════════════════════" echo " veilor-os :: VM test" echo " ISO : $ISO" echo " Disk : $DISK" echo " NVRAM : $NVRAM" echo " Seed : ${SEED_ISO:-}" echo " Mode : ${SECBOOT:+secboot}${SECBOOT:-stock UEFI}" echo " Inject: ${HOST_PUBKEY:+yes}${HOST_PUBKEY:-no (no host pubkey)}" echo "════════════════════════════════════════════════════════" exec qemu-system-x86_64 \ -name veilor-os \ -enable-kvm \ -cpu host \ -smp 4 \ -m 4096 \ -machine q35,smm=on \ -global driver=cfi.pflash01,property=secure,value=on \ -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \ -drive if=pflash,format=raw,file="$NVRAM" \ -drive file="$DISK",if=virtio,format=qcow2,cache=writeback \ -drive file="$ISO",media=cdrom,readonly=on \ "${SEED_ARGS[@]}" \ "${MONITOR_ARGS[@]}" \ -boot menu=on,splash-time=2000 \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 \ -device virtio-rng-pci \ -vga virtio \ -display gtk,gl=on \ -audiodev pa,id=snd0 \ -device intel-hda \ -device hda-output,audiodev=snd0