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:
11
SETUP.md
11
SETUP.md
@@ -80,8 +80,19 @@ sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
|||||||
|
|
||||||
# With WinPE images bundled (single USB, larger drive needed)
|
# With WinPE images bundled (single USB, larger drive needed)
|
||||||
sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04.iso /path/to/winpe-images
|
sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04.iso /path/to/winpe-images
|
||||||
|
|
||||||
|
# Same layout into a disk-image file, no root (dd it to a stick later, or
|
||||||
|
# boot it in a VM: see tests/vm/README.md). SIZE is e.g. 16G or "auto".
|
||||||
|
./scripts/build-usb.sh --image pxe-usb.img auto /path/to/ubuntu-24.04.iso
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Options: `--payload-root DIR` takes offline-packages/, pip-wheels/,
|
||||||
|
boot-files/, boot-tools/, enrollment/, bios-staging/ and drivers-staging/
|
||||||
|
from another checkout; `--skip-enrollment`, `--skip-bios`,
|
||||||
|
`--skip-drivers`, `--skip-boot-tools` leave those out; `--yes` skips the
|
||||||
|
prompt. The ISO volume label and El Torito load sizes are read from the
|
||||||
|
ISO, so any 24.04.x point release works.
|
||||||
|
|
||||||
This creates a bootable USB with:
|
This creates a bootable USB with:
|
||||||
- **Partition 1:** Ubuntu Server installer
|
- **Partition 1:** Ubuntu Server installer
|
||||||
- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
|
- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
|
||||||
|
|||||||
@@ -225,9 +225,12 @@ echo " Copied playbook/"
|
|||||||
|
|
||||||
# Flask webapp
|
# Flask webapp
|
||||||
if [ -d "$WEBAPP_DIR" ]; then
|
if [ -d "$WEBAPP_DIR" ]; then
|
||||||
mkdir -p "$PXE_DATA/webapp"
|
# Whole tree: app.py imports config, services/ and views/, and the
|
||||||
cp "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$PXE_DATA/webapp/"
|
# Blancco evidence timer runs blancco_evidence_monthly.py.
|
||||||
cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$PXE_DATA/webapp/"
|
rm -rf "$PXE_DATA/webapp"
|
||||||
|
cp -r "$WEBAPP_DIR" "$PXE_DATA/webapp"
|
||||||
|
find "$PXE_DATA/webapp" -name __pycache__ -type d -prune -exec rm -rf {} +
|
||||||
|
find "$PXE_DATA/webapp" -name '*.py[co]' -delete
|
||||||
echo " Copied webapp/"
|
echo " Copied webapp/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,233 +1,511 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# build-usb.sh — Build a bootable PXE-server installer USB
|
# build-usb.sh - Build a bootable PXE-server installer USB (or disk image)
|
||||||
#
|
#
|
||||||
# Creates a two-partition USB:
|
# Creates a two-partition layout:
|
||||||
# Partition 1: Ubuntu Server 24.04 installer (ISO contents)
|
# Partition 1: Ubuntu Server 24.04 installer (ISO contents, autoinstall patched)
|
||||||
# Partition 2: CIDATA volume (autoinstall config, .debs, playbook)
|
# Partition N: CIDATA volume (autoinstall config, .debs, playbook, webapp...)
|
||||||
#
|
#
|
||||||
# The target machine boots from this USB, Ubuntu auto-installs with
|
# The target machine boots from this USB, Ubuntu auto-installs with
|
||||||
# cloud-init (user-data/meta-data from CIDATA), installs offline .debs,
|
# cloud-init (user-data/meta-data from CIDATA), installs offline .debs,
|
||||||
# and on first boot runs the Ansible playbook to configure PXE services.
|
# and on first boot runs the Ansible playbook to configure PXE services.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# sudo ./build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
# sudo ./build-usb.sh [options] /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso [winpe-dir]
|
||||||
|
# ./build-usb.sh [options] --image out.img SIZE /path/to/ubuntu.iso [winpe-dir]
|
||||||
#
|
#
|
||||||
# WARNING: This will ERASE the target USB device.
|
# Image mode writes the same layout into a regular FILE and needs no root:
|
||||||
|
# the CIDATA filesystem is built with mkfs.vfat on an image file and filled
|
||||||
|
# with mtools (mcopy), then placed after the ISO with sfdisk on the file.
|
||||||
|
# SIZE is a byte count with an optional K/M/G suffix (1024-based), or
|
||||||
|
# "auto" to size the image to fit the payload.
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --image FILE SIZE build a disk image file instead of writing a device
|
||||||
|
# --payload-root DIR take the gitignored payload dirs (offline-packages/,
|
||||||
|
# pip-wheels/, boot-files/, boot-tools/, enrollment/,
|
||||||
|
# bios-staging/, drivers-staging/) from DIR instead of
|
||||||
|
# the repo root (e.g. a main checkout next to a worktree)
|
||||||
|
# --skip-enrollment do not copy enrollment/
|
||||||
|
# --skip-bios do not copy bios-staging/
|
||||||
|
# --skip-drivers do not copy drivers-staging/
|
||||||
|
# --skip-boot-tools do not copy boot-tools/
|
||||||
|
# --yes do not ask for confirmation (device mode still asks
|
||||||
|
# YES for a non-removable device)
|
||||||
|
#
|
||||||
|
# WARNING: device mode ERASES the target USB device.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
|
REPO_ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
|
||||||
AUTOINSTALL_DIR="$REPO_ROOT/autoinstall"
|
AUTOINSTALL_DIR="$REPO_ROOT/autoinstall"
|
||||||
PLAYBOOK_DIR="$REPO_ROOT/playbook"
|
PLAYBOOK_DIR="$REPO_ROOT/playbook"
|
||||||
OFFLINE_PKG_DIR="$REPO_ROOT/offline-packages"
|
WEBAPP_DIR="$REPO_ROOT/webapp"
|
||||||
|
|
||||||
# --- Validate arguments ---
|
usage() {
|
||||||
if [ $# -lt 2 ]; then
|
echo "Usage: sudo $0 [options] /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
|
||||||
echo "Usage: sudo $0 /dev/sdX /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
|
echo " $0 [options] --image out.img SIZE|auto /path/to/ubuntu-24.04.iso [/path/to/winpe-images]"
|
||||||
echo ""
|
echo ""
|
||||||
echo " The optional third argument is the path to WinPE deployment content"
|
echo " Options: --payload-root DIR, --skip-enrollment, --skip-bios,"
|
||||||
|
echo " --skip-drivers, --skip-boot-tools, --yes"
|
||||||
|
echo ""
|
||||||
|
echo " The optional last argument is the path to WinPE deployment content"
|
||||||
echo " (e.g., the mounted Media Creator LITE USB). If provided, the images"
|
echo " (e.g., the mounted Media Creator LITE USB). If provided, the images"
|
||||||
echo " will be bundled onto the CIDATA partition for automatic import."
|
echo " will be bundled onto the CIDATA partition for automatic import."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Available removable devices:"
|
echo "Available removable devices:"
|
||||||
lsblk -d -o NAME,SIZE,TRAN,RM | grep -E '^\S+\s+\S+\s+(usb)\s+1'
|
lsblk -d -o NAME,SIZE,TRAN,RM 2>/dev/null | grep -E '^\S+\s+\S+\s+(usb)\s+1' || true
|
||||||
exit 1
|
}
|
||||||
|
|
||||||
|
# --- Parse arguments ---
|
||||||
|
IMAGE_MODE=0
|
||||||
|
IMAGE_FILE=""
|
||||||
|
IMAGE_SIZE_ARG=""
|
||||||
|
PAYLOAD_ROOT="$REPO_ROOT"
|
||||||
|
SKIP_ENROLLMENT=0
|
||||||
|
SKIP_BIOS=0
|
||||||
|
SKIP_DRIVERS=0
|
||||||
|
SKIP_BOOT_TOOLS=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
POSITIONAL=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--image)
|
||||||
|
[ $# -ge 3 ] || { usage; exit 1; }
|
||||||
|
IMAGE_MODE=1; IMAGE_FILE="$2"; IMAGE_SIZE_ARG="$3"; shift 3 ;;
|
||||||
|
--payload-root)
|
||||||
|
[ $# -ge 2 ] || { usage; exit 1; }
|
||||||
|
PAYLOAD_ROOT="$(cd "$2" && pwd)"; shift 2 ;;
|
||||||
|
--skip-enrollment) SKIP_ENROLLMENT=1; shift ;;
|
||||||
|
--skip-bios) SKIP_BIOS=1; shift ;;
|
||||||
|
--skip-drivers) SKIP_DRIVERS=1; shift ;;
|
||||||
|
--skip-boot-tools) SKIP_BOOT_TOOLS=1; shift ;;
|
||||||
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--) shift; POSITIONAL+=("$@"); break ;;
|
||||||
|
-*) echo "ERROR: unknown option $1"; usage; exit 1 ;;
|
||||||
|
*) POSITIONAL+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
|
if [ ${#POSITIONAL[@]} -lt 1 ]; then usage; exit 1; fi
|
||||||
|
USB_DEV=""
|
||||||
|
ISO_PATH="${POSITIONAL[0]}"
|
||||||
|
WINPE_SOURCE="${POSITIONAL[1]:-}"
|
||||||
|
else
|
||||||
|
if [ ${#POSITIONAL[@]} -lt 2 ]; then usage; exit 1; fi
|
||||||
|
USB_DEV="${POSITIONAL[0]}"
|
||||||
|
ISO_PATH="${POSITIONAL[1]}"
|
||||||
|
WINPE_SOURCE="${POSITIONAL[2]:-}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
USB_DEV="$1"
|
OFFLINE_PKG_DIR="$PAYLOAD_ROOT/offline-packages"
|
||||||
ISO_PATH="$2"
|
PIP_WHEELS_DIR="$PAYLOAD_ROOT/pip-wheels"
|
||||||
WINPE_SOURCE="${3:-}"
|
BOOT_FILES_DIR="$PAYLOAD_ROOT/boot-files"
|
||||||
|
BOOT_TOOLS_DIR="$PAYLOAD_ROOT/boot-tools"
|
||||||
|
ENROLLMENT_DIR="$PAYLOAD_ROOT/enrollment"
|
||||||
|
BIOS_DIR="$PAYLOAD_ROOT/bios-staging"
|
||||||
|
DRIVERS_DIR="$PAYLOAD_ROOT/drivers-staging"
|
||||||
|
[ "$SKIP_ENROLLMENT" -eq 1 ] && ENROLLMENT_DIR=""
|
||||||
|
[ "$SKIP_BIOS" -eq 1 ] && BIOS_DIR=""
|
||||||
|
[ "$SKIP_DRIVERS" -eq 1 ] && DRIVERS_DIR=""
|
||||||
|
[ "$SKIP_BOOT_TOOLS" -eq 1 ] && BOOT_TOOLS_DIR=""
|
||||||
|
|
||||||
# Safety checks
|
# FAT32 has a 4GB max file size; files larger than this are split into
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
# chunks that the playbook reassembles with `cat`.
|
||||||
echo "ERROR: Must run as root (sudo)."
|
FAT32_MAX=$((3500 * 1024 * 1024)) # 3500 MiB chunks, safely under 4GiB
|
||||||
exit 1
|
SECTOR_SIZE=512
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -b "$USB_DEV" ]; then
|
|
||||||
echo "ERROR: $USB_DEV is not a block device."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
# --- Safety checks ---
|
||||||
if [ ! -f "$ISO_PATH" ]; then
|
if [ ! -f "$ISO_PATH" ]; then
|
||||||
echo "ERROR: ISO not found at $ISO_PATH"
|
echo "ERROR: ISO not found at $ISO_PATH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
ISO_PATH="$(cd "$(dirname "$ISO_PATH")" && pwd)/$(basename "$ISO_PATH")"
|
||||||
|
|
||||||
# Verify it's a removable device (safety against wiping system disks)
|
need_tools=(xorriso 7z sfdisk mkfs.vfat)
|
||||||
REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null || echo "0")
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
if [ "$REMOVABLE" != "1" ]; then
|
need_tools+=(mcopy mmd mdir truncate)
|
||||||
echo "WARNING: $USB_DEV does not appear to be a removable device."
|
else
|
||||||
read -rp "Are you SURE you want to erase $USB_DEV? (type YES): " CONFIRM
|
need_tools+=(partprobe)
|
||||||
if [ "$CONFIRM" != "YES" ]; then
|
fi
|
||||||
|
for t in "${need_tools[@]}"; do
|
||||||
|
if ! command -v "$t" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: required tool '$t' not found in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
|
if [ -e "$IMAGE_FILE" ] && [ ! -f "$IMAGE_FILE" ]; then
|
||||||
|
echo "ERROR: $IMAGE_FILE exists and is not a regular file (image mode never writes devices)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
IMAGE_FILE="$(cd "$(dirname "$IMAGE_FILE")" && pwd)/$(basename "$IMAGE_FILE")"
|
||||||
|
else
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "ERROR: Must run as root (sudo). (Use --image to build a file without root.)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -b "$USB_DEV" ]; then
|
||||||
|
echo "ERROR: $USB_DEV is not a block device."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Verify it's a removable device (safety against wiping system disks)
|
||||||
|
REMOVABLE=$(lsblk -nd -o RM "$USB_DEV" 2>/dev/null | tr -d '[:space:]' || echo "0")
|
||||||
|
if [ "$REMOVABLE" != "1" ]; then
|
||||||
|
echo "WARNING: $USB_DEV does not appear to be a removable device."
|
||||||
|
read -rp "Are you SURE you want to erase $USB_DEV? (type YES): " CONFIRM
|
||||||
|
if [ "$CONFIRM" != "YES" ]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify required source files exist
|
||||||
|
for f in "$AUTOINSTALL_DIR/user-data" "$AUTOINSTALL_DIR/meta-data" "$PLAYBOOK_DIR/pxe_server_setup.yml"; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "ERROR: $f not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo "PXE Server USB Builder"
|
||||||
|
echo "============================================"
|
||||||
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
|
echo "Image file : $IMAGE_FILE (size: $IMAGE_SIZE_ARG)"
|
||||||
|
else
|
||||||
|
echo "USB Device : $USB_DEV"
|
||||||
|
fi
|
||||||
|
echo "ISO : $ISO_PATH"
|
||||||
|
echo "Source Dir : $REPO_ROOT"
|
||||||
|
echo "Payloads : $PAYLOAD_ROOT"
|
||||||
|
echo ""
|
||||||
|
if [ "$ASSUME_YES" -ne 1 ]; then
|
||||||
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
|
echo "This will overwrite $IMAGE_FILE."
|
||||||
|
else
|
||||||
|
echo "This will ERASE all data on $USB_DEV."
|
||||||
|
fi
|
||||||
|
read -rp "Continue? (y/N): " PROCEED
|
||||||
|
if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
|
||||||
echo "Aborted."
|
echo "Aborted."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify required source files exist
|
# Work dir: next to the output image in image mode (multi-GB, keep it off
|
||||||
if [ ! -f "$AUTOINSTALL_DIR/user-data" ]; then
|
# a tmpfs /tmp), else the default temp dir.
|
||||||
echo "ERROR: user-data not found at $AUTOINSTALL_DIR/user-data"
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
exit 1
|
WORK=$(mktemp -d "$(dirname "$IMAGE_FILE")/.build-usb.XXXXXX")
|
||||||
fi
|
else
|
||||||
|
WORK=$(mktemp -d)
|
||||||
if [ ! -f "$AUTOINSTALL_DIR/meta-data" ]; then
|
|
||||||
echo "ERROR: meta-data not found at $AUTOINSTALL_DIR/meta-data"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$PLAYBOOK_DIR/pxe_server_setup.yml" ]; then
|
|
||||||
echo "ERROR: pxe_server_setup.yml not found at $PLAYBOOK_DIR/"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "============================================"
|
|
||||||
echo "PXE Server USB Builder"
|
|
||||||
echo "============================================"
|
|
||||||
echo "USB Device : $USB_DEV"
|
|
||||||
echo "ISO : $ISO_PATH"
|
|
||||||
echo "Source Dir : $REPO_ROOT"
|
|
||||||
echo ""
|
|
||||||
echo "This will ERASE all data on $USB_DEV."
|
|
||||||
read -rp "Continue? (y/N): " PROCEED
|
|
||||||
if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
MOUNT_POINT=""
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$MOUNT_POINT" ] && mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
|
||||||
|
umount "$MOUNT_POINT" || true
|
||||||
|
fi
|
||||||
|
[ -n "$MOUNT_POINT" ] && rmdir "$MOUNT_POINT" 2>/dev/null || true
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
# --- Unmount any existing partitions ---
|
# --- Unmount any existing partitions ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "[1/6] Unmounting existing partitions on $USB_DEV..."
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
for part in "${USB_DEV}"*; do
|
echo "[1/6] Unmounting existing partitions on $USB_DEV..."
|
||||||
umount "$part" 2>/dev/null || true
|
for part in "${USB_DEV}"*; do
|
||||||
done
|
umount "$part" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "[1/6] Image mode: nothing to unmount"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Rebuild ISO with 'autoinstall' kernel parameter ---
|
# --- Rebuild ISO with 'autoinstall' kernel parameter ---
|
||||||
echo "[2/6] Rebuilding ISO with autoinstall kernel parameter..."
|
echo "[2/6] Rebuilding ISO with autoinstall kernel parameter..."
|
||||||
|
|
||||||
ISO_WORK=$(mktemp -d)
|
# Volume label and El Torito load sizes come from the source ISO, so a
|
||||||
7z -y x "$ISO_PATH" -o"$ISO_WORK/iso" >/dev/null 2>&1
|
# point release (24.04.3, 24.04.5, ...) does not need a script edit.
|
||||||
mv "$ISO_WORK/iso/[BOOT]" "$ISO_WORK/BOOT"
|
ELTORITO_REPORT=$(xorriso -indev "$ISO_PATH" -report_el_torito as_mkisofs 2>/dev/null)
|
||||||
chmod -R u+w "$ISO_WORK/iso"
|
ISO_VOLID=$(printf '%s\n' "$ELTORITO_REPORT" | sed -n "s/^-V '\(.*\)'\$/\1/p" | head -1)
|
||||||
|
# Load sizes in order: first follows -b (BIOS), last follows -e (EFI).
|
||||||
|
mapfile -t LOAD_SIZES < <(printf '%s\n' "$ELTORITO_REPORT" | sed -n 's/^-boot-load-size \([0-9][0-9]*\)$/\1/p')
|
||||||
|
if [ -z "$ISO_VOLID" ] || [ ${#LOAD_SIZES[@]} -lt 2 ]; then
|
||||||
|
echo "ERROR: could not read volume id / El Torito load sizes from $ISO_PATH"
|
||||||
|
echo " (xorriso -indev ISO -report_el_torito as_mkisofs)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BIOS_LOAD_SIZE="${LOAD_SIZES[0]}"
|
||||||
|
EFI_LOAD_SIZE="${LOAD_SIZES[${#LOAD_SIZES[@]}-1]}"
|
||||||
|
echo " Volume id: $ISO_VOLID (boot-load-size BIOS=$BIOS_LOAD_SIZE EFI=$EFI_LOAD_SIZE)"
|
||||||
|
|
||||||
|
7z -y x "$ISO_PATH" -o"$WORK/iso" >/dev/null 2>&1
|
||||||
|
mv "$WORK/iso/[BOOT]" "$WORK/BOOT"
|
||||||
|
chmod -R u+w "$WORK/iso"
|
||||||
|
|
||||||
# Patch grub.cfg: add 'autoinstall' to kernel cmdline, reduce timeout
|
# Patch grub.cfg: add 'autoinstall' to kernel cmdline, reduce timeout
|
||||||
sed -i 's|linux\t/casper/vmlinuz ---|linux\t/casper/vmlinuz autoinstall ---|' "$ISO_WORK/iso/boot/grub/grub.cfg"
|
GRUB_CFG="$WORK/iso/boot/grub/grub.cfg"
|
||||||
sed -i 's/^set timeout=30/set timeout=5/' "$ISO_WORK/iso/boot/grub/grub.cfg"
|
sed -i 's|linux\t/casper/vmlinuz ---|linux\t/casper/vmlinuz autoinstall ---|' "$GRUB_CFG"
|
||||||
|
sed -i 's/^set timeout=30/set timeout=5/' "$GRUB_CFG"
|
||||||
|
if ! grep -q 'vmlinuz.*autoinstall' "$GRUB_CFG"; then
|
||||||
|
echo "ERROR: grub.cfg patch failed: no 'autoinstall' on a kernel line in boot/grub/grub.cfg"
|
||||||
|
echo " The ISO's grub.cfg format changed; update the sed in this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
PATCHED_ISO="$ISO_WORK/patched.iso"
|
PATCHED_ISO="$WORK/patched.iso"
|
||||||
cd "$ISO_WORK/iso"
|
(
|
||||||
xorriso -as mkisofs -r \
|
cd "$WORK/iso"
|
||||||
-V 'Ubuntu-Server 24.04.3 LTS amd64' \
|
xorriso -as mkisofs -r \
|
||||||
-o "$PATCHED_ISO" \
|
-V "$ISO_VOLID" \
|
||||||
--grub2-mbr ../BOOT/1-Boot-NoEmul.img \
|
-o "$PATCHED_ISO" \
|
||||||
--protective-msdos-label \
|
--grub2-mbr ../BOOT/1-Boot-NoEmul.img \
|
||||||
-partition_cyl_align off \
|
--protective-msdos-label \
|
||||||
-partition_offset 16 \
|
-partition_cyl_align off \
|
||||||
--mbr-force-bootable \
|
-partition_offset 16 \
|
||||||
-append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b ../BOOT/2-Boot-NoEmul.img \
|
--mbr-force-bootable \
|
||||||
-appended_part_as_gpt \
|
-append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b ../BOOT/2-Boot-NoEmul.img \
|
||||||
-iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \
|
-appended_part_as_gpt \
|
||||||
-c '/boot.catalog' \
|
-iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \
|
||||||
-b '/boot/grub/i386-pc/eltorito.img' \
|
-c '/boot.catalog' \
|
||||||
-no-emul-boot \
|
-b '/boot/grub/i386-pc/eltorito.img' \
|
||||||
-boot-load-size 4 \
|
-no-emul-boot \
|
||||||
-boot-info-table \
|
-boot-load-size "$BIOS_LOAD_SIZE" \
|
||||||
--grub2-boot-info \
|
-boot-info-table \
|
||||||
-eltorito-alt-boot \
|
--grub2-boot-info \
|
||||||
-e '--interval:appended_partition_2:::' \
|
-eltorito-alt-boot \
|
||||||
-no-emul-boot \
|
-e '--interval:appended_partition_2:::' \
|
||||||
-boot-load-size 10160 \
|
-no-emul-boot \
|
||||||
. 2>/dev/null
|
-boot-load-size "$EFI_LOAD_SIZE" \
|
||||||
cd "$REPO_ROOT"
|
. 2>"$WORK/xorriso.log"
|
||||||
|
) || { echo "ERROR: xorriso failed:"; tail -20 "$WORK/xorriso.log"; exit 1; }
|
||||||
|
rm -rf "$WORK/iso"
|
||||||
echo " ISO rebuilt with 'autoinstall' kernel param and 5s GRUB timeout"
|
echo " ISO rebuilt with 'autoinstall' kernel param and 5s GRUB timeout"
|
||||||
|
|
||||||
echo " Writing patched ISO to $USB_DEV..."
|
|
||||||
dd if="$PATCHED_ISO" of="$USB_DEV" bs=4M status=progress oflag=sync
|
|
||||||
sync
|
|
||||||
ISO_SIZE=$(stat -c%s "$PATCHED_ISO")
|
ISO_SIZE=$(stat -c%s "$PATCHED_ISO")
|
||||||
rm -rf "$ISO_WORK"
|
|
||||||
|
|
||||||
# --- Find the end of the ISO to create CIDATA partition ---
|
|
||||||
echo "[3/6] Creating CIDATA partition after ISO data..."
|
|
||||||
|
|
||||||
# Get ISO size in bytes and calculate the start sector for the new partition
|
|
||||||
SECTOR_SIZE=512
|
|
||||||
# Start the CIDATA partition 1MB after the ISO ends (alignment)
|
# Start the CIDATA partition 1MB after the ISO ends (alignment)
|
||||||
START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 ))
|
START_SECTOR=$(( (ISO_SIZE / SECTOR_SIZE) + 2048 ))
|
||||||
|
|
||||||
# Use sfdisk to append a new partition
|
# --- Payload size (image mode sizing) ---
|
||||||
|
src_bytes() {
|
||||||
|
# Apparent size in bytes of the given paths (missing ones count 0)
|
||||||
|
local total=0 p n
|
||||||
|
for p in "$@"; do
|
||||||
|
[ -n "$p" ] && [ -e "$p" ] || continue
|
||||||
|
n=$(du -sbL "$p" 2>/dev/null | cut -f1)
|
||||||
|
total=$((total + ${n:-0}))
|
||||||
|
done
|
||||||
|
echo "$total"
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_size() {
|
||||||
|
local s="$1" num unit
|
||||||
|
num="${s%[KkMmGgTt]}"
|
||||||
|
unit="${s#"$num"}"
|
||||||
|
[[ "$num" =~ ^[0-9]+$ ]] || { echo "ERROR: bad size '$s'" >&2; return 1; }
|
||||||
|
case "$unit" in
|
||||||
|
"") echo "$num" ;;
|
||||||
|
K|k) echo $((num * 1024)) ;;
|
||||||
|
M|m) echo $((num * 1024 * 1024)) ;;
|
||||||
|
G|g) echo $((num * 1024 * 1024 * 1024)) ;;
|
||||||
|
T|t) echo $((num * 1024 * 1024 * 1024 * 1024)) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
|
PAYLOAD_BYTES=$(src_bytes "$AUTOINSTALL_DIR/user-data" "$AUTOINSTALL_DIR/meta-data" \
|
||||||
|
"$OFFLINE_PKG_DIR" "$PLAYBOOK_DIR" "$WEBAPP_DIR" "$PIP_WHEELS_DIR" \
|
||||||
|
"$BOOT_FILES_DIR" "$BOOT_TOOLS_DIR" "$ENROLLMENT_DIR" "$BIOS_DIR" \
|
||||||
|
"$DRIVERS_DIR" "$WINPE_SOURCE")
|
||||||
|
if [ "$IMAGE_SIZE_ARG" = "auto" ]; then
|
||||||
|
# payload + 10% + 256 MiB FAT/cluster slack, rounded up to 1 MiB
|
||||||
|
CIDATA_BYTES=$(( PAYLOAD_BYTES + PAYLOAD_BYTES / 10 + 256 * 1024 * 1024 ))
|
||||||
|
IMAGE_BYTES=$(( START_SECTOR * SECTOR_SIZE + CIDATA_BYTES + 2 * 1024 * 1024 ))
|
||||||
|
else
|
||||||
|
IMAGE_BYTES=$(parse_size "$IMAGE_SIZE_ARG")
|
||||||
|
fi
|
||||||
|
IMAGE_BYTES=$(( (IMAGE_BYTES + 1048575) / 1048576 * 1048576 ))
|
||||||
|
MIN_BYTES=$(( START_SECTOR * SECTOR_SIZE + PAYLOAD_BYTES + 64 * 1024 * 1024 ))
|
||||||
|
if [ "$IMAGE_BYTES" -lt "$MIN_BYTES" ]; then
|
||||||
|
echo "ERROR: image size $((IMAGE_BYTES / 1048576)) MiB too small; need at least $((MIN_BYTES / 1048576)) MiB"
|
||||||
|
echo " (ISO $((ISO_SIZE / 1048576)) MiB + payload $((PAYLOAD_BYTES / 1048576)) MiB). Use SIZE=auto."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " Payload: $((PAYLOAD_BYTES / 1048576)) MiB, image: $((IMAGE_BYTES / 1048576)) MiB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Write the ISO ---
|
||||||
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
|
echo " Writing patched ISO to $IMAGE_FILE..."
|
||||||
|
rm -f "$IMAGE_FILE"
|
||||||
|
truncate -s "$IMAGE_BYTES" "$IMAGE_FILE"
|
||||||
|
dd if="$PATCHED_ISO" of="$IMAGE_FILE" bs=4M conv=notrunc,sparse status=none
|
||||||
|
TARGET="$IMAGE_FILE"
|
||||||
|
else
|
||||||
|
echo " Writing patched ISO to $USB_DEV..."
|
||||||
|
dd if="$PATCHED_ISO" of="$USB_DEV" bs=4M status=progress oflag=sync
|
||||||
|
sync
|
||||||
|
TARGET="$USB_DEV"
|
||||||
|
fi
|
||||||
|
rm -f "$PATCHED_ISO"
|
||||||
|
|
||||||
|
# --- Create the CIDATA partition after the ISO data ---
|
||||||
|
echo "[3/6] Creating CIDATA partition after ISO data..."
|
||||||
echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB"
|
echo " ISO size: $((ISO_SIZE / 1024 / 1024)) MB"
|
||||||
echo " CIDATA partition starts at sector $START_SECTOR"
|
echo " CIDATA partition starts at sector $START_SECTOR"
|
||||||
|
|
||||||
# Add a new partition using sfdisk --append
|
# A failed append must stop the build: the old fallback ("last partition")
|
||||||
echo "${START_SECTOR},+,L" | sfdisk --append "$USB_DEV" --no-reread 2>/dev/null || true
|
# then formatted the ISO's own EFI partition.
|
||||||
partprobe "$USB_DEV"
|
if ! echo "${START_SECTOR},+,L" | sfdisk --append "$TARGET" --no-reread >"$WORK/sfdisk.log" 2>&1; then
|
||||||
sleep 2
|
echo "ERROR: sfdisk --append failed on $TARGET:"
|
||||||
|
sed 's/^/ /' "$WORK/sfdisk.log"
|
||||||
# Determine the new partition name (could be sdX3, sdX4, etc.)
|
exit 1
|
||||||
CIDATA_PART=""
|
|
||||||
for part in "${USB_DEV}"[0-9]*; do
|
|
||||||
# Find the partition that starts at or after our start sector
|
|
||||||
PART_START=$(sfdisk -d "$USB_DEV" 2>/dev/null | grep "$part" | grep -o 'start=[[:space:]]*[0-9]*' | grep -o '[0-9]*')
|
|
||||||
if [ -n "$PART_START" ] && [ "$PART_START" -ge "$START_SECTOR" ]; then
|
|
||||||
CIDATA_PART="$part"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Fallback: use the last partition
|
|
||||||
if [ -z "$CIDATA_PART" ]; then
|
|
||||||
CIDATA_PART=$(lsblk -ln -o NAME "$USB_DEV" | tail -1)
|
|
||||||
CIDATA_PART="/dev/$CIDATA_PART"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " CIDATA partition: $CIDATA_PART"
|
# Find the partition that starts exactly at our start sector.
|
||||||
|
# sfdisk -d lines look like: "/dev/sdb3 : start= 6324224, size= ..., type=..."
|
||||||
|
read_new_part() {
|
||||||
|
sfdisk -d "$TARGET" 2>/dev/null | awk -v want="$START_SECTOR" '
|
||||||
|
/ : start=/ {
|
||||||
|
node = $1
|
||||||
|
line = $0
|
||||||
|
sub(/.*start=[ \t]*/, "", line); start = line; sub(/[^0-9].*/, "", start)
|
||||||
|
line = $0
|
||||||
|
sub(/.*size=[ \t]*/, "", line); size = line; sub(/[^0-9].*/, "", size)
|
||||||
|
if (start + 0 == want + 0) { print node, start, size; exit }
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
NEW_PART=$(read_new_part)
|
||||||
|
if [ -z "$NEW_PART" ]; then
|
||||||
|
echo "ERROR: no partition starting at sector $START_SECTOR after sfdisk --append. Table:"
|
||||||
|
sfdisk -d "$TARGET" 2>&1 | sed 's/^/ /'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
read -r PART_NODE PART_START PART_SECTORS <<<"$NEW_PART"
|
||||||
|
|
||||||
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
|
partprobe "$USB_DEV"
|
||||||
|
sleep 2
|
||||||
|
CIDATA_PART="$PART_NODE"
|
||||||
|
if [ ! -b "$CIDATA_PART" ]; then
|
||||||
|
echo "ERROR: $CIDATA_PART (from sfdisk) is not a block device after partprobe"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Refuse to format anything that starts before the computed start sector
|
||||||
|
# (the ISO9660 data and its EFI partition live there).
|
||||||
|
SYS_START=$(cat "/sys/class/block/$(basename "$CIDATA_PART")/start" 2>/dev/null || echo 0)
|
||||||
|
if [ "$SYS_START" -lt "$START_SECTOR" ]; then
|
||||||
|
echo "ERROR: refusing to format $CIDATA_PART: it starts at sector $SYS_START, before $START_SECTOR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " CIDATA partition: $CIDATA_PART"
|
||||||
|
else
|
||||||
|
if [ "$PART_START" -lt "$START_SECTOR" ]; then
|
||||||
|
echo "ERROR: refusing to format partition at sector $PART_START (< $START_SECTOR)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " CIDATA partition: $PART_NODE (sectors $PART_START + $PART_SECTORS)"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Format CIDATA partition ---
|
# --- Format CIDATA partition ---
|
||||||
echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..."
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART"
|
echo "[4/6] Formatting $CIDATA_PART as FAT32 (label: CIDATA)..."
|
||||||
|
mkfs.vfat -F 32 -n CIDATA "$CIDATA_PART"
|
||||||
|
MOUNT_POINT=$(mktemp -d)
|
||||||
|
mount "$CIDATA_PART" "$MOUNT_POINT"
|
||||||
|
else
|
||||||
|
echo "[4/6] Building FAT32 CIDATA filesystem image ($((PART_SECTORS / 2048)) MiB)..."
|
||||||
|
CIDATA_IMG="$WORK/cidata.img"
|
||||||
|
# mkfs.vfat -C takes the size in 1 KiB blocks
|
||||||
|
mkfs.vfat -F 32 -n CIDATA -C "$CIDATA_IMG" $((PART_SECTORS / 2)) >/dev/null
|
||||||
|
export MTOOLS_SKIP_CHECK=1
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Mount and copy files ---
|
# --- Target-neutral copy helpers ---
|
||||||
|
# Paths are relative to the CIDATA root ("" = root).
|
||||||
|
t_mkdir() {
|
||||||
|
local rel="$1" acc="" part
|
||||||
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
|
mkdir -p "$MOUNT_POINT/$rel"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local IFS=/
|
||||||
|
for part in $rel; do
|
||||||
|
[ -n "$part" ] || continue
|
||||||
|
acc="$acc/$part"
|
||||||
|
mmd -i "$CIDATA_IMG" "::$acc" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
# t_cp DEST_REL_DIR SRC... : copy files/dirs (recursively) into a CIDATA dir
|
||||||
|
t_cp() {
|
||||||
|
local dest="$1"; shift
|
||||||
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
|
cp -r "$@" "$MOUNT_POINT/$dest/"
|
||||||
|
else
|
||||||
|
mcopy -i "$CIDATA_IMG" -s -Q -D o "$@" "::/${dest:+$dest/}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# t_cp_split SRC DEST_REL_DIR : copy one file, splitting it if over FAT32_MAX
|
||||||
|
SPLIT_COUNT=0
|
||||||
|
t_cp_split() {
|
||||||
|
local f="$1" dest="$2" bn
|
||||||
|
bn="$(basename "$f")"
|
||||||
|
if [ "$(stat -L -c%s "$f")" -gt "$FAT32_MAX" ]; then
|
||||||
|
echo " Splitting $bn (>$((FAT32_MAX / 1024 / 1024))M) into chunks..."
|
||||||
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
|
split -b "$FAT32_MAX" -d -a 2 "$f" "$MOUNT_POINT/$dest/${bn}.part."
|
||||||
|
else
|
||||||
|
mkdir -p "$WORK/split"
|
||||||
|
split -b "$FAT32_MAX" -d -a 2 "$f" "$WORK/split/${bn}.part."
|
||||||
|
t_cp "$dest" "$WORK/split/"*
|
||||||
|
rm -rf "$WORK/split"
|
||||||
|
fi
|
||||||
|
SPLIT_COUNT=$((SPLIT_COUNT + 1))
|
||||||
|
else
|
||||||
|
t_cp "$dest" "$f"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Copy files ---
|
||||||
echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
|
echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
|
||||||
MOUNT_POINT=$(mktemp -d)
|
|
||||||
mount "$CIDATA_PART" "$MOUNT_POINT"
|
|
||||||
|
|
||||||
# Copy cloud-init files
|
# Copy cloud-init files
|
||||||
cp "$AUTOINSTALL_DIR/user-data" "$MOUNT_POINT/"
|
t_cp "" "$AUTOINSTALL_DIR/user-data" "$AUTOINSTALL_DIR/meta-data"
|
||||||
cp "$AUTOINSTALL_DIR/meta-data" "$MOUNT_POINT/"
|
|
||||||
|
|
||||||
# Copy offline .deb packages into packages/ subdirectory
|
# Copy offline .deb packages into packages/ subdirectory
|
||||||
mkdir -p "$MOUNT_POINT/packages"
|
t_mkdir packages
|
||||||
DEB_COUNT=0
|
DEB_COUNT=0
|
||||||
|
if [ -d "$OFFLINE_PKG_DIR" ] && compgen -G "$OFFLINE_PKG_DIR/*.deb" >/dev/null; then
|
||||||
if [ -d "$OFFLINE_PKG_DIR" ]; then
|
t_cp packages "$OFFLINE_PKG_DIR"/*.deb
|
||||||
for deb in "$OFFLINE_PKG_DIR"/*.deb; do
|
DEB_COUNT=$(find "$OFFLINE_PKG_DIR" -maxdepth 1 -name '*.deb' | wc -l)
|
||||||
if [ -f "$deb" ]; then
|
else
|
||||||
cp "$deb" "$MOUNT_POINT/packages/"
|
echo " WARNING: no .deb packages in $OFFLINE_PKG_DIR (run download-packages.sh)"
|
||||||
DEB_COUNT=$((DEB_COUNT + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
echo " Copied $DEB_COUNT .deb packages to packages/"
|
echo " Copied $DEB_COUNT .deb packages to packages/"
|
||||||
|
|
||||||
# Copy playbook directory
|
# Copy playbook directory
|
||||||
cp -r "$PLAYBOOK_DIR" "$MOUNT_POINT/playbook"
|
t_cp "" "$PLAYBOOK_DIR"
|
||||||
echo " Copied playbook/"
|
echo " Copied playbook/"
|
||||||
|
|
||||||
# Copy webapp
|
# Copy the whole webapp tree (app.py imports config, services/, views/;
|
||||||
WEBAPP_DIR="$REPO_ROOT/webapp"
|
# the Blancco evidence timer runs blancco_evidence_monthly.py), minus caches.
|
||||||
if [ -d "$WEBAPP_DIR" ]; then
|
if [ -d "$WEBAPP_DIR" ]; then
|
||||||
mkdir -p "$MOUNT_POINT/webapp"
|
mkdir -p "$WORK/stage"
|
||||||
cp -r "$WEBAPP_DIR/app.py" "$WEBAPP_DIR/requirements.txt" "$MOUNT_POINT/webapp/"
|
cp -r "$WEBAPP_DIR" "$WORK/stage/webapp"
|
||||||
cp -r "$WEBAPP_DIR/templates" "$WEBAPP_DIR/static" "$MOUNT_POINT/webapp/"
|
find "$WORK/stage/webapp" -name __pycache__ -type d -prune -exec rm -rf {} +
|
||||||
|
find "$WORK/stage/webapp" -name '*.py[co]' -delete
|
||||||
|
t_cp "" "$WORK/stage/webapp"
|
||||||
|
rm -rf "$WORK/stage"
|
||||||
echo " Copied webapp/"
|
echo " Copied webapp/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy pip wheels for offline Flask install
|
# Copy pip wheels for offline Flask install
|
||||||
PIP_WHEELS_DIR="$REPO_ROOT/pip-wheels"
|
|
||||||
if [ ! -d "$PIP_WHEELS_DIR" ]; then
|
if [ ! -d "$PIP_WHEELS_DIR" ]; then
|
||||||
echo " pip-wheels/ not found — downloading now..."
|
echo " pip-wheels/ not found - downloading now..."
|
||||||
mkdir -p "$PIP_WHEELS_DIR"
|
mkdir -p "$PIP_WHEELS_DIR"
|
||||||
if pip3 download -d "$PIP_WHEELS_DIR" flask lxml 2>/dev/null; then
|
if pip3 download -d "$PIP_WHEELS_DIR" flask lxml 2>/dev/null; then
|
||||||
echo " Downloaded pip wheels successfully."
|
echo " Downloaded pip wheels successfully."
|
||||||
@@ -239,100 +517,92 @@ if [ ! -d "$PIP_WHEELS_DIR" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -d "$PIP_WHEELS_DIR" ]; then
|
if [ -d "$PIP_WHEELS_DIR" ]; then
|
||||||
cp -r "$PIP_WHEELS_DIR" "$MOUNT_POINT/pip-wheels"
|
t_cp "" "$PIP_WHEELS_DIR"
|
||||||
WHEEL_COUNT=$(find "$PIP_WHEELS_DIR" -name '*.whl' | wc -l)
|
WHEEL_COUNT=$(find "$PIP_WHEELS_DIR" -name '*.whl' | wc -l)
|
||||||
echo " Copied pip-wheels/ ($WHEEL_COUNT wheels)"
|
echo " Copied pip-wheels/ ($WHEEL_COUNT wheels)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.)
|
# Copy WinPE boot files (wimboot, boot.wim, BCD, ipxe.efi, etc.) to the root
|
||||||
BOOT_FILES_DIR="$REPO_ROOT/boot-files"
|
|
||||||
if [ -d "$BOOT_FILES_DIR" ]; then
|
if [ -d "$BOOT_FILES_DIR" ]; then
|
||||||
BOOT_FILE_COUNT=0
|
BOOT_FILE_COUNT=0
|
||||||
for bf in "$BOOT_FILES_DIR"/*; do
|
for bf in "$BOOT_FILES_DIR"/*; do
|
||||||
if [ -f "$bf" ]; then
|
if [ -f "$bf" ]; then
|
||||||
cp "$bf" "$MOUNT_POINT/"
|
t_cp_split "$bf" ""
|
||||||
BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1))
|
BOOT_FILE_COUNT=$((BOOT_FILE_COUNT + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
BOOT_FILES_SIZE=$(du -sh "$BOOT_FILES_DIR" | cut -f1)
|
BOOT_FILES_SIZE=$(du -shL "$BOOT_FILES_DIR" | cut -f1)
|
||||||
echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) — wimboot, boot.wim, ipxe.efi, etc."
|
echo " Copied $BOOT_FILE_COUNT boot files ($BOOT_FILES_SIZE) - wimboot, boot.wim, ipxe.efi, etc."
|
||||||
else
|
else
|
||||||
echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)"
|
echo " WARNING: No boot-files/ found (copy WinPE boot files from Media Creator)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
|
# Copy boot tools (Clonezilla, Blancco, Memtest) if prepared
|
||||||
BOOT_TOOLS_DIR="$REPO_ROOT/boot-tools"
|
if [ -n "$BOOT_TOOLS_DIR" ] && [ -d "$BOOT_TOOLS_DIR" ]; then
|
||||||
if [ -d "$BOOT_TOOLS_DIR" ]; then
|
t_cp "" "$BOOT_TOOLS_DIR"
|
||||||
cp -r "$BOOT_TOOLS_DIR" "$MOUNT_POINT/boot-tools"
|
TOOLS_SIZE=$(du -shL "$BOOT_TOOLS_DIR" | cut -f1)
|
||||||
TOOLS_SIZE=$(du -sh "$MOUNT_POINT/boot-tools" | cut -f1)
|
|
||||||
echo " Copied boot-tools/ ($TOOLS_SIZE)"
|
echo " Copied boot-tools/ ($TOOLS_SIZE)"
|
||||||
|
elif [ -z "$BOOT_TOOLS_DIR" ]; then
|
||||||
|
echo " Skipped boot-tools/ (--skip-boot-tools)"
|
||||||
else
|
else
|
||||||
echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
|
echo " No boot-tools/ found (run prepare-boot-tools.sh first)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy enrollment directory (PPKGs, run-enrollment.ps1) if present
|
# Copy enrollment directory (PPKGs, run-enrollment.ps1) if present.
|
||||||
# FAT32 has a 4GB max file size; files larger than that are split into chunks
|
# Files over the FAT32 limit are split; the playbook reassembles them.
|
||||||
# that the playbook reassembles with `cat`.
|
if [ -n "$ENROLLMENT_DIR" ] && [ -d "$ENROLLMENT_DIR" ]; then
|
||||||
ENROLLMENT_DIR="$REPO_ROOT/enrollment"
|
t_mkdir enrollment
|
||||||
FAT32_MAX=$((3500 * 1024 * 1024)) # 3500 MiB chunks, safely under 4GiB FAT32 limit
|
|
||||||
if [ -d "$ENROLLMENT_DIR" ]; then
|
|
||||||
mkdir -p "$MOUNT_POINT/enrollment"
|
|
||||||
SPLIT_COUNT=0
|
SPLIT_COUNT=0
|
||||||
for f in "$ENROLLMENT_DIR"/*; do
|
for f in "$ENROLLMENT_DIR"/*; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
bn="$(basename "$f")"
|
if [ -f "$f" ]; then
|
||||||
if [ -f "$f" ] && [ "$(stat -c%s "$f")" -gt "$FAT32_MAX" ]; then
|
t_cp_split "$f" enrollment
|
||||||
echo " Splitting $bn (>$((FAT32_MAX / 1024 / 1024))M) into chunks..."
|
|
||||||
split -b "$FAT32_MAX" -d -a 2 "$f" "$MOUNT_POINT/enrollment/${bn}.part."
|
|
||||||
SPLIT_COUNT=$((SPLIT_COUNT + 1))
|
|
||||||
else
|
else
|
||||||
cp -r "$f" "$MOUNT_POINT/enrollment/"
|
t_cp enrollment "$f"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
PPKG_COUNT=$(find "$ENROLLMENT_DIR" -maxdepth 1 -name '*.ppkg' 2>/dev/null | wc -l)
|
PPKG_COUNT=$(find "$ENROLLMENT_DIR" -maxdepth 1 -name '*.ppkg' 2>/dev/null | wc -l)
|
||||||
ENROLL_SIZE=$(du -sh "$MOUNT_POINT/enrollment" | cut -f1)
|
ENROLL_SIZE=$(du -shL "$ENROLLMENT_DIR" | cut -f1)
|
||||||
echo " Copied enrollment/ ($ENROLL_SIZE, $PPKG_COUNT PPKGs, $SPLIT_COUNT split)"
|
echo " Copied enrollment/ ($ENROLL_SIZE, $PPKG_COUNT PPKGs, $SPLIT_COUNT split)"
|
||||||
|
elif [ -z "$ENROLLMENT_DIR" ]; then
|
||||||
|
echo " Skipped enrollment/ (--skip-enrollment)"
|
||||||
else
|
else
|
||||||
echo " No enrollment/ directory found (PPKGs can be uploaded via webapp later)"
|
echo " No enrollment/ directory found (PPKGs can be uploaded via webapp later)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy BIOS update binaries if staged
|
# Copy BIOS update binaries if staged
|
||||||
BIOS_DIR="$REPO_ROOT/bios-staging"
|
if [ -n "$BIOS_DIR" ] && [ -d "$BIOS_DIR" ] && [ "$(ls -A "$BIOS_DIR" 2>/dev/null)" ]; then
|
||||||
if [ -d "$BIOS_DIR" ] && [ "$(ls -A "$BIOS_DIR" 2>/dev/null)" ]; then
|
|
||||||
echo " Copying BIOS update binaries from bios-staging/..."
|
echo " Copying BIOS update binaries from bios-staging/..."
|
||||||
mkdir -p "$MOUNT_POINT/bios"
|
t_mkdir bios
|
||||||
cp -r "$BIOS_DIR"/* "$MOUNT_POINT/bios/" 2>/dev/null || true
|
t_cp bios "$BIOS_DIR"/* || true
|
||||||
BIOS_COUNT=$(find "$MOUNT_POINT/bios" -name '*.exe' 2>/dev/null | wc -l)
|
BIOS_COUNT=$(find "$BIOS_DIR" -name '*.exe' 2>/dev/null | wc -l)
|
||||||
BIOS_SIZE=$(du -sh "$MOUNT_POINT/bios" | cut -f1)
|
BIOS_SIZE=$(du -shL "$BIOS_DIR" | cut -f1)
|
||||||
echo " Copied bios/ ($BIOS_SIZE, $BIOS_COUNT files)"
|
echo " Copied bios/ ($BIOS_SIZE, $BIOS_COUNT files)"
|
||||||
|
elif [ -z "$BIOS_DIR" ]; then
|
||||||
|
echo " Skipped bios-staging/ (--skip-bios)"
|
||||||
else
|
else
|
||||||
echo " No bios-staging/ found (BIOS updates can be pushed via download-drivers.py later)"
|
echo " No bios-staging/ found (BIOS updates can be pushed via download-drivers.py later)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy Dell driver packs if staged
|
# Copy Dell driver packs if staged (split over the FAT32 limit)
|
||||||
# Files larger than the FAT32 4GB limit are split into chunks; the playbook
|
if [ -n "$DRIVERS_DIR" ] && [ -d "$DRIVERS_DIR" ] && [ "$(ls -A "$DRIVERS_DIR" 2>/dev/null)" ]; then
|
||||||
# reassembles them on the server.
|
|
||||||
DRIVERS_DIR="$REPO_ROOT/drivers-staging"
|
|
||||||
if [ -d "$DRIVERS_DIR" ] && [ "$(ls -A "$DRIVERS_DIR" 2>/dev/null)" ]; then
|
|
||||||
echo " Copying Dell driver packs from drivers-staging/..."
|
echo " Copying Dell driver packs from drivers-staging/..."
|
||||||
mkdir -p "$MOUNT_POINT/drivers"
|
t_mkdir drivers
|
||||||
DRV_SPLIT=0
|
SPLIT_COUNT=0
|
||||||
# Mirror directory tree first (fast)
|
# Mirror directory tree first
|
||||||
(cd "$DRIVERS_DIR" && find . -type d -exec mkdir -p "$MOUNT_POINT/drivers/{}" \;)
|
while IFS= read -r d; do
|
||||||
# Copy files <4GB directly, split files >=4GB into chunks
|
t_mkdir "drivers/${d#./}"
|
||||||
|
done < <(cd "$DRIVERS_DIR" && find . -mindepth 1 -type d)
|
||||||
while IFS= read -r f; do
|
while IFS= read -r f; do
|
||||||
rel="${f#$DRIVERS_DIR/}"
|
rel="${f#"$DRIVERS_DIR"/}"
|
||||||
dest="$MOUNT_POINT/drivers/$rel"
|
reldir="$(dirname "$rel")"
|
||||||
if [ "$(stat -c%s "$f")" -gt "$FAT32_MAX" ]; then
|
[ "$reldir" = "." ] && reldir="" || reldir="/$reldir"
|
||||||
echo " Splitting $rel..."
|
t_cp_split "$f" "drivers$reldir"
|
||||||
split -b "$FAT32_MAX" -d -a 2 "$f" "${dest}.part."
|
|
||||||
DRV_SPLIT=$((DRV_SPLIT + 1))
|
|
||||||
else
|
|
||||||
cp "$f" "$dest"
|
|
||||||
fi
|
|
||||||
done < <(find "$DRIVERS_DIR" -type f)
|
done < <(find "$DRIVERS_DIR" -type f)
|
||||||
DRIVERS_SIZE=$(du -sh "$MOUNT_POINT/drivers" | cut -f1)
|
DRIVERS_SIZE=$(du -shL "$DRIVERS_DIR" | cut -f1)
|
||||||
echo " Copied drivers/ ($DRIVERS_SIZE, $DRV_SPLIT split)"
|
echo " Copied drivers/ ($DRIVERS_SIZE, $SPLIT_COUNT split)"
|
||||||
|
elif [ -z "$DRIVERS_DIR" ]; then
|
||||||
|
echo " Skipped drivers-staging/ (--skip-drivers)"
|
||||||
else
|
else
|
||||||
echo " No drivers-staging/ found (drivers can be downloaded later)"
|
echo " No drivers-staging/ found (drivers can be downloaded later)"
|
||||||
fi
|
fi
|
||||||
@@ -340,9 +610,9 @@ fi
|
|||||||
# Optionally copy WinPE deployment images
|
# Optionally copy WinPE deployment images
|
||||||
if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then
|
if [ -n "$WINPE_SOURCE" ] && [ -d "$WINPE_SOURCE" ]; then
|
||||||
echo " Copying WinPE deployment content from $WINPE_SOURCE..."
|
echo " Copying WinPE deployment content from $WINPE_SOURCE..."
|
||||||
mkdir -p "$MOUNT_POINT/images"
|
t_mkdir images
|
||||||
cp -r "$WINPE_SOURCE"/* "$MOUNT_POINT/images/" 2>/dev/null || true
|
t_cp images "$WINPE_SOURCE"/* || true
|
||||||
IMG_SIZE=$(du -sh "$MOUNT_POINT/images" | cut -f1)
|
IMG_SIZE=$(du -shL "$WINPE_SOURCE" | cut -f1)
|
||||||
echo " Copied WinPE images ($IMG_SIZE)"
|
echo " Copied WinPE images ($IMG_SIZE)"
|
||||||
elif [ -n "$WINPE_SOURCE" ]; then
|
elif [ -n "$WINPE_SOURCE" ]; then
|
||||||
echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)"
|
echo " WARNING: WinPE source path not found: $WINPE_SOURCE (skipping)"
|
||||||
@@ -351,23 +621,44 @@ fi
|
|||||||
# List what's on CIDATA
|
# List what's on CIDATA
|
||||||
echo ""
|
echo ""
|
||||||
echo " CIDATA contents:"
|
echo " CIDATA contents:"
|
||||||
ls -lh "$MOUNT_POINT/" | sed 's/^/ /'
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
|
# shellcheck disable=SC2012 # human-readable listing only
|
||||||
|
ls -lh "$MOUNT_POINT/" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
mdir -i "$CIDATA_IMG" ::/ | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Cleanup ---
|
# --- Finish ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "[6/6] Syncing and unmounting..."
|
if [ "$IMAGE_MODE" -eq 0 ]; then
|
||||||
sync
|
echo "[6/6] Syncing and unmounting..."
|
||||||
umount "$MOUNT_POINT"
|
sync
|
||||||
rmdir "$MOUNT_POINT"
|
umount "$MOUNT_POINT"
|
||||||
|
rmdir "$MOUNT_POINT"
|
||||||
|
MOUNT_POINT=""
|
||||||
|
else
|
||||||
|
echo "[6/6] Writing CIDATA filesystem into $IMAGE_FILE at sector $PART_START..."
|
||||||
|
dd if="$CIDATA_IMG" of="$IMAGE_FILE" bs=1M conv=notrunc,sparse \
|
||||||
|
oflag=seek_bytes seek=$((PART_START * SECTOR_SIZE)) status=none
|
||||||
|
rm -f "$CIDATA_IMG"
|
||||||
|
echo " Partition table:"
|
||||||
|
sfdisk -l "$IMAGE_FILE" 2>/dev/null | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo "USB build complete!"
|
echo "USB build complete!"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
if [ "$IMAGE_MODE" -eq 1 ]; then
|
||||||
echo " 1. Insert USB into target machine"
|
echo "Image: $IMAGE_FILE"
|
||||||
echo " 2. Boot from USB (F12 / boot menu)"
|
echo " Write to a stick with: sudo dd if=$IMAGE_FILE of=/dev/sdX bs=4M conv=fsync status=progress"
|
||||||
echo " 3. Ubuntu will auto-install and configure the PXE server"
|
echo " or attach it to a VM as a USB disk (see tests/vm/README.md)."
|
||||||
echo " 4. After reboot, move the NIC to the isolated PXE network"
|
else
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Insert USB into target machine"
|
||||||
|
echo " 2. Boot from USB (F12 / boot menu)"
|
||||||
|
echo " 3. Ubuntu will auto-install and configure the PXE server"
|
||||||
|
echo " 4. After reboot, move the NIC to the isolated PXE network"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
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