build-usb: whole webapp, hard sfdisk failure, ISO-derived boot params, image-file mode; tests/vm harness

- Copy the whole webapp/ tree (minus __pycache__) in build-usb.sh and
  build-proxmox-iso.sh, so config, services/, views/ and
  blancco_evidence_monthly.py reach the server.
- sfdisk --append failure now aborts; the CIDATA partition is the one
  starting exactly at the computed sector, and nothing that starts
  before it is ever formatted (the old fallback hit the ISO's EFI
  partition).
- Volume id and El Torito load sizes come from the ISO
  (xorriso -report_el_torito as_mkisofs); grub.cfg must contain
  autoinstall after patching.
- --image FILE SIZE|auto builds the same layout in a file without root
  (mkfs.vfat -C + mtools, sfdisk on the file). Plus --payload-root,
  --skip-enrollment/-bios/-drivers/-boot-tools, --yes.
- tests/vm: libvirt harness (pxetest-server, pxetest-client, isolated
  pxetest-imaging network) with fetch-iso/build-image/create/start/
  status/stop/destroy.
This commit is contained in:
cproudlock
2026-10-15 08:55:40 -04:00
parent 689eebf9d1
commit 45cf7be37a
12 changed files with 1084 additions and 219 deletions

60
tests/vm/README.md Normal file
View File

@@ -0,0 +1,60 @@
# pxetest VM harness
Proves a new PXE server can be built from this repo, in two libvirt VMs on
a laptop, without root and without touching a real network.
## Topology
| Object | What |
|-------------------|----------------------------------------------------------------------|
| `pxetest-imaging` | libvirt network, bridge `virbr-pxetest`. No `<forward>`, no `<ip>`: the host has no address on it and libvirt runs no DHCP there. Only VM taps are on the bridge. |
| `default` | libvirt NAT, 192.168.122.0/24. Stands in for the GE/corp network. |
| `pxetest-server` | 2 vCPU, 4 GiB, q35 + OVMF Secure Boot (MS keys), 60G thin qcow2 (`vda`, boot 1), the USB image as a USB disk (`sda`, boot 2). |
| `pxetest-client` | 2 vCPU, 2 GiB, OVMF Secure Boot (MS keys), 1G blank disk, network boot first (UEFI IPv4 PXE, virtio-net, OVMF driver, no option ROM). |
NIC MACs (fixed):
| VM | NIC | MAC | Network | Playbook var |
|--------|---------|---------------------|-------------------|-------------------|
| server | GE | `52:54:00:9e:10:01` | `default` NAT | `pxe_ge_mac` / corp_nic_mac |
| server | imaging | `52:54:00:9e:10:02` | `pxetest-imaging` | `pxe_imaging_mac` / pxe_nic_mac |
| client | - | `52:54:00:9e:10:03` | `pxetest-imaging` | - |
The disk boots before the USB on purpose: a blank disk falls through to
the installer, and after the install the server boots its disk instead of
re-running autoinstall. CIDATA stays attached for the first-boot playbook.
Never attach a pxetest VM to a physical NIC, a host bridge with physical
ports, or macvtap: the server's dnsmasq serves DHCP/PXE as 172.16.9.1.
`create.sh`, `start.sh` and `status.sh` check the isolation and refuse to
continue if `pxetest-imaging` ever gains a `<forward>`, an `<ip>`, a host
IPv4 address or a non-tap bridge port.
## Run it
export PXETEST_CACHE=~/.cache/pxetest # ISO + image (about 10 GB)
export PXETEST_PAYLOAD_ROOT=/home/camp/projects/pxe # checkout holding offline-packages/, boot-files/, ...
tests/vm/fetch-iso.sh # Ubuntu 24.04.5 live-server ISO, SHA256SUMS-verified
tests/vm/build-image.sh # build-usb.sh --image, upload as volume pxetest-usb.img
tests/vm/create.sh # network, volumes, both VMs (prints the MAC pins)
tests/vm/start.sh server # autoinstall, reboot, first-boot playbook
tests/vm/status.sh --shot /tmp/pxetest-shots # states, leases, screenshots
tests/vm/start.sh client # once the server playbook is done
tests/vm/stop.sh # ACPI shutdown both, keep them defined
tests/vm/destroy.sh # remove every pxetest-* object
`build-image.sh` skips enrollment/, bios-staging/, drivers-staging/ and
boot-tools/ by default; `--with-enrollment` and `--with-boot-tools` add
them back, `--size 20G` fixes the image size (default `auto`). If the host
has no `sfdisk` or `mtools`, the debs are extracted into
`$PXETEST_CACHE/tools` (no install, no root).
Watch the server with `virt-viewer -c qemu:///system pxetest-server` (VNC on
127.0.0.1) or screenshots. On the installed system, first-boot output goes
to `/var/log/first-boot.log`. Once the GE NIC takes a DHCP lease on the NAT
network, `status.sh` shows its address and `ssh pxe@<address>` works from
the host.
Memory: server 4 GiB + client 2 GiB. Keep other VMs (win11) off while
both run, and stop them when done.

