summaryrefslogtreecommitdiff
path: root/scripts/testing/lib
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-01-27 17:22:55 -0600
committerCraig Jennings <c@cjennings.net>2026-01-27 17:22:55 -0600
commit70bb2d5ab1bf6787bc613e33f5398be2eca1f5fd (patch)
tree0d8cd5057dd32f5f312a7f3534d590b99c2f0f91 /scripts/testing/lib
parent0c6175bfc98f2c5ff2debc665fd8bf91f9171f4e (diff)
feat(testing): rewrite test infrastructure from libvirt to direct QEMU
Replace the never-fully-operational libvirt-based VM test infrastructure with direct QEMU management and archangel ISO for fully automated, unattended base VM creation. Key changes: - vm-utils.sh: complete rewrite — QEMU process mgmt via PID file, monitor socket for graceful shutdown, qemu-img snapshots, SSH port forwarding (localhost:2222) - create-base-vm.sh: boots archangel ISO, SSHs in, runs unattended install via config file, verifies, creates clean-install snapshot - run-test.sh: snapshot revert, git bundle transfer, detached archsetup execution with setsid, polling, validation, and report generation - debug-vm.sh: CoW overlay disk, GTK display, auto-cleanup on close - setup-testing-env.sh: reduced deps to qemu-full/sshpass/edk2-ovmf/socat - cleanup-tests.sh: PID-based process management, orphan detection - validation.sh: port-based SSH (backward compatible), fuzzel/foot for Hyprland, corrected package list paths - network-diagnostics.sh: getent/curl instead of nslookup/ping (SLIRP) New files: - archsetup-test.conf: archangel config for base VM (btrfs, no encrypt) - archsetup-vm.conf: archsetup config for unattended test execution - assets/archangel.conf.example: reference archangel config Deleted: - finalize-base-vm.sh: merged into create-base-vm.sh - archinstall-config.json: replaced by archangel .conf format Tested: full end-to-end run — 51 validations passed, 0 failures. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'scripts/testing/lib')
-rw-r--r--scripts/testing/lib/network-diagnostics.sh24
-rw-r--r--scripts/testing/lib/validation.sh11
-rwxr-xr-xscripts/testing/lib/vm-utils.sh471
3 files changed, 285 insertions, 221 deletions
diff --git a/scripts/testing/lib/network-diagnostics.sh b/scripts/testing/lib/network-diagnostics.sh
index 3f9735b..f7cae11 100644
--- a/scripts/testing/lib/network-diagnostics.sh
+++ b/scripts/testing/lib/network-diagnostics.sh
@@ -3,27 +3,30 @@
# Author: Craig Jennings <craigmartinjennings@gmail.com>
# License: GNU GPLv3
-# Note: logging.sh should already be sourced by the calling script
+# Note: logging.sh and vm-utils.sh should already be sourced by the calling script
+# Uses globals: ROOT_PASSWORD, SSH_PORT, SSH_OPTS, VM_IP (from vm-utils.sh or calling script)
# Run quick network diagnostics
-# Args: $1 = VM IP address or hostname
run_network_diagnostics() {
- local vm_host="$1"
+ local password="${ROOT_PASSWORD:-archsetup}"
+ local port="${SSH_PORT:-22}"
+ local host="${VM_IP:-localhost}"
+ local ssh_base="sshpass -p $password ssh $SSH_OPTS -p $port root@$host"
section "Pre-flight Network Diagnostics"
- # Test 1: Basic connectivity
+ # Test 1: Basic connectivity (use curl instead of ping - SLIRP may not handle ICMP)
step "Testing internet connectivity"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "ping -c 3 8.8.8.8 >/dev/null 2>&1"; then
+ if $ssh_base "curl -s --connect-timeout 5 -o /dev/null http://archlinux.org" 2>/dev/null; then
success "Internet connectivity OK"
else
error "No internet connectivity"
return 1
fi
- # Test 2: DNS resolution
+ # Test 2: DNS resolution (use getent which is always available, unlike nslookup/dig)
step "Testing DNS resolution"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "nslookup archlinux.org >/dev/null 2>&1"; then
+ if $ssh_base "getent hosts archlinux.org >/dev/null 2>&1" 2>/dev/null; then
success "DNS resolution OK"
else
error "DNS resolution failed"
@@ -32,7 +35,7 @@ run_network_diagnostics() {
# Test 3: Arch mirror accessibility
step "Testing Arch mirror access"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "curl -s -I https://mirrors.kernel.org/archlinux/ | head -1 | grep -qE '(200|301)'"; then
+ if $ssh_base "curl -s -I https://mirrors.kernel.org/archlinux/ | head -1 | grep -qE '(200|301)'" 2>/dev/null; then
success "Arch mirrors accessible"
else
error "Cannot reach Arch mirrors"
@@ -41,7 +44,7 @@ run_network_diagnostics() {
# Test 4: AUR accessibility
step "Testing AUR access"
- if sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'"; then
+ if $ssh_base "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'" 2>/dev/null; then
success "AUR accessible"
else
error "Cannot reach AUR"
@@ -50,8 +53,7 @@ run_network_diagnostics() {
# Show network info
info "Network configuration:"
- sshpass -p 'archsetup' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$vm_host \
- "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while read line; do
+ $ssh_base "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while read line; do
info " $line"
done
diff --git a/scripts/testing/lib/validation.sh b/scripts/testing/lib/validation.sh
index 8c4787e..3191c64 100644
--- a/scripts/testing/lib/validation.sh
+++ b/scripts/testing/lib/validation.sh
@@ -20,7 +20,7 @@ declare -a UNKNOWN_ISSUES
# SSH helper (uses globals: VM_IP, ROOT_PASSWORD)
ssh_cmd() {
sshpass -p "$ROOT_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- -o ConnectTimeout=10 "root@$VM_IP" "$@" 2>/dev/null
+ -o ConnectTimeout=10 -p "${SSH_PORT:-22}" "root@$VM_IP" "$@" 2>/dev/null
}
# Validation result helpers
@@ -458,17 +458,12 @@ validate_hyprland_tools() {
local missing=""
# Check core Hyprland packages
- for pkg in hyprland hypridle hyprlock waybar wofi swww grim slurp gammastep; do
+ for pkg in hyprland hypridle hyprlock waybar fuzzel swww grim slurp gammastep foot; do
if ! ssh_cmd "pacman -Q $pkg &>/dev/null"; then
missing="$missing $pkg"
fi
done
- # st should still be installed (via XWayland)
- if ! ssh_cmd "test -f /usr/local/bin/st"; then
- missing="$missing st"
- fi
-
if [ -z "$missing" ]; then
validation_pass "All Hyprland tools installed"
else
@@ -483,7 +478,7 @@ validate_hyprland_config() {
for config in ".config/hypr/hyprland.conf" ".config/hypr/hypridle.conf" \
".config/hypr/hyprlock.conf" ".config/waybar/config" \
- ".config/wofi/config" ".config/gammastep/config.ini"; do
+ ".config/fuzzel/fuzzel.ini" ".config/gammastep/config.ini"; do
if ! ssh_cmd "test -f /home/cjennings/$config"; then
missing="$missing $config"
fi
diff --git a/scripts/testing/lib/vm-utils.sh b/scripts/testing/lib/vm-utils.sh
index 81aec33..47bd391 100755
--- a/scripts/testing/lib/vm-utils.sh
+++ b/scripts/testing/lib/vm-utils.sh
@@ -1,39 +1,52 @@
#!/bin/bash
-# VM management utilities for archsetup testing
+# VM management utilities for archsetup testing (direct QEMU)
# Author: Craig Jennings <craigmartinjennings@gmail.com>
# License: GNU GPLv3
+#
+# Manages QEMU VMs directly without libvirt. Uses user-mode networking
+# with port forwarding for SSH access and qemu-img for snapshots.
# Note: logging.sh should already be sourced by the calling script
# VM configuration defaults
VM_CPUS="${VM_CPUS:-4}"
-VM_RAM="${VM_RAM:-8192}" # MB
-VM_DISK="${VM_DISK:-50}" # GB
-VM_NETWORK="${VM_NETWORK:-default}"
-LIBVIRT_URI="qemu:///system" # Use system session, not user session
-
-# Check if libvirt is running
-check_libvirt() {
- if ! systemctl is-active --quiet libvirtd; then
- error "libvirtd service is not running"
- info "Start it with: sudo systemctl start libvirtd"
- return 1
- fi
- return 0
+VM_RAM="${VM_RAM:-4096}" # MB
+VM_DISK_SIZE="${VM_DISK_SIZE:-50}" # GB
+
+# SSH configuration
+SSH_PORT="${SSH_PORT:-2222}"
+SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10"
+ROOT_PASSWORD="${ROOT_PASSWORD:-archsetup}"
+
+# OVMF firmware paths
+OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
+OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
+
+# VM runtime paths (set by init_vm_paths)
+VM_IMAGES_DIR=""
+DISK_PATH=""
+OVMF_VARS=""
+PID_FILE=""
+MONITOR_SOCK=""
+SERIAL_LOG=""
+
+# Initialize all VM paths from images directory
+# Must be called before any other vm-utils function
+init_vm_paths() {
+ local images_dir="${1:-$VM_IMAGES_DIR}"
+ [ -z "$images_dir" ] && fatal "VM_IMAGES_DIR not set"
+
+ VM_IMAGES_DIR="$images_dir"
+ DISK_PATH="$VM_IMAGES_DIR/archsetup-base.qcow2"
+ OVMF_VARS="$VM_IMAGES_DIR/OVMF_VARS.fd"
+ PID_FILE="$VM_IMAGES_DIR/qemu.pid"
+ MONITOR_SOCK="$VM_IMAGES_DIR/qemu-monitor.sock"
+ SERIAL_LOG="$VM_IMAGES_DIR/qemu-serial.log"
+ mkdir -p "$VM_IMAGES_DIR"
}
-# Check if user is in libvirt group
-check_libvirt_group() {
- if ! groups | grep -q libvirt; then
- warn "Current user is not in libvirt group"
- info "Add yourself with: sudo usermod -a -G libvirt $USER"
- info "Then log out and back in for changes to take effect"
- return 1
- fi
- return 0
-}
+# ─── Prerequisite Checks ─────────────────────────────────────────────
-# Check if KVM is available
check_kvm() {
if [ ! -e /dev/kvm ]; then
error "KVM is not available"
@@ -44,249 +57,311 @@ check_kvm() {
return 0
}
-# Wait for VM to boot (check for SSH or serial console)
-wait_for_vm() {
- local vm_name="$1"
- local timeout="${2:-300}" # 5 minutes default
- local elapsed=0
+check_qemu() {
+ if ! command -v qemu-system-x86_64 &>/dev/null; then
+ error "qemu-system-x86_64 not found"
+ info "Install with: sudo pacman -S qemu-full"
+ return 1
+ fi
+ return 0
+}
- progress "Waiting for VM $vm_name to boot..."
+check_ovmf() {
+ if [ ! -f "$OVMF_CODE" ]; then
+ error "OVMF firmware not found: $OVMF_CODE"
+ info "Install with: sudo pacman -S edk2-ovmf"
+ return 1
+ fi
+ return 0
+}
- while [ $elapsed -lt $timeout ]; do
- if virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null | grep -q "running"; then
- sleep 5
- complete "VM $vm_name is running"
- return 0
- fi
- sleep 2
- elapsed=$((elapsed + 2))
- done
+check_sshpass() {
+ if ! command -v sshpass &>/dev/null; then
+ error "sshpass not found"
+ info "Install with: sudo pacman -S sshpass"
+ return 1
+ fi
+ return 0
+}
- error "Timeout waiting for VM $vm_name to boot"
- return 1
+check_socat() {
+ if ! command -v socat &>/dev/null; then
+ error "socat not found"
+ info "Install with: sudo pacman -S socat"
+ return 1
+ fi
+ return 0
}
-# Check if VM exists
-vm_exists() {
- local vm_name="$1"
- virsh --connect "$LIBVIRT_URI" dominfo "$vm_name" &>/dev/null
- return $?
+check_prerequisites() {
+ local failed=0
+ check_kvm || failed=1
+ check_qemu || failed=1
+ check_ovmf || failed=1
+ check_sshpass || failed=1
+ check_socat || failed=1
+ return $failed
}
-# Check if VM is running
+# ─── VM Lifecycle ─────────────────────────────────────────────────────
+
+# Check if a QEMU VM is running via PID file
vm_is_running() {
- local vm_name="$1"
- [ "$(virsh --connect "$LIBVIRT_URI" domstate "$vm_name" 2>/dev/null)" = "running" ]
- return $?
-}
+ [ -f "$PID_FILE" ] || return 1
-# Start VM
-start_vm() {
- local vm_name="$1"
+ local pid
+ pid=$(cat "$PID_FILE" 2>/dev/null) || return 1
- if vm_is_running "$vm_name"; then
- warn "VM $vm_name is already running"
+ if kill -0 "$pid" 2>/dev/null && grep -q "qemu" "/proc/$pid/cmdline" 2>/dev/null; then
return 0
fi
- step "Starting VM: $vm_name"
- if virsh --connect "$LIBVIRT_URI" start "$vm_name" >> "$LOGFILE" 2>&1; then
- success "VM $vm_name started"
- return 0
- else
- error "Failed to start VM $vm_name"
+ # Stale PID file
+ rm -f "$PID_FILE"
+ return 1
+}
+
+# Start a QEMU VM
+# Args: $1 = disk path
+# $2 = boot mode: "iso" or "disk" (default: disk)
+# $3 = ISO path (required if mode=iso)
+# $4 = display: "none" (headless) or "gtk" (graphical, default: none)
+start_qemu() {
+ local disk="$1"
+ local mode="${2:-disk}"
+ local iso_path="${3:-}"
+ local display="${4:-none}"
+
+ # Stop any existing instance
+ stop_qemu 2>/dev/null || true
+
+ # Check port availability
+ if ss -tln 2>/dev/null | grep -q ":${SSH_PORT} "; then
+ error "Port $SSH_PORT is already in use"
+ info "Another QEMU instance or service may be running"
return 1
fi
-}
-# Stop VM gracefully
-stop_vm() {
- local vm_name="$1"
- local timeout="${2:-60}"
+ # Ensure OVMF_VARS exists
+ if [ ! -f "$OVMF_VARS" ]; then
+ cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS"
+ fi
+
+ # Truncate serial log
+ : > "$SERIAL_LOG"
+
+ # Build QEMU command
+ local qemu_cmd=(
+ qemu-system-x86_64
+ -name "archsetup-test"
+ -machine "q35,accel=kvm"
+ -cpu host
+ -smp "$VM_CPUS"
+ -m "$VM_RAM"
+ -drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE"
+ -drive "if=pflash,format=raw,file=$OVMF_VARS"
+ -drive "file=$disk,format=qcow2,if=virtio"
+ -netdev "user,id=net0,hostfwd=tcp::${SSH_PORT}-:22"
+ -device "virtio-net-pci,netdev=net0"
+ -monitor "unix:$MONITOR_SOCK,server,nowait"
+ -pidfile "$PID_FILE"
+ -serial "file:$SERIAL_LOG"
+ -usb
+ -device usb-tablet
+ )
+
+ # Boot mode
+ if [ "$mode" = "iso" ]; then
+ [ -z "$iso_path" ] && fatal "ISO path required for iso boot mode"
+ qemu_cmd+=(-cdrom "$iso_path" -boot d)
+ else
+ qemu_cmd+=(-boot c)
+ fi
- if ! vm_is_running "$vm_name"; then
- info "VM $vm_name is not running"
- return 0
+ # Display mode
+ if [ "$display" = "gtk" ]; then
+ qemu_cmd+=(-device virtio-vga-gl -display "gtk,gl=on")
+ else
+ qemu_cmd+=(-display none)
fi
- step "Shutting down VM: $vm_name"
- if virsh --connect "$LIBVIRT_URI" shutdown "$vm_name" >> "$LOGFILE" 2>&1; then
- # Wait for graceful shutdown
- local elapsed=0
- while [ $elapsed -lt $timeout ]; do
- if ! vm_is_running "$vm_name"; then
- success "VM $vm_name stopped gracefully"
- return 0
- fi
- sleep 2
- elapsed=$((elapsed + 2))
- done
-
- warn "VM $vm_name did not stop gracefully, forcing..."
- virsh --connect "$LIBVIRT_URI" destroy "$vm_name" >> "$LOGFILE" 2>&1
+ step "Starting QEMU (mode=$mode, display=$display)"
+
+ # Launch in background
+ "${qemu_cmd[@]}" &>> "$LOGFILE" &
+
+ # Wait for PID file to appear
+ local wait=0
+ while [ ! -f "$PID_FILE" ] && [ $wait -lt 10 ]; do
+ sleep 1
+ wait=$((wait + 1))
+ done
+
+ if ! vm_is_running; then
+ error "QEMU failed to start"
+ return 1
fi
- success "VM $vm_name stopped"
+ success "QEMU started (PID: $(cat "$PID_FILE"))"
return 0
}
-# Destroy VM (force stop)
-destroy_vm() {
- local vm_name="$1"
+# Stop VM gracefully via ACPI powerdown, fallback to kill
+stop_qemu() {
+ local timeout="${1:-60}"
- if ! vm_exists "$vm_name"; then
- info "VM $vm_name does not exist"
+ if ! vm_is_running; then
return 0
fi
- step "Destroying VM: $vm_name"
- if vm_is_running "$vm_name"; then
- virsh --connect "$LIBVIRT_URI" destroy "$vm_name" >> "$LOGFILE" 2>&1
+ step "Sending shutdown signal to VM"
+
+ # Send ACPI powerdown via monitor socket
+ if [ -S "$MONITOR_SOCK" ]; then
+ echo "system_powerdown" | socat - "UNIX-CONNECT:$MONITOR_SOCK" >> "$LOGFILE" 2>&1 || true
fi
- virsh --connect "$LIBVIRT_URI" undefine "$vm_name" --nvram >> "$LOGFILE" 2>&1
- success "VM $vm_name destroyed"
+ # Wait for graceful shutdown
+ local elapsed=0
+ while [ $elapsed -lt $timeout ]; do
+ if ! vm_is_running; then
+ success "VM stopped gracefully"
+ _cleanup_qemu_files
+ return 0
+ fi
+ sleep 2
+ elapsed=$((elapsed + 2))
+ done
+
+ # Force kill
+ warn "VM did not stop gracefully after ${timeout}s, force killing"
+ kill_qemu
return 0
}
-# Create snapshot
+# Force kill VM immediately
+kill_qemu() {
+ if [ -f "$PID_FILE" ]; then
+ local pid
+ pid=$(cat "$PID_FILE" 2>/dev/null)
+ if [ -n "$pid" ]; then
+ kill -9 "$pid" 2>/dev/null || true
+ fi
+ fi
+ _cleanup_qemu_files
+}
+
+# Clean up runtime files
+_cleanup_qemu_files() {
+ rm -f "$PID_FILE" "$MONITOR_SOCK"
+}
+
+# ─── Snapshot Operations (qemu-img) ──────────────────────────────────
+# All snapshot operations require the VM to be stopped.
+
create_snapshot() {
- local vm_name="$1"
- local snapshot_name="$2"
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
+
+ if vm_is_running; then
+ error "Cannot create snapshot while VM is running"
+ return 1
+ fi
step "Creating snapshot: $snapshot_name"
- if virsh --connect "$LIBVIRT_URI" snapshot-create-as "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then
- success "Snapshot $snapshot_name created"
+ if qemu-img snapshot -c "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then
+ success "Snapshot '$snapshot_name' created"
return 0
else
- error "Failed to create snapshot $snapshot_name"
+ error "Failed to create snapshot '$snapshot_name'"
return 1
fi
}
-# Restore snapshot
restore_snapshot() {
- local vm_name="$1"
- local snapshot_name="$2"
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
+
+ if vm_is_running; then
+ error "Cannot restore snapshot while VM is running"
+ return 1
+ fi
step "Restoring snapshot: $snapshot_name"
- if virsh --connect "$LIBVIRT_URI" snapshot-revert "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then
- success "Snapshot $snapshot_name restored"
+ if qemu-img snapshot -a "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then
+ success "Snapshot '$snapshot_name' restored"
return 0
else
- error "Failed to restore snapshot $snapshot_name"
+ error "Failed to restore snapshot '$snapshot_name'"
return 1
fi
}
-# Delete snapshot
delete_snapshot() {
- local vm_name="$1"
- local snapshot_name="$2"
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
step "Deleting snapshot: $snapshot_name"
- if virsh --connect "$LIBVIRT_URI" snapshot-delete "$vm_name" "$snapshot_name" >> "$LOGFILE" 2>&1; then
- success "Snapshot $snapshot_name deleted"
+ if qemu-img snapshot -d "$snapshot_name" "$disk" >> "$LOGFILE" 2>&1; then
+ success "Snapshot '$snapshot_name' deleted"
return 0
else
- error "Failed to delete snapshot $snapshot_name"
+ error "Failed to delete snapshot '$snapshot_name'"
return 1
fi
}
-# Clone disk image (copy-on-write)
-clone_disk() {
- local base_image="$1"
- local new_image="$2"
-
- if [ ! -f "$base_image" ]; then
- error "Base image not found: $base_image"
- return 1
- fi
-
- step "Cloning disk image (full copy)"
- if qemu-img convert -f qcow2 -O qcow2 "$base_image" "$new_image" >> "$LOGFILE" 2>&1; then
- success "Disk cloned: $new_image"
- else
- error "Failed to clone disk"
- return 1
- fi
-
- # Truncate machine-id so systemd generates a new one on boot (avoids DHCP conflicts)
- step "Clearing machine-id for unique network identity"
- if guestfish -a "$new_image" -i truncate /etc/machine-id >> "$LOGFILE" 2>&1; then
- success "Machine-ID cleared (will regenerate on boot)"
- return 0
- else
- warn "Failed to clear machine-ID (guestfish failed)"
- info "Network may conflict with base VM if both run simultaneously"
- return 0 # Don't fail the whole operation
- fi
+list_snapshots() {
+ local disk="${1:-$DISK_PATH}"
+ qemu-img snapshot -l "$disk" 2>/dev/null
}
-# Get VM IP address (requires guest agent or DHCP lease)
-get_vm_ip() {
- local vm_name="$1"
+snapshot_exists() {
+ local disk="${1:-$DISK_PATH}"
+ local snapshot_name="${2:-clean-install}"
+ qemu-img snapshot -l "$disk" 2>/dev/null | grep -q "$snapshot_name"
+}
- # Try guest agent first
- local ip
- ip=$(virsh --connect "$LIBVIRT_URI" domifaddr "$vm_name" 2>/dev/null | grep -oP '(\d+\.){3}\d+' | head -1)
+# ─── SSH Operations ───────────────────────────────────────────────────
- if [ -n "$ip" ]; then
- echo "$ip"
- return 0
- fi
-
- # Fall back to DHCP leases
- local mac
- mac=$(virsh --connect "$LIBVIRT_URI" domiflist "$vm_name" | grep -oP '([0-9a-f]{2}:){5}[0-9a-f]{2}' | head -1)
+# Wait for SSH to become available on localhost:$SSH_PORT
+wait_for_ssh() {
+ local password="${1:-$ROOT_PASSWORD}"
+ local timeout="${2:-120}"
+ local elapsed=0
- if [ -n "$mac" ]; then
- ip=$(grep "$mac" /var/lib/libvirt/dnsmasq/default.leases 2>/dev/null | awk '{print $3}')
- if [ -n "$ip" ]; then
- echo "$ip"
+ progress "Waiting for SSH on localhost:$SSH_PORT..."
+ while [ $elapsed -lt $timeout ]; do
+ if sshpass -p "$password" ssh $SSH_OPTS -p "$SSH_PORT" root@localhost true 2>/dev/null; then
+ success "SSH is available"
return 0
fi
- fi
+ sleep 5
+ elapsed=$((elapsed + 5))
+ done
+ error "SSH timeout after ${timeout}s"
return 1
}
# Execute command in VM via SSH
vm_exec() {
- local vm_name="$1"
+ local password="${1:-$ROOT_PASSWORD}"
shift
- local cmd="$*"
-
- local ip
- ip=$(get_vm_ip "$vm_name")
-
- if [ -z "$ip" ]; then
- error "Could not get IP address for VM $vm_name"
- return 1
- fi
-
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$ip" "$cmd" 2>> "$LOGFILE"
+ sshpass -p "$password" ssh $SSH_OPTS \
+ -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
+ -p "$SSH_PORT" root@localhost "$@" 2>> "$LOGFILE"
}
# Copy file to VM
copy_to_vm() {
- local vm_name="$1"
- local local_file="$2"
- local remote_path="$3"
-
- local ip
- ip=$(get_vm_ip "$vm_name")
-
- if [ -z "$ip" ]; then
- error "Could not get IP address for VM $vm_name"
- return 1
- fi
+ local local_file="$1"
+ local remote_path="$2"
+ local password="${3:-$ROOT_PASSWORD}"
- step "Copying $local_file to VM"
- if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "$local_file" "root@$ip:$remote_path" >> "$LOGFILE" 2>&1; then
+ step "Copying $(basename "$local_file") to VM:$remote_path"
+ if sshpass -p "$password" scp $SSH_OPTS -P "$SSH_PORT" \
+ "$local_file" "root@localhost:$remote_path" >> "$LOGFILE" 2>&1; then
success "File copied to VM"
return 0
else
@@ -297,21 +372,13 @@ copy_to_vm() {
# Copy file from VM
copy_from_vm() {
- local vm_name="$1"
- local remote_file="$2"
- local local_path="$3"
-
- local ip
- ip=$(get_vm_ip "$vm_name")
-
- if [ -z "$ip" ]; then
- error "Could not get IP address for VM $vm_name"
- return 1
- fi
+ local remote_file="$1"
+ local local_path="$2"
+ local password="${3:-$ROOT_PASSWORD}"
step "Copying $remote_file from VM"
- if scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
- "root@$ip:$remote_file" "$local_path" >> "$LOGFILE" 2>&1; then
+ if sshpass -p "$password" scp $SSH_OPTS -P "$SSH_PORT" \
+ "root@localhost:$remote_file" "$local_path" >> "$LOGFILE" 2>&1; then
success "File copied from VM"
return 0
else