#!/usr/bin/env bash # qga.sh - host-side helpers for driving the win11 VM via qemu-guest-agent. # # Source this from harness scripts: `source "$(dirname "$0")/../lib/qga.sh"` # # All commands run as NT AUTHORITY\SYSTEM inside the VM (qemu-ga's service # context). See reference-vm-qga-as-system memory note for why this is # preferred over WinRM for dispatcher / manifest-engine tests. set -euo pipefail VM_DOMAIN="${VM_DOMAIN:-win11}" VM_IP="${VM_IP:-192.168.122.225}" HOST_SAMBA_USER="${HOST_SAMBA_USER:-camp}" HOST_SAMBA_PASS="${HOST_SAMBA_PASS:-vos313}" HOST_SAMBA_HOST="${HOST_SAMBA_HOST:-192.168.122.1}" # Helper: fail loud die() { echo "[ERROR] $*" >&2; exit 1; } log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; } # Send a JSON command to qemu-ga, return parsed return field qga() { local payload="$1" virsh -c qemu:///system qemu-agent-command "$VM_DOMAIN" "$payload" 2>/dev/null \ || die "qga call failed: $payload" } # Run a PowerShell snippet inside the VM as SYSTEM. Stdin = snippet, # stdout = exit-decorated combined stdout+stderr from the PS process. qga_run_ps() { python3 - <<'PY' import base64, json, subprocess, sys, time, os DOMAIN = os.environ.get('VM_DOMAIN', 'win11') TIMEOUT = int(os.environ.get('QGA_TIMEOUT', '300')) def virsh(cmd): p = subprocess.run(['virsh','-c','qemu:///system','qemu-agent-command',DOMAIN, json.dumps(cmd)], capture_output=True, text=True, timeout=120) if p.returncode != 0: sys.exit(f"virsh err: {p.stderr.strip()}") return json.loads(p.stdout)['return'] snippet = sys.stdin.read() args = ['-NoProfile','-ExecutionPolicy','Bypass','-Command', snippet] pid = virsh({'execute':'guest-exec','arguments':{'path':'powershell.exe','arg':args,'capture-output':True}})['pid'] deadline = time.time() + TIMEOUT while time.time() < deadline: st = virsh({'execute':'guest-exec-status','arguments':{'pid':pid}}) if st.get('exited'): out = base64.b64decode(st.get('out-data','')).decode('utf-8','replace') err = base64.b64decode(st.get('err-data','')).decode('utf-8','replace') rc = st.get('exitcode') sys.stdout.write(out) if err: sys.stdout.write('\n--- STDERR ---\n') sys.stdout.write(err) sys.stdout.write(f'\n--- exit {rc} ---\n') sys.exit(0 if rc == 0 else 1) time.sleep(0.5) sys.exit(f"timeout waiting for pid {pid}") PY } # Wait until qemu-ga responds to guest-ping vm_wait_for_ready() { local max=${1:-90} local i=0 while ! virsh -c qemu:///system qemu-agent-command "$VM_DOMAIN" '{"execute":"guest-ping"}' >/dev/null 2>&1; do ((i++)) if (( i > max )); then die "VM never became ready ($max attempts)"; fi sleep 1 done } # Make sure VM is up and qga is alive. Start if not. vm_start_if_needed() { local state state=$(virsh -c qemu:///system domstate "$VM_DOMAIN" 2>/dev/null || echo unknown) if [[ "$state" != "running" ]]; then log "VM is $state - starting" virsh -c qemu:///system start "$VM_DOMAIN" >/dev/null fi vm_wait_for_ready 90 } # Mount the host samba share inside the VM as SYSTEM. Idempotent. # After this call Z:\ inside the VM points at /home/camp/pxe-images/. vm_mount_share() { qga_run_ps <&1 | Out-Null "Z reachable: \$(Test-Path 'Z:\\')" PS } # Upload a single file from host to VM by reading it into VM via WriteAllBytes. # Best for tiny files (config, single ps1). For bigger files use vm_mount_share + Copy-Item. vm_put_file() { local local_path="$1" remote_path="$2" [[ -f "$local_path" ]] || die "no such file: $local_path" local b64 b64=$(base64 -w0 < "$local_path") qga_run_ps <