57
tests/vm/build-image.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/bin/bash
# Build the installer USB as a disk image FILE with build-usb.sh image mode,
# then upload it into the libvirt 'default' pool as volume pxetest-usb.img.
#
# Usage: tests/vm/build-image.sh [--with-enrollment] [--with-boot-tools] [--size SIZE]
# Default skips enrollment/, bios-staging/, drivers-staging/ and boot-tools/
# to keep the image small; SIZE defaults to "auto".
# Env: PXETEST_PAYLOAD_ROOT (where offline-packages/, boot-files/... live)
source "$(dirname "$0")/common.sh"
SIZE=auto
SKIPS=(--skip-enrollment --skip-bios --skip-drivers --skip-boot-tools)
while [ $# -gt 0 ]; do
case "$1" in
--with-enrollment) SKIPS=("${SKIPS[@]/--skip-enrollment}"); shift ;;
--with-boot-tools) SKIPS=("${SKIPS[@]/--skip-boot-tools}"); shift ;;
--size) SIZE="$2"; shift 2 ;;
*) die "unknown option $1" ;;
esac
done
ARGS=()
for s in "${SKIPS[@]}"; do [ -n "$s" ] && ARGS+=("$s"); done
[ -f "$UBUNTU_ISO" ] || die "no ISO at $UBUNTU_ISO; run tests/vm/fetch-iso.sh"
ensure_image_tools
# Report what the payload root has and lacks (do not invent content).
log "payload root: $PXETEST_PAYLOAD_ROOT"
for d in offline-packages pip-wheels boot-files boot-tools enrollment bios-staging drivers-staging; do
if [ -d "$PXETEST_PAYLOAD_ROOT/$d" ]; then
printf ' %-17s %s\n' "$d" "$(du -shL "$PXETEST_PAYLOAD_ROOT/$d" 2>/dev/null | cut -f1)"
else
printf ' %-17s MISSING\n' "$d"
fi
done
for f in ipxe-shimx64.efi ipxeboot.tar.gz wimboot boot.wim BCD boot.sdi bootx64.efi undionly.kpxe; do
[ -f "$PXETEST_PAYLOAD_ROOT/boot-files/$f" ] || log " boot-files/$f MISSING"
done
if [ -e "$USB_IMAGE" ] && [ ! -f "$USB_IMAGE" ]; then
die "$USB_IMAGE is not a regular file"
fi
"$REPO_ROOT/scripts/build-usb.sh" --yes --payload-root "$PXETEST_PAYLOAD_ROOT" \
"${ARGS[@]}" --image "$USB_IMAGE" "$SIZE" "$UBUNTU_ISO"
BYTES=$(stat -c%s "$USB_IMAGE")
if dom_exists "$SERVER_VM" && [ "$(dom_state "$SERVER_VM")" != "shut off" ]; then
die "$SERVER_VM is running; stop it before replacing its USB volume"
fi
if vol_exists "$USB_VOL"; then
virsh vol-delete --pool "$POOL" "$USB_VOL" >/dev/null
fi
log "uploading $USB_IMAGE ($((BYTES / 1048576)) MiB) to pool $POOL as $USB_VOL"
virsh vol-create-as "$POOL" "$USB_VOL" "$BYTES" --format raw >/dev/null
virsh vol-upload --pool "$POOL" --sparse "$USB_VOL" "$USB_IMAGE"
log "done"

