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:
60
tests/vm/README.md
Normal file
60
tests/vm/README.md
Normal 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
57
tests/vm/build-image.sh
Executable 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
113
tests/vm/common.sh
Executable 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
172
tests/vm/create.sh
Executable 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
23
tests/vm/destroy.sh
Executable 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
37
tests/vm/fetch-iso.sh
Executable 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
32
tests/vm/start.sh
Executable 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
39
tests/vm/status.sh
Executable 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
27
tests/vm/stop.sh
Executable 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
|
||||
Reference in New Issue
Block a user