113
tests/vm/common.sh Executable file
View File

@@ -0,0 +1,113 @@
# shellcheck shell=bash disable=SC2034
# Shared settings for the pxetest VM harness. Sourced, not run.
#
# Everything this harness creates is named pxetest-*: the two domains, the
# isolated network, and the volumes in the libvirt 'default' pool. Nothing
# else is touched.
set -euo pipefail
VM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$VM_DIR/../.." && pwd)"
export LIBVIRT_DEFAULT_URI="${LIBVIRT_DEFAULT_URI:-qemu:///system}"
# Download / build cache (ISO, USB image, extracted helper tools).
PXETEST_CACHE="${PXETEST_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/pxetest}"
# Where the gitignored payload dirs (offline-packages/, pip-wheels/,
# boot-files/, boot-tools/, enrollment/, ...) live. Defaults to this repo;
# point it at the main checkout when running from a worktree.
PXETEST_PAYLOAD_ROOT="${PXETEST_PAYLOAD_ROOT:-$REPO_ROOT}"
# Ubuntu ISO
UBUNTU_SERIES="${UBUNTU_SERIES:-24.04}"
UBUNTU_ISO_NAME="${UBUNTU_ISO_NAME:-ubuntu-24.04.5-live-server-amd64.iso}"
UBUNTU_RELEASE_URL="${UBUNTU_RELEASE_URL:-https://releases.ubuntu.com/$UBUNTU_SERIES}"
UBUNTU_ISO="$PXETEST_CACHE/$UBUNTU_ISO_NAME"
USB_IMAGE="$PXETEST_CACHE/pxetest-usb.img"
# libvirt objects
POOL="default"
SERVER_VM="pxetest-server"
CLIENT_VM="pxetest-client"
IMAGING_NET="pxetest-imaging"
IMAGING_BRIDGE="virbr-pxetest"
GE_NET="default" # libvirt NAT, 192.168.122.0/24 = "GE/corp"
SERVER_DISK_VOL="pxetest-server.qcow2"
SERVER_DISK_SIZE="${SERVER_DISK_SIZE:-60G}"
USB_VOL="pxetest-usb.img"
CLIENT_DISK_VOL="pxetest-client.qcow2"
SERVER_MEM_MIB="${SERVER_MEM_MIB:-4096}"
CLIENT_MEM_MIB="${CLIENT_MEM_MIB:-2048}"
# Fixed MACs so the playbook's NIC pins can be written once for this site.
SERVER_GE_MAC="52:54:00:9e:10:01" # on 'default' NAT (GE/corp side)
SERVER_IMAGING_MAC="52:54:00:9e:10:02" # on pxetest-imaging (imaging side)
CLIENT_MAC="52:54:00:9e:10:03" # on pxetest-imaging only
log() { printf '[pxetest] %s\n' "$*"; }
die() { printf '[pxetest] ERROR: %s\n' "$*" >&2; exit 1; }
dom_exists() { virsh dominfo "$1" >/dev/null 2>&1; }
dom_state() { virsh domstate "$1" 2>/dev/null | head -1; }
net_exists() { virsh net-info "$1" >/dev/null 2>&1; }
# No "| grep -q" under pipefail: grep exits early, the writer gets SIGPIPE
# and the pipeline reads as failed even on a match.
net_active() { [ "$(virsh net-info "$1" 2>/dev/null | awk '/^Active:/{print $2}')" = "yes" ]; }
vol_exists() { virsh vol-info --pool "$POOL" "$1" >/dev/null 2>&1; }
# sfdisk and mtools are needed for image mode. If the host lacks them,
# extract the Ubuntu debs into the cache (no root, no install).
ensure_image_tools() {
local troot="$PXETEST_CACHE/tools/root"
if [ -d "$troot" ]; then
export PATH="$troot/usr/sbin:$troot/usr/bin:$PATH"
export LD_LIBRARY_PATH="$troot/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
if command -v sfdisk >/dev/null 2>&1 && command -v mcopy >/dev/null 2>&1 \
&& sfdisk --version >/dev/null 2>&1; then
return 0
fi
log "sfdisk/mtools missing: extracting fdisk, libfdisk1, mtools debs into $troot"
mkdir -p "$PXETEST_CACHE/tools/debs" "$troot"
(cd "$PXETEST_CACHE/tools/debs" && apt-get download fdisk libfdisk1 mtools) \
|| die "apt-get download fdisk libfdisk1 mtools failed; install fdisk + mtools"
local d
for d in "$PXETEST_CACHE/tools/debs"/*.deb; do
dpkg-deb -x "$d" "$troot"
done
export PATH="$troot/usr/sbin:$troot/usr/bin:$PATH"
export LD_LIBRARY_PATH="$troot/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
if ! sfdisk --version >/dev/null || ! command -v mcopy >/dev/null; then
die "extracted sfdisk/mcopy do not run"
fi
}
# The imaging network must be fully isolated: no <forward>, no <ip>
# (so the host has no address on it and libvirt runs no DHCP there).
assert_imaging_isolated() {
local xml
xml=$(virsh net-dumpxml "$IMAGING_NET")
if grep -q '<forward' <<<"$xml" || grep -q '<ip' <<<"$xml"; then
die "$IMAGING_NET has a <forward> or <ip> element: NOT isolated. Run destroy.sh and create.sh."
fi
local v4
v4=$(ip -4 -o addr show dev "$IMAGING_BRIDGE" 2>/dev/null || true)
if [ -n "$v4" ]; then
die "host has an IPv4 address on $IMAGING_BRIDGE: NOT isolated"
fi
# The bridge must hold only libvirt tap devices (vnetN), never a
# physical port.
local port
for port in /sys/class/net/"$IMAGING_BRIDGE"/brif/*; do
[ -e "$port" ] || continue
port=$(basename "$port")
case "$port" in
vnet*) ;;
*) die "$IMAGING_BRIDGE has non-tap port $port: NOT isolated" ;;
esac
done
}

172
tests/vm/create.sh Executable file
View File

@@ -0,0 +1,172 @@
#!/bin/bash
# Define the pxetest network, volumes and VMs. Idempotent: existing
# pxetest-* objects are kept (run destroy.sh first to rebuild them).
#
# pxetest-imaging isolated libvirt network: no <forward>, no <ip>, so the
# host has no address on it and libvirt runs no DHCP.
# Bridge virbr-pxetest holds only the VMs' tap devices.
# pxetest-server 2 vCPU, 4 GiB, OVMF (Secure Boot, MS keys), 60G thin
# qcow2 + the USB image as a USB disk.
# NIC1 52:54:00:9e:10:01 on 'default' NAT ("GE/corp")
# NIC2 52:54:00:9e:10:02 on pxetest-imaging ("imaging")
# pxetest-client 2 vCPU, 2 GiB, OVMF Secure Boot with MS keys, 1G blank
# disk, one NIC 52:54:00:9e:10:03 on pxetest-imaging,
# network boot first (UEFI IPv4 PXE).
#
# Usage: tests/vm/create.sh
source "$(dirname "$0")/common.sh"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# --- isolated imaging network ---
if ! net_exists "$IMAGING_NET"; then
cat >"$TMP/net.xml" <<XML
<network>
<name>$IMAGING_NET</name>
<bridge name='$IMAGING_BRIDGE' stp='on' delay='0'/>
</network>
XML
virsh net-define "$TMP/net.xml" >/dev/null
log "defined network $IMAGING_NET (isolated, no forward, no ip)"
fi
virsh net-autostart --disable "$IMAGING_NET" >/dev/null 2>&1 || true
if ! net_active "$IMAGING_NET"; then
virsh net-start "$IMAGING_NET" >/dev/null
fi
assert_imaging_isolated
net_exists "$GE_NET" || die "libvirt network '$GE_NET' (NAT) does not exist"
net_active "$GE_NET" || virsh net-start "$GE_NET" >/dev/null
# --- volumes ---
if ! vol_exists "$SERVER_DISK_VOL"; then
virsh vol-create-as "$POOL" "$SERVER_DISK_VOL" "$SERVER_DISK_SIZE" --format qcow2 >/dev/null
log "created $SERVER_DISK_VOL ($SERVER_DISK_SIZE thin qcow2)"
fi
if ! vol_exists "$CLIENT_DISK_VOL"; then
virsh vol-create-as "$POOL" "$CLIENT_DISK_VOL" 1G --format qcow2 >/dev/null
log "created $CLIENT_DISK_VOL (1G blank)"
fi
vol_exists "$USB_VOL" || log "WARNING: no $USB_VOL volume yet; run build-image.sh before start.sh"
# Disks are type='file' with the pool's path: with type='volume' sources
# qemu got "Permission denied" opening them (AppArmor profile).
POOL_DIR=$(virsh pool-dumpxml "$POOL" | sed -n 's|.*<path>\(.*\)</path>.*|\1|p' | head -1)
[ -n "$POOL_DIR" ] || die "cannot read the path of pool $POOL"
# Secure Boot with Microsoft keys enrolled: libvirt picks the edk2
# descriptor (40-edk2-x86_64-secure-enrolled.json) and copies its
# OVMF_VARS_4M.ms.fd template into a per-VM nvram.
efi_os() {
cat <<XML
<os firmware='efi'>
<type arch='x86_64' machine='q35'>hvm</type>
<firmware>
<feature enabled='yes' name='secure-boot'/>
<feature enabled='yes' name='enrolled-keys'/>
</firmware>
</os>
<features><acpi/><apic/><smm state='on'/></features>
XML
}
# --- server VM ---
# Boot order: the installed disk first, the USB second. A blank disk falls
# through to the USB; after the install the disk boots, so autoinstall does
# not loop. CIDATA stays attached for the first-boot playbook.
if ! dom_exists "$SERVER_VM"; then
cat >"$TMP/server.xml" <<XML
<domain type='kvm'>
<name>$SERVER_VM</name>
<memory unit='MiB'>$SERVER_MEM_MIB</memory>
<vcpu>2</vcpu>
$(efi_os)
<cpu mode='host-passthrough'/>
<clock offset='utc'/>
<on_reboot>restart</on_reboot>
<devices>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' discard='unmap'/>
<source file='$POOL_DIR/$SERVER_DISK_VOL'/>
<target dev='vda' bus='virtio'/>
<boot order='1'/>
</disk>
<disk type='file' device='disk'>
<driver name='qemu' type='raw'/>
<source file='$POOL_DIR/$USB_VOL'/>
<target dev='sda' bus='usb' removable='on'/>
<boot order='2'/>
</disk>
<controller type='usb' model='qemu-xhci'/>
<interface type='network'>
<source network='$GE_NET'/>
<mac address='$SERVER_GE_MAC'/>
<model type='virtio'/>
</interface>
<interface type='network'>
<source network='$IMAGING_NET'/>
<mac address='$SERVER_IMAGING_MAC'/>
<model type='virtio'/>
</interface>
<serial type='pty'/>
<console type='pty'/>
<graphics type='vnc' listen='127.0.0.1' autoport='yes'/>
<video><model type='virtio'/></video>
<memballoon model='virtio'/>
<rng model='virtio'><backend model='random'>/dev/urandom</backend></rng>
</devices>
</domain>
XML
virsh define "$TMP/server.xml" >/dev/null
log "defined $SERVER_VM"
fi
# --- client VM ---
if ! dom_exists "$CLIENT_VM"; then
cat >"$TMP/client.xml" <<XML
<domain type='kvm'>
<name>$CLIENT_VM</name>
<memory unit='MiB'>$CLIENT_MEM_MIB</memory>
<vcpu>2</vcpu>
$(efi_os)
<cpu mode='host-passthrough'/>
<clock offset='utc'/>
<devices>
<interface type='network'>
<source network='$IMAGING_NET'/>
<mac address='$CLIENT_MAC'/>
<model type='virtio'/>
<rom enabled='no'/>
<boot order='1'/>
</interface>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='$POOL_DIR/$CLIENT_DISK_VOL'/>
<target dev='vda' bus='virtio'/>
<boot order='2'/>
</disk>
<serial type='pty'/>
<console type='pty'/>
<graphics type='vnc' listen='127.0.0.1' autoport='yes'/>
<video><model type='virtio'/></video>
<memballoon model='virtio'/>
</devices>
</domain>
XML
virsh define "$TMP/client.xml" >/dev/null
log "defined $CLIENT_VM"
fi
# Never attach a pxetest VM anywhere but default NAT / pxetest-imaging.
for vm in "$SERVER_VM" "$CLIENT_VM"; do
while read -r _ type src _ _; do
case "$type:$src" in
network:"$GE_NET") [ "$vm" = "$SERVER_VM" ] || die "$vm is on $GE_NET" ;;
network:"$IMAGING_NET") ;;
*) die "$vm has an interface on $type:$src (only $GE_NET / $IMAGING_NET allowed)" ;;
esac
done < <(virsh domiflist "$vm" | tail -n +3 | grep -v '^$')
done
log "ready. Playbook NIC pins for this site:"
log " pxe_imaging_mac: \"$SERVER_IMAGING_MAC\" (pxe_nic_mac)"
log " pxe_ge_mac: \"$SERVER_GE_MAC\" (corp_nic_mac)"

23
tests/vm/destroy.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Remove every pxetest-* object: both domains (and their nvram), the
# volumes, and the isolated network. Idempotent. Does not touch the
# cached ISO / USB image file (delete $PXETEST_CACHE by hand).
# Usage: tests/vm/destroy.sh
source "$(dirname "$0")/common.sh"
for vm in "$CLIENT_VM" "$SERVER_VM"; do
dom_exists "$vm" || continue
[ "$(dom_state "$vm")" = "shut off" ] || virsh destroy "$vm" >/dev/null
virsh undefine "$vm" --nvram >/dev/null
log "undefined $vm"
done
for v in "$SERVER_DISK_VOL" "$USB_VOL" "$CLIENT_DISK_VOL"; do
vol_exists "$v" || continue
virsh vol-delete --pool "$POOL" "$v" >/dev/null
log "deleted volume $v"
done
if net_exists "$IMAGING_NET"; then
virsh net-destroy "$IMAGING_NET" >/dev/null 2>&1 || true
virsh net-undefine "$IMAGING_NET" >/dev/null
log "removed network $IMAGING_NET"
fi

37
tests/vm/fetch-iso.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Download the Ubuntu live-server ISO into the cache and verify it against
# the release's SHA256SUMS. Idempotent: a verified ISO is not re-fetched.
#
# Usage: tests/vm/fetch-iso.sh
# Env: PXETEST_CACHE, UBUNTU_ISO_NAME (default ubuntu-24.04.5-live-server-amd64.iso)
source "$(dirname "$0")/common.sh"
mkdir -p "$PXETEST_CACHE"
cd "$PXETEST_CACHE"
log "fetching $UBUNTU_RELEASE_URL/SHA256SUMS"
curl -fsSL -m 60 -o SHA256SUMS.new "$UBUNTU_RELEASE_URL/SHA256SUMS" \
|| die "could not download SHA256SUMS (proxy? offline?)"
mv SHA256SUMS.new SHA256SUMS
if ! grep -q "[ *]$UBUNTU_ISO_NAME\$" SHA256SUMS; then
log "available live-server ISOs:"
grep live-server SHA256SUMS | sed 's/^/ /'
die "$UBUNTU_ISO_NAME is not in SHA256SUMS; set UBUNTU_ISO_NAME"
fi
verify() { grep "[ *]$UBUNTU_ISO_NAME\$" SHA256SUMS | sha256sum -c --status; }
if [ -f "$UBUNTU_ISO_NAME" ] && verify; then
log "ISO already present and verified: $UBUNTU_ISO"
exit 0
fi
log "downloading $UBUNTU_RELEASE_URL/$UBUNTU_ISO_NAME (resumable)"
curl -fL -C - --retry 3 -o "$UBUNTU_ISO_NAME" "$UBUNTU_RELEASE_URL/$UBUNTU_ISO_NAME" \
|| die "ISO download failed (proxy? offline?)"
if ! verify; then
rm -f "$UBUNTU_ISO_NAME"
die "SHA256 mismatch for $UBUNTU_ISO_NAME (deleted; run again)"
fi
log "ISO verified: $UBUNTU_ISO"

32
tests/vm/start.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Start pxetest VMs. Idempotent (a running VM is left alone).
# Usage: tests/vm/start.sh [server|client|all] (default: server)
# Start the client only once the server's playbook has finished; both
# together use 6 GiB.
source "$(dirname "$0")/common.sh"
which="${1:-server}"
case "$which" in
server) vms=("$SERVER_VM") ;;
client) vms=("$CLIENT_VM") ;;
all) vms=("$SERVER_VM" "$CLIENT_VM") ;;
*) die "usage: $0 [server|client|all]" ;;
esac
net_exists "$IMAGING_NET" || die "run create.sh first"
net_active "$IMAGING_NET" || virsh net-start "$IMAGING_NET" >/dev/null
assert_imaging_isolated
for vm in "${vms[@]}"; do
dom_exists "$vm" || die "$vm not defined; run create.sh"
if [ "$vm" = "$SERVER_VM" ]; then
vol_exists "$USB_VOL" || die "no $USB_VOL; run build-image.sh"
fi
if [ "$(dom_state "$vm")" = "running" ]; then
log "$vm already running"
else
virsh start "$vm" >/dev/null
log "started $vm (VNC: $(virsh vncdisplay "$vm" 2>/dev/null | head -1))"
fi
done
assert_imaging_isolated

39
tests/vm/status.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# Show the state of every pxetest-* object, the isolation check, the NIC
# MACs, the server's GE-side DHCP lease, and optionally a screenshot.
# Usage: tests/vm/status.sh [--shot DIR]
source "$(dirname "$0")/common.sh"
SHOT_DIR=""
[ "${1:-}" = "--shot" ] && SHOT_DIR="${2:?--shot needs a dir}"
if ! net_exists "$IMAGING_NET"; then n=absent; elif net_active "$IMAGING_NET"; then n=active; else n=inactive; fi
echo "network $IMAGING_NET: $n"
if net_active "$IMAGING_NET"; then
(assert_imaging_isolated && echo " isolation: OK (no forward, no ip, no host IPv4, only vnet ports)")
fi
for v in "$SERVER_DISK_VOL" "$USB_VOL" "$CLIENT_DISK_VOL"; do
if vol_exists "$v"; then
echo "volume $v: $(virsh vol-info --pool "$POOL" "$v" | awk '/Capacity|Allocation/{printf "%s %s %s ", $1, $2, $3}')"
else
echo "volume $v: absent"
fi
done
for vm in "$SERVER_VM" "$CLIENT_VM"; do
if dom_exists "$vm"; then
echo "domain $vm: $(dom_state "$vm")"
virsh domiflist "$vm" | tail -n +3 | grep -v '^$' | sed 's/^/ /'
if [ "$(dom_state "$vm")" = "running" ]; then
echo " VNC $(virsh vncdisplay "$vm" 2>/dev/null | head -1)"
if [ -n "$SHOT_DIR" ]; then
mkdir -p "$SHOT_DIR"
f="$SHOT_DIR/$vm-$(date +%H%M%S).ppm"
virsh screenshot "$vm" "$f" >/dev/null 2>&1 && echo " screenshot $f"
fi
fi
else
echo "domain $vm: absent"
fi
done
echo "GE-side DHCP leases for $SERVER_GE_MAC (on $GE_NET):"
virsh net-dhcp-leases "$GE_NET" --mac "$SERVER_GE_MAC" 2>/dev/null | tail -n +3 | grep -v '^$' | sed 's/^/ /' || true

27
tests/vm/stop.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Shut pxetest VMs down (ACPI, then force after a timeout). Keeps them
# defined. Idempotent.
# Usage: tests/vm/stop.sh [server|client|all] (default: all)
source "$(dirname "$0")/common.sh"
which="${1:-all}"
case "$which" in
server) vms=("$SERVER_VM") ;;
client) vms=("$CLIENT_VM") ;;
all) vms=("$CLIENT_VM" "$SERVER_VM") ;;
*) die "usage: $0 [server|client|all]" ;;
esac
for vm in "${vms[@]}"; do
dom_exists "$vm" || continue
[ "$(dom_state "$vm")" = "shut off" ] && { log "$vm already off"; continue; }
virsh shutdown "$vm" >/dev/null 2>&1 || true
for _ in $(seq 1 60); do
[ "$(dom_state "$vm")" = "shut off" ] && break
sleep 1
done
if [ "$(dom_state "$vm")" != "shut off" ]; then
log "$vm ignored ACPI shutdown; forcing off"
virsh destroy "$vm" >/dev/null
fi
log "$vm off"
done