Offline install that lands fully patched: resolve against the ISO's own base

- autoinstall: dead mirror + fallback offline-install, so the installed
  base is exactly the ISO's; late-commands run offline-apt-install.sh on
  CIDATA packages/ (local apt repo, one transaction, errors fail the
  install, log /var/log/offline-apt-install.log); first boot no longer
  runs dpkg -i; real archive sources written back at the end.
- download-packages.sh --iso: unpacks the ISO's default install source
  into a docker image (pxe-iso-base:<version>) and downloads there: all
  updates of the base, the explicit list (zip included) and the newest
  pool packages with their closure; writes Packages + explicit.list and
  checks the set offline in a fresh base container (--network none).
- offline-apt-install.sh: new; also used by that check (--simulate).
- build-usb.sh: ships Packages, explicit.list and the script; refuses a
  set without them.
- tests/vm: pxetest-client-nosb (Secure Boot off, e1000e, SATA), one
  client at a time.
This commit is contained in:
cproudlock
2026-10-15 10:59:30 -04:00
parent c1a5fb3e02
commit bcd91b488a
13 changed files with 1034 additions and 671 deletions

View File

@@ -54,7 +54,8 @@ Client PXE boot (UEFI Secure Boot)
### Step 1: Download Offline Packages ### Step 1: Download Offline Packages
```bash ```bash
./scripts/download-packages.sh ./scripts/download-packages.sh --iso /path/to/ubuntu-24.04.x-live-server-amd64.iso
./scripts/check-updates.py # expect: PASSED, 0 security updates
./scripts/fetch-ipxe.sh ./scripts/fetch-ipxe.sh
``` ```
@@ -245,7 +246,7 @@ Reports are viewable and downloadable from the web interface at `http://172.16.9
## Notes ## Notes
- Run `download-packages.sh` before building USB — it downloads all offline `.deb` packages including wimtools (needed for startnet.cmd editing) - Run `download-packages.sh --iso <the server ISO>` before building the USB: it builds the offline apt repo (every update for the ISO's base plus the explicit packages, wimtools included) that the installer patches and installs from
- The webapp uses session-based CSRF tokens on all POST forms and API endpoints - The webapp uses session-based CSRF tokens on all POST forms and API endpoints
## Commit History ## Commit History

View File

@@ -46,7 +46,8 @@ Client PXE boot
### Step 1: Download Offline Packages (one-time, requires internet) ### Step 1: Download Offline Packages (one-time, requires internet)
```bash ```bash
./scripts/download-packages.sh ./scripts/download-packages.sh --iso /path/to/ubuntu-24.04.x-live-server-amd64.iso
./scripts/check-updates.py # expect: PASSED, 0 security updates
./scripts/fetch-ipxe.sh ./scripts/fetch-ipxe.sh
``` ```
@@ -55,7 +56,20 @@ tarball (its snponly build becomes `ipxe.efi`) and iPXE's dual-signed shim (Micr
which every UEFI client boots as `ipxe-shim.efi`. Keep the old Broadcom-signed `boot-files/ipxe.efi` too: the which every UEFI client boots as `ipxe-shim.efi`. Keep the old Broadcom-signed `boot-files/ipxe.efi` too: the
playbook installs it as `ipxe-bcm.efi`, the rollback boot file. playbook installs it as `ipxe-bcm.efi`, the rollback boot file.
Downloads all .deb packages (ansible, dnsmasq, apache2, samba, wimtools, etc.) into `offline-packages/` and Python wheels (flask, lxml) into `pip-wheels/`. Approximately 252 packages (~140 MB) + 8 Python wheels. `download-packages.sh` needs docker (no sudo) and the SAME server ISO that
`build-usb.sh` will use. The install is offline (autoinstall uses a dead
mirror on purpose), so the server's base is exactly that ISO's; the script
unpacks the ISO's install image into a docker image and resolves there:
every update of an installed package, the explicit package list
(ansible, dnsmasq, apache2, samba, wimtools, zip, the HWE kernel, ...) and
the newest version of what the installer adds from the ISO's pool (kernel,
grub, openssh-server). `offline-packages/` becomes an apt repository
(debs + `Packages` + `explicit.list`), checked offline in a fresh container
of the base before the script finishes. On the server, autoinstall's
late-commands run `scripts/offline-apt-install.sh` on it: one apt
transaction from the local repo only, any error fails the install (log:
`/var/log/offline-apt-install.log`). The webapp's pinned, hashed wheels go
to `pip-wheels/`. About 500 debs (~1.6 GB) + 8 wheels.
**Packages included:** **Packages included:**
- **Server:** dnsmasq, apache2, samba, ufw, cron - **Server:** dnsmasq, apache2, samba, ufw, cron

View File

@@ -27,11 +27,19 @@ autoinstall:
dhcp6: false dhcp6: false
optional: true optional: true
# The corp NIC may now have a lease, but the install stays offline-capable: # The install is always offline, even when the corp NIC has a lease and
# no geoip lookup, and if the Ubuntu mirror is unreachable (Zscaler, # internet: the mirror points at a closed local port, so subiquity's
# no route) install from the ISO instead of failing. # mirror check fails and it installs from the ISO alone (no "security
# updates" step from the archive). The base is then exactly the ISO's,
# which is what download-packages.sh resolved the offline set against;
# late-commands patch it from CIDATA. The real archive sources are
# written back at the end of late-commands.
apt: apt:
geoip: false geoip: false
preserve_sources_list: false
primary:
- arches: [default]
uri: "http://127.0.0.1:9/ubuntu"
fallback: offline-install fallback: offline-install
# Storage configuration # Storage configuration
@@ -56,23 +64,25 @@ autoinstall:
# Installer-stage late commands # Installer-stage late commands
late-commands: late-commands:
# Step 1: Install deb packages from CIDATA USB # Step 1: Upgrade and install from the offline set on CIDATA
# (packages/ is an apt repo: debs + Packages + explicit.list). Any apt
# error fails the install; the log is /var/log/offline-apt-install.log.
- | - |
curtin in-target --target=/target -- bash -c ' curtin in-target --target=/target -- bash -euc '
mkdir -p /mnt/cidata mkdir -p /mnt/cidata
CIDATA_DEV=$(blkid -L CIDATA) CIDATA_DEV=$(blkid -L CIDATA)
if [ -n "$CIDATA_DEV" ]; then mount -o ro "$CIDATA_DEV" /mnt/cidata
mount "$CIDATA_DEV" /mnt/cidata set +e
if compgen -G "/mnt/cidata/packages/*.deb" > /dev/null; then bash /mnt/cidata/offline-apt-install.sh /mnt/cidata/packages 2>&1 | tee /var/log/offline-apt-install.log
cp /mnt/cidata/packages/*.deb /tmp/ rc=${PIPESTATUS[0]}
dpkg -i /tmp/*.deb 2>/dev/null || true set -e
dpkg -i /tmp/*.deb 2>/dev/null || true umount /mnt/cidata
dpkg --configure -a 2>/dev/null || true if [ "$rc" -ne 0 ]; then
if command -v nmcli >/dev/null; then echo "offline package install FAILED (rc=$rc), see /var/log/offline-apt-install.log" >&2
systemctl enable NetworkManager exit "$rc"
fi fi
fi if command -v nmcli >/dev/null; then
umount /mnt/cidata systemctl enable NetworkManager
fi fi
' '
@@ -98,14 +108,10 @@ autoinstall:
if [ -n "$CIDATA_DEV" ]; then if [ -n "$CIDATA_DEV" ]; then
mkdir -p /mnt/usb mkdir -p /mnt/usb
mount "$CIDATA_DEV" /mnt/usb mount "$CIDATA_DEV" /mnt/usb
if compgen -G "/mnt/usb/packages/*.deb" > /dev/null; then
dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
dpkg --configure -a 2>/dev/null || true
fi
if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then
cd /mnt/usb/playbook cd /mnt/usb/playbook
ansible-playbook -i localhost, -c local pxe_server_setup.yml ansible-playbook -i localhost, -c local pxe_server_setup.yml
cd /
fi fi
umount /mnt/usb umount /mnt/usb
fi fi
@@ -125,6 +131,25 @@ autoinstall:
chmod +x /etc/rc.local chmod +x /etc/rc.local
' '
# Step 5: Real archive sources for later online maintenance (the
# install used a dead mirror on purpose, see apt: above).
- |
rm -f /target/etc/apt/sources.list.d/*.list /target/etc/apt/sources.list.d/*.curtin.orig
echo "# Ubuntu sources have moved to /etc/apt/sources.list.d/ubuntu.sources" > /target/etc/apt/sources.list
cat > /target/etc/apt/sources.list.d/ubuntu.sources <<"EOF"
Types: deb
URIs: http://archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
user-data: user-data:
disable_root: false disable_root: false

View File

@@ -170,6 +170,17 @@ else
fi fi
fi fi
# The installer patches and installs from packages/ as an apt repo
# (autoinstall late-commands -> offline-apt-install.sh): it needs the index
# and the explicit list that download-packages.sh writes with the debs.
for f in "$OFFLINE_PKG_DIR/Packages" "$OFFLINE_PKG_DIR/explicit.list" "$REPO_ROOT/scripts/offline-apt-install.sh"; do
if [ ! -f "$f" ]; then
echo "ERROR: $f not found. Build the offline set with"
echo " scripts/download-packages.sh --iso $ISO_PATH $OFFLINE_PKG_DIR"
exit 1
fi
done
if [ -n "$SITE_VARS" ] && [ ! -f "$SITE_VARS" ]; then if [ -n "$SITE_VARS" ] && [ ! -f "$SITE_VARS" ]; then
echo "ERROR: --site-vars file not found: $SITE_VARS" echo "ERROR: --site-vars file not found: $SITE_VARS"
exit 1 exit 1
@@ -487,16 +498,17 @@ echo "[5/6] Copying autoinstall config, packages, and playbook to CIDATA..."
# Copy cloud-init files # Copy cloud-init files
t_cp "" "$AUTOINSTALL_DIR/user-data" "$AUTOINSTALL_DIR/meta-data" t_cp "" "$AUTOINSTALL_DIR/user-data" "$AUTOINSTALL_DIR/meta-data"
# Copy offline .deb packages into packages/ subdirectory # Copy the offline package set (an apt repo: debs + Packages + explicit.list)
# into packages/, and the script the installer runs on it to the root.
t_mkdir packages t_mkdir packages
t_cp packages "$OFFLINE_PKG_DIR/Packages" "$OFFLINE_PKG_DIR/explicit.list"
DEB_COUNT=0 DEB_COUNT=0
if [ -d "$OFFLINE_PKG_DIR" ] && compgen -G "$OFFLINE_PKG_DIR/*.deb" >/dev/null; then if compgen -G "$OFFLINE_PKG_DIR/*.deb" >/dev/null; then
t_cp packages "$OFFLINE_PKG_DIR"/*.deb t_cp packages "$OFFLINE_PKG_DIR"/*.deb
DEB_COUNT=$(find "$OFFLINE_PKG_DIR" -maxdepth 1 -name '*.deb' | wc -l) DEB_COUNT=$(find "$OFFLINE_PKG_DIR" -maxdepth 1 -name '*.deb' | wc -l)
else
echo " WARNING: no .deb packages in $OFFLINE_PKG_DIR (run download-packages.sh)"
fi fi
echo " Copied $DEB_COUNT .deb packages to packages/" t_cp "" "$REPO_ROOT/scripts/offline-apt-install.sh"
echo " Copied $DEB_COUNT .deb packages + Packages + explicit.list to packages/"
# Copy playbook directory # Copy playbook directory
t_cp "" "$PLAYBOOK_DIR" t_cp "" "$PLAYBOOK_DIR"

View File

@@ -1,63 +1,48 @@
#!/bin/bash #!/bin/bash
# #
# download-packages.sh - Download all .deb packages needed for offline PXE server setup # download-packages.sh - Build the offline package set for the PXE server
# #
# The PXE server installs Ubuntu 24.04 (Noble), so all packages MUST come from the # The server installs from the Ubuntu 24.04 server ISO with no network
# 24.04 archive. If this script is run on a non-24.04 host (e.g. Zorin 17 / 22.04), # (autoinstall/user-data), so the installed base is exactly the ISO's
# it auto-spawns an Ubuntu 24.04 docker container to do the download. # "ubuntu-server" image plus what the installer adds from the ISO's pool/.
# This script resolves against THAT base, not against whatever the build
# host has:
#
# 1. The ISO's default install source (squashfs layers) is unpacked into
# a docker image, pxe-iso-base:<iso version> (cached).
# 2. In a container of that image, with the Ubuntu archive (noble,
# noble-updates, noble-security) as the source, apt downloads:
# - every update of an installed package (dist-upgrade closure)
# - the explicit package list below, with its dependencies
# - the newest version of every package in the ISO's pool/ (what the
# installer may add: kernel, bootloader, openssh-server...) and
# the dependencies those versions need
# and writes Packages (an apt index) and explicit.list next to the
# debs, so the set is an apt repository.
# 3. A second container of the base image, with NO network, resolves the
# install exactly as the server will (offline-apt-install.sh
# --simulate). An unsatisfiable set fails the build here.
# 4. The webapp's pinned, hashed wheels go to pip-wheels/.
#
# On the server, autoinstall's late-commands run
# offline-apt-install.sh /mnt/cidata/packages
# which upgrades every installed package that the set has a newer version
# of and installs explicit.list, all from the local repo only.
#
# Needs docker (no sudo; the user must be in the docker group), xorriso and
# internet access to the Ubuntu archive and PyPI.
# #
# Usage: # Usage:
# ./download-packages.sh [output_directory] # ./download-packages.sh --iso ubuntu-24.04.x-live-server-amd64.iso [output_directory]
# ISO=... ./download-packages.sh [output_directory]
# #
# Default output: ./offline-packages/ # Default output: ./offline-packages/ (its old .debs are removed first).
# Wheels go to PIP_DIR (default: <repo>/pip-wheels).
set -euo pipefail set -euo pipefail
OUT_DIR="${1:-./offline-packages}" SCRIPT_PATH="$(readlink -f "$0")"
OUT_DIR_ABS="$(cd "$(dirname "$OUT_DIR")" 2>/dev/null && pwd)/$(basename "$OUT_DIR")" REPO_DIR="$(cd "$(dirname "$SCRIPT_PATH")"/.. && pwd)"
# Detect host Ubuntu codename. Run inside the container if not Noble (24.04).
HOST_CODENAME="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}")"
# USE_DOCKER=1 forces the container even on a noble host: no sudo needed,
# and the host's own apt state is left alone.
if [ "${IN_DOCKER:-}" != "1" ] && { [ "$HOST_CODENAME" != "noble" ] || [ "${USE_DOCKER:-}" = "1" ]; }; then
echo "Host is '$HOST_CODENAME'; downloading inside an ubuntu:24.04 container."
echo "Re-running inside ubuntu:24.04 docker container..."
echo ""
if ! command -v docker >/dev/null; then
echo "ERROR: docker not installed. Install docker or run on a real Ubuntu 24.04 host."
exit 1
fi
SCRIPT_PATH="$(readlink -f "$0")"
REPO_DIR="$(cd "$(dirname "$SCRIPT_PATH")"/.. && pwd)"
mkdir -p "$OUT_DIR_ABS"
# Wheels go to their own mount so PIP_DIR is honoured inside the container
# (the in-container default would be the repo's pip-wheels/).
PIP_DIR_HOST="$(mkdir -p "${PIP_DIR:-$REPO_DIR/pip-wheels}" && cd "${PIP_DIR:-$REPO_DIR/pip-wheels}" && pwd)"
docker run --rm -i \
-v "$REPO_DIR:/repo" \
-v "$OUT_DIR_ABS:/out" \
-v "$PIP_DIR_HOST:/pipout" \
-e IN_DOCKER=1 \
-e PIP_DIR=/pipout \
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
-e BLANCCO_KVER="${BLANCCO_KVER:-}" \
-w /repo \
ubuntu:24.04 \
bash -c "apt-get update -qq && apt-get install -y --no-install-recommends sudo python3-pip python3-setuptools python3-wheel ca-certificates >/dev/null && /repo/scripts/download-packages.sh /out; rc=\$?; chown -R \$HOST_UID:\$HOST_GID /out /pipout; exit \$rc"
echo ""
echo "============================================"
echo "Container build complete. Files in: $OUT_DIR_ABS"
echo "============================================"
exit 0
fi
mkdir -p "$OUT_DIR"
# Kernel the Blancco UEFI path boots (playbook var blancco_kver; keep the two # Kernel the Blancco UEFI path boots (playbook var blancco_kver; keep the two
# in step). The playbook builds vmlinuz-ubuntu-70, kexec-initrd-70.img and # in step). The playbook builds vmlinuz-ubuntu-70, kexec-initrd-70.img and
@@ -108,75 +93,241 @@ AUTOINSTALL_PACKAGES=(
ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}") ALL_PACKAGES=("${PLAYBOOK_PACKAGES[@]}" "${AUTOINSTALL_PACKAGES[@]}")
# ---------------------------------------------------------------------------
# Inside the base container (internal; started by the host part below)
# ---------------------------------------------------------------------------
if [ "${IN_BASE:-}" = "1" ]; then
export DEBIAN_FRONTEND=noninteractive
OUT=/out
ARCH=/tmp/arch
mkdir -p "$ARCH/partial"
# The ISO's sources point at the archive (and maybe the cdrom); use the
# archive pockets only.
rm -f /etc/apt/sources.list /etc/apt/sources.list.d/*
cat >/etc/apt/sources.list.d/ubuntu.sources <<'SRC'
Types: deb
URIs: http://archive.ubuntu.com/ubuntu/
Suites: noble noble-updates
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
SRC
echo "[base] apt-get update"
apt-get update -qq
GET=(apt-get -o "Dir::Cache::archives=$ARCH" -o APT::Sandbox::User=root)
# 1+2: dist-upgrade closure plus the explicit list, as one transaction
# (the same one offline-apt-install.sh runs on the server).
DU=$(apt-get -s dist-upgrade)
if grep -q 'kept back' <<<"$DU"; then
echo "ERROR: dist-upgrade keeps packages back on the ISO base:" >&2
echo "$DU" >&2
exit 1
fi
mapfile -t UPGRADES < <(awk '/^Inst /{print $2}' <<<"$DU")
echo "[base] ${#UPGRADES[@]} installed packages have updates"
if ! SIM=$(apt-get -s install "${ALL_PACKAGES[@]}" "${UPGRADES[@]}" 2>&1); then
echo "ERROR: apt could not resolve the package list on the ISO base:" >&2
grep -E "^(E|W):| : Depends" <<<"$SIM" >&2 || echo "$SIM" >&2
exit 1
fi
echo "[base] downloading the install + upgrade transaction"
"${GET[@]}" -y -q --download-only install "${ALL_PACKAGES[@]}" "${UPGRADES[@]}" >/tmp/dl.log 2>&1 \
|| { echo "ERROR: download failed:" >&2; tail -30 /tmp/dl.log >&2; exit 1; }
# 3: newest version of every pool package the installer may add, with
# the dependencies that version needs on the base. Versioned kernel ABI
# packages (linux-*-6.8.0-71*) are reached through their metapackages.
POOL_EXTRA=()
SKIPPED=()
while read -r name; do
[ -n "$name" ] || continue
case "$name" in
linux-*-[0-9]*.[0-9]*.[0-9]*-[0-9]*) continue ;;
# Driver packages for ubuntu-drivers (NVIDIA): never on this box.
*nvidia*) continue ;;
esac
if PS=$(apt-get -s install "$name" 2>&1); then
mapfile -t -O "${#POOL_EXTRA[@]}" POOL_EXTRA < <(awk '/^Inst /{print $2}' <<<"$PS")
else
SKIPPED+=("$name")
fi
done < /meta/pool.txt
mapfile -t POOL_EXTRA < <(printf '%s\n' "${POOL_EXTRA[@]}" | sort -u)
echo "[base] pool packages: $(wc -l </meta/pool.txt) names, closure ${#POOL_EXTRA[@]} packages"
[ ${#SKIPPED[@]} -eq 0 ] || echo "[base] WARNING: pool packages not installable on the base, skipped: ${SKIPPED[*]}"
(cd "$ARCH" && apt-get -q download "${POOL_EXTRA[@]}" >/tmp/pool-dl.log 2>&1) \
|| { echo "ERROR: pool download failed:" >&2; tail -30 /tmp/pool-dl.log >&2; exit 1; }
# Several versions of one package (download + pool): keep the newest.
find "$OUT" -maxdepth 1 \( -name '*.deb' -o -name 'Packages*' -o -name explicit.list \) -delete
cp "$ARCH"/*.deb "$OUT"/
python3 - "$OUT" <<'PY'
import glob, os, subprocess, sys
best = {}
for p in glob.glob(os.path.join(sys.argv[1], "*.deb")):
n, v, a = os.path.basename(p)[:-4].split("_")
v = v.replace("%3a", ":")
k = (n, a)
if k in best:
old_v, old_p = best[k]
if subprocess.run(["dpkg", "--compare-versions", v, "gt", old_v]).returncode == 0:
os.remove(old_p); best[k] = (v, p)
else:
os.remove(p)
else:
best[k] = (v, p)
PY
printf '%s\n' "${ALL_PACKAGES[@]}" >"$OUT/explicit.list"
# Uncompressed index only: apt's file: method takes it as is.
(cd "$OUT" && apt-ftparchive packages . >Packages 2>/dev/null)
echo "[base] $(find "$OUT" -maxdepth 1 -name '*.deb' | wc -l) debs, $(du -sh "$OUT" | cut -f1)"
chown -R "${HOST_UID:-0}:${HOST_GID:-0}" "$OUT"
exit 0
fi
# ---------------------------------------------------------------------------
# Host part
# ---------------------------------------------------------------------------
ISO="${ISO:-}"
POSITIONAL=()
while [ $# -gt 0 ]; do
case "$1" in
--iso) ISO="$2"; shift 2 ;;
-h|--help) sed -n '2,45p' "$SCRIPT_PATH"; exit 0 ;;
*) POSITIONAL+=("$1"); shift ;;
esac
done
OUT_DIR="${POSITIONAL[0]:-./offline-packages}"
mkdir -p "$OUT_DIR"
OUT_DIR_ABS="$(cd "$OUT_DIR" && pwd)"
PIP_DIR_HOST="${PIP_DIR:-$REPO_DIR/pip-wheels}"
mkdir -p "$PIP_DIR_HOST"
PIP_DIR_HOST="$(cd "$PIP_DIR_HOST" && pwd)"
[ -n "$ISO" ] || { echo "ERROR: give the server ISO: --iso ubuntu-24.04.x-live-server-amd64.iso (the same one build-usb.sh uses)" >&2; exit 1; }
[ -f "$ISO" ] || { echo "ERROR: ISO not found: $ISO" >&2; exit 1; }
for t in docker xorriso; do
command -v "$t" >/dev/null || { echo "ERROR: $t not installed" >&2; exit 1; }
done
META=$(mktemp -d "${TMPDIR:-/tmp}/dlpkg.XXXXXX")
trap 'rm -rf "$META"' EXIT
echo "============================================" echo "============================================"
echo "Offline Package Downloader (Ubuntu 24.04 noble)" echo "Offline package set for the PXE server"
echo "============================================" echo "============================================"
echo "Output directory: $OUT_DIR" echo "ISO : $ISO"
echo "" echo "Output : $OUT_DIR_ABS"
echo "Packages to resolve:" echo "Wheels : $PIP_DIR_HOST"
printf ' - %s\n' "${ALL_PACKAGES[@]}"
echo "" echo ""
# Update package cache # --- 1. base image from the ISO's default install source ---
echo "[1/4] Updating package cache..." VOLID=$(xorriso -indev "$ISO" -pvd_info 2>/dev/null | sed -n 's/^Volume Id *: *//p' | head -1)
sudo apt-get update -qq TAG="pxe-iso-base:$(tr 'A-Z ' 'a-z-' <<<"$VOLID" | tr -cd 'a-z0-9.-')"
xorriso -osirrox on -indev "$ISO" -extract /casper/install-sources.yaml "$META/install-sources.yaml" >/dev/null 2>&1
# Default source: the entry with "default: true"; a layered path a.b.squashfs
# is layer a.squashfs under a.b.squashfs.
SRC_PATH=$(awk '/^- /{d=0} /default: true/{d=1} /^ path:/{p=$2} /^ type:/{ if (d) {print p; exit} }' "$META/install-sources.yaml")
[ -n "$SRC_PATH" ] || { echo "ERROR: no default source in casper/install-sources.yaml" >&2; exit 1; }
LAYERS=()
stem="${SRC_PATH%.squashfs}"
acc=""
IFS=. read -r -a parts <<<"$stem"
for p in "${parts[@]}"; do
acc="${acc:+$acc.}$p"
LAYERS+=("$acc.squashfs")
done
echo "[1/4] Base: $VOLID, source $SRC_PATH (layers: ${LAYERS[*]}) -> $TAG"
# Simulate install to find all dependencies xorriso -indev "$ISO" -find /pool -type f -name '*.deb' 2>/dev/null \
echo "[2/4] Resolving dependencies..." | sed "s|.*/||; s|'\$||; s|_.*||" | sort -u >"$META/pool.txt"
SIM=$(apt-get install --simulate "${ALL_PACKAGES[@]}" 2>&1) || {
# e.g. a package name that does not exist in noble: say which.
echo "ERROR: apt could not resolve the package list:" >&2
echo "$SIM" | grep -E "^(E|W):" >&2
exit 1
}
EXPLICIT_DEPS=$(echo "$SIM" | grep "^Inst " | awk '{print $2}')
# ALSO pull every package that would upgrade in a dist-upgrade. This is if docker image inspect "$TAG" >/dev/null 2>&1; then
# critical: the Ubuntu ISO ships a point-in-time baseline, but our explicit echo " image $TAG exists (docker rmi $TAG to rebuild)"
# packages (from noble-updates) may depend on *newer* versions of ISO-baseline else
# packages (e.g. gnupg 17.4 needs matching gpgv 17.4). Without this, offline for l in "${LAYERS[@]}"; do
# install fails with dpkg "dependency problems" because transitive version xorriso -osirrox on -indev "$ISO" -extract "/casper/$l" "$META/$l" >/dev/null 2>&1
# bumps aren't captured by --simulate on the explicit list. done
UPGRADE_DEPS=$(apt-get dist-upgrade --simulate 2>&1 \ # Unpack the layers as root in a throwaway container (keeps ownership),
| grep "^Inst " \ # apply overlayfs whiteouts, stream the tree into docker import.
| awk '{print $2}') docker run --rm -i -v "$META:/meta:ro" ubuntu:24.04 bash -euc '
apt-get update -qq >/dev/null && apt-get install -y -qq squashfs-tools >/dev/null
mkdir /r
first=1
for l in '"${LAYERS[*]}"'; do
if [ $first = 1 ]; then
unsquashfs -q -no-xattrs -f -d /r "/meta/$l" >/dev/null; first=0
else
# Whiteouts (char devices) = files deleted in this layer.
# The container may not mknod, so list them, delete those
# paths from the tree and extract the layer without them.
unsquashfs -lls "/meta/$l" | awk '"'"'$1 ~ /^c/ {sub(/^.*squashfs-root/, ""); print}'"'"' >/wo
while read -r w; do rm -rf "/r$w"; done </wo
rm -rf /l
if [ -s /wo ]; then
unsquashfs -q -no-xattrs -d /l -excludes "/meta/$l" $(cat /wo) >/dev/null
else
unsquashfs -q -no-xattrs -d /l "/meta/$l" >/dev/null
fi
cp -a /l/. /r/
fi
done
tar -C /r -c .' >"$META/base.tar"
# Import only a complete tree: a failed unpack must not leave an image.
docker import "$META/base.tar" "$TAG" >/dev/null
rm -f "$META/base.tar"
docker run --rm "$TAG" test -x /usr/bin/apt-get \
|| { docker rmi "$TAG" >/dev/null; echo "ERROR: base image $TAG is incomplete" >&2; exit 1; }
echo " built $TAG"
fi
for l in "${LAYERS[@]}"; do rm -f "$META/$l"; done
DEPS=$(printf '%s\n%s\n' "$EXPLICIT_DEPS" "$UPGRADE_DEPS" | sort -u | grep -v '^$') # --- 2. resolve + download against the base ---
echo "[2/4] Resolving and downloading on the ISO base (this takes a while)..."
docker run --rm \
-v "$REPO_DIR:/repo:ro" \
-v "$OUT_DIR_ABS:/out" \
-v "$META:/meta:ro" \
-e IN_BASE=1 -e BLANCCO_KVER="$BLANCCO_KVER" \
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
"$TAG" bash /repo/scripts/download-packages.sh
DEP_COUNT=$(echo "$DEPS" | wc -l) # --- 3. offline check: resolve exactly as the server will, no network ---
echo " Found $DEP_COUNT packages (explicit + baseline upgrades)" echo "[3/4] Offline check on a fresh base container (no network)..."
docker run --rm --network none \
-v "$REPO_DIR:/repo:ro" -v "$OUT_DIR_ABS:/out:ro" \
"$TAG" bash /repo/scripts/offline-apt-install.sh --simulate /out
# Download all packages # --- 4. pinned webapp wheels ---
echo "[3/4] Downloading .deb packages to $OUT_DIR..." echo "[4/4] Downloading Python wheels for the webapp..."
cd "$OUT_DIR"
apt-get download $DEPS 2>&1 | tail -5
DEB_COUNT=$(ls -1 *.deb 2>/dev/null | wc -l)
TOTAL_SIZE=$(du -sh . | cut -f1)
echo " $DEB_COUNT packages ($TOTAL_SIZE)"
# Download pip wheels for Flask webapp (offline install)
echo "[4/4] Downloading Python wheels for webapp..."
# Place pip-wheels next to the script (or /repo when in docker), not next to OUT_DIR
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
PIP_DIR="${PIP_DIR:-$REPO_ROOT/pip-wheels}"
# Exactly the pinned, hashed set in webapp/requirements.txt, for the server's # Exactly the pinned, hashed set in webapp/requirements.txt, for the server's
# Python 3.12 on x86_64. The folder is emptied first: the playbook installs # Python 3.12 on x86_64. The folder is emptied first: the playbook installs
# every wheel in it and refuses any wheel that is not pinned. # every wheel in it and refuses any wheel that is not pinned.
find "$PIP_DIR" -mindepth 1 -delete 2>/dev/null; mkdir -p "$PIP_DIR" find "$PIP_DIR_HOST" -mindepth 1 -delete
pip3 download -d "$PIP_DIR" --require-hashes -r "$REPO_ROOT/webapp/requirements.txt" \ docker run --rm -v "$REPO_DIR:/repo:ro" -v "$PIP_DIR_HOST:/pipout" \
--only-binary=:all: --no-deps --python-version 3.12 --implementation cp --abi cp312 \ -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" ubuntu:24.04 bash -euc '
--platform manylinux_2_28_x86_64 --platform manylinux_2_17_x86_64 --platform manylinux2014_x86_64 \ apt-get update -qq >/dev/null
2>&1 | tail -5 apt-get install -y -qq --no-install-recommends python3-pip ca-certificates >/dev/null
pip3 download -q -d /pipout --require-hashes -r /repo/webapp/requirements.txt \
WHL_COUNT=$(ls -1 "$PIP_DIR"/*.whl "$PIP_DIR"/*.tar.gz 2>/dev/null | wc -l) --only-binary=:all: --no-deps --python-version 3.12 --implementation cp --abi cp312 \
echo " $WHL_COUNT Python packages downloaded to pip-wheels/" --platform manylinux_2_28_x86_64 --platform manylinux_2_17_x86_64 --platform manylinux2014_x86_64
chown -R "$HOST_UID:$HOST_GID" /pipout'
DEB_COUNT=$(find "$OUT_DIR_ABS" -maxdepth 1 -name '*.deb' | wc -l)
WHL_COUNT=$(find "$PIP_DIR_HOST" -maxdepth 1 \( -name '*.whl' -o -name '*.tar.gz' \) | wc -l)
echo "" echo ""
echo "============================================" echo "============================================"
echo "Download complete!" echo "Download complete!"
echo "============================================" echo "============================================"
echo " .deb packages: $DEB_COUNT ($TOTAL_SIZE) in $OUT_DIR/" echo " .deb packages: $DEB_COUNT ($(du -sh "$OUT_DIR_ABS" | cut -f1)) in $OUT_DIR_ABS/ (apt repo: Packages, explicit.list)"
echo " Python wheels: $WHL_COUNT in $PIP_DIR/" echo " Python wheels: $WHL_COUNT in $PIP_DIR_HOST/"
echo " Check them: scripts/check-updates.py --debs $OUT_DIR_ABS"
echo "" echo ""

85
scripts/offline-apt-install.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/bin/bash
#
# offline-apt-install.sh - Install and patch from the offline package set
#
# Uses DIR (offline-packages/ as built by download-packages.sh: the .debs,
# a Packages index and explicit.list) as the ONLY apt source:
# 1. every installed package that DIR has a newer version of is upgraded
# 2. every package in DIR/explicit.list is installed
# in one apt transaction, then checks that nothing is left to upgrade.
# The system's own sources.list and lists are not read or changed.
#
# Any apt failure stops the script with a non-zero exit and apt's message.
#
# Usage:
# offline-apt-install.sh DIR install (as root)
# offline-apt-install.sh --simulate DIR resolve only (apt-get -s)
#
# Runs in the autoinstall late-commands (curtin in-target) and in
# download-packages.sh's offline check of the bundle.
set -euo pipefail
SIMULATE=0
if [ "${1:-}" = "--simulate" ]; then SIMULATE=1; shift; fi
DIR="${1:?usage: $0 [--simulate] DIR}"
DIR="$(cd "$DIR" && pwd)"
for f in Packages explicit.list; do
[ -f "$DIR/$f" ] || { echo "offline-apt: ERROR: $DIR/$f missing (rebuild with download-packages.sh)" >&2; exit 1; }
done
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/lists/partial" "$WORK/parts"
echo "deb [trusted=yes] file:$DIR ./" >"$WORK/sources.list"
APT=(apt-get
-o "Dir::Etc::SourceList=$WORK/sources.list"
-o "Dir::Etc::SourceParts=$WORK/parts"
-o "Dir::State::Lists=$WORK/lists"
-o "APT::Sandbox::User=root"
-o "Dpkg::Options::=--force-confdef"
-o "Dpkg::Options::=--force-confold")
export DEBIAN_FRONTEND=noninteractive
echo "offline-apt: source $DIR ($(find "$DIR" -maxdepth 1 -name '*.deb' | wc -l) debs)"
"${APT[@]}" update
mapfile -t EXPLICIT < <(grep -v '^[[:space:]]*\(#\|$\)' "$DIR/explicit.list")
DU=$("${APT[@]}" -s dist-upgrade)
if grep -q 'kept back' <<<"$DU"; then
echo "offline-apt: ERROR: upgrades kept back (missing dependencies in $DIR):" >&2
echo "$DU" >&2
exit 1
fi
mapfile -t UPGRADES < <(awk '/^Inst /{print $2}' <<<"$DU")
echo "offline-apt: ${#UPGRADES[@]} upgrades, ${#EXPLICIT[@]} explicit packages"
if [ "$SIMULATE" -eq 1 ]; then
OUT=$("${APT[@]}" -s install "${EXPLICIT[@]}" "${UPGRADES[@]}")
echo "offline-apt: simulated: $(grep -c '^Inst ' <<<"$OUT") packages to unpack, $(grep -c '^Conf ' <<<"$OUT") to configure"
exit 0
fi
"${APT[@]}" -y install "${EXPLICIT[@]}" "${UPGRADES[@]}"
AFTER=$("${APT[@]}" -s dist-upgrade)
LEFT=$(awk '/^Inst /{print $2}' <<<"$AFTER")
if grep -q 'kept back' <<<"$AFTER"; then
echo "offline-apt: ERROR: upgrades kept back (missing dependencies in $DIR):" >&2
echo "$AFTER" >&2
exit 1
fi
if [ -n "$LEFT" ]; then
echo "offline-apt: ERROR: still upgradable from $DIR after install:" >&2
echo "$LEFT" >&2
exit 1
fi
BROKEN=$(dpkg --audit || true)
if [ -n "$BROKEN" ]; then
echo "offline-apt: ERROR: dpkg --audit reports:" >&2
echo "$BROKEN" >&2
exit 1
fi
echo "offline-apt: done, nothing left to upgrade from the offline set"

View File

@@ -10,7 +10,8 @@ a laptop, without root and without touching a real network.
| `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. | | `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. | | `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-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). | | `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). Proves shim -> iPXE -> menu -> WinPE under Secure Boot; WinPE has no virtio driver, so it stops at "Waiting for network". |
| `pxetest-client-nosb` | 2 vCPU, 2 GiB, OVMF without Secure Boot, e1000e NIC, 64G thin SATA disk. WinPE has in-box drivers for both, so startnet gets a network and maps the shares. |
NIC MACs (fixed): NIC MACs (fixed):
@@ -19,6 +20,7 @@ NIC MACs (fixed):
| server | GE | `52:54:00:9e:10:01` | `default` NAT | `corp_nic_mac` | | server | GE | `52:54:00:9e:10:01` | `default` NAT | `corp_nic_mac` |
| server | imaging | `52:54:00:9e:10:02` | `pxetest-imaging` | `pxe_nic_mac` | | server | imaging | `52:54:00:9e:10:02` | `pxetest-imaging` | `pxe_nic_mac` |
| client | - | `52:54:00:9e:10:03` | `pxetest-imaging` | - | | client | - | `52:54:00:9e:10:03` | `pxetest-imaging` | - |
| client-nosb | - | `52:54:00:9e:10:04` | `pxetest-imaging` | - |
`build-image.sh` writes `$PXETEST_CACHE/site-vars.yml` with these two `build-image.sh` writes `$PXETEST_CACHE/site-vars.yml` with these two
MACs (`pxe_nic_mac`, `corp_nic_mac`) and passes it to `build-usb.sh MACs (`pxe_nic_mac`, `corp_nic_mac`) and passes it to `build-usb.sh
@@ -40,14 +42,19 @@ IPv4 address or a non-tap bridge port.
export PXETEST_CACHE=~/.cache/pxetest # ISO + image (about 10 GB) export PXETEST_CACHE=~/.cache/pxetest # ISO + image (about 10 GB)
export PXETEST_PAYLOAD_ROOT=/home/camp/projects/pxe # checkout holding offline-packages/, boot-files/, ... export PXETEST_PAYLOAD_ROOT=/home/camp/projects/pxe # checkout holding offline-packages/, boot-files/, ...
export UBUNTU_ISO_NAME=ubuntu-24.04.3-live-server-amd64.iso # optional; default 24.04.5
tests/vm/fetch-iso.sh # Ubuntu 24.04.5 live-server ISO, SHA256SUMS-verified tests/vm/fetch-iso.sh # the live-server ISO, SHA256SUMS-verified
scripts/download-packages.sh --iso "$PXETEST_CACHE/$UBUNTU_ISO_NAME" "$PXETEST_PAYLOAD_ROOT/offline-packages"
scripts/check-updates.py --debs "$PXETEST_PAYLOAD_ROOT/offline-packages"
tests/vm/build-image.sh # build-usb.sh --image, upload as volume pxetest-usb.img 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/create.sh # network, volumes, both VMs (prints the MAC pins)
tests/vm/start.sh server # autoinstall, reboot, first-boot playbook tests/vm/start.sh server # autoinstall, reboot, first-boot playbook
tests/vm/status.sh --shot /tmp/pxetest-shots # states, leases, screenshots tests/vm/status.sh --shot /tmp/pxetest-shots # states, leases, screenshots
tests/vm/start.sh client # once the server playbook is done tests/vm/start.sh client # once the server playbook is done (Secure Boot client)
tests/vm/stop.sh # ACPI shutdown both, keep them defined tests/vm/stop.sh client
tests/vm/start.sh client2 # Secure Boot off, e1000e: WinPE maps the shares
tests/vm/stop.sh # ACPI shutdown all, keep them defined
tests/vm/destroy.sh # remove every pxetest-* object tests/vm/destroy.sh # remove every pxetest-* object
`build-image.sh` skips enrollment/, bios-staging/, drivers-staging/ and `build-image.sh` skips enrollment/, bios-staging/, drivers-staging/ and
@@ -62,5 +69,10 @@ 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 network, `status.sh` shows its address and `ssh pxe@<address>` works from
the host. the host.
Memory: server 4 GiB + client 2 GiB. Keep other VMs (win11) off while The offline package set must be built against the SAME ISO the image is
built from (download-packages.sh resolves against that ISO's base). For a
fresh server disk: `tests/vm/destroy.sh`, then build-image.sh, create.sh,
start.sh server.
Memory: server 4 GiB + one client 2 GiB (start.sh refuses a second client). Keep other VMs (win11) off while
both run, and stop them when done. both run, and stop them when done.

View File

@@ -47,6 +47,11 @@ CLIENT_MEM_MIB="${CLIENT_MEM_MIB:-2048}"
SERVER_GE_MAC="52:54:00:9e:10:01" # on 'default' NAT (GE/corp side) 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) 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 CLIENT_MAC="52:54:00:9e:10:03" # on pxetest-imaging only
# Second client: Secure Boot OFF, e1000e NIC and SATA disk (in-box WinPE
# drivers), so WinPE gets a network and startnet can map the shares.
CLIENT2_VM="pxetest-client-nosb"
CLIENT2_DISK_VOL="pxetest-client-nosb.qcow2"
CLIENT2_MAC="52:54:00:9e:10:04" # on pxetest-imaging only
log() { printf '[pxetest] %s\n' "$*"; } log() { printf '[pxetest] %s\n' "$*"; }
die() { printf '[pxetest] ERROR: %s\n' "$*" >&2; exit 1; } die() { printf '[pxetest] ERROR: %s\n' "$*" >&2; exit 1; }

View File

@@ -43,6 +43,10 @@ if ! vol_exists "$SERVER_DISK_VOL"; then
virsh vol-create-as "$POOL" "$SERVER_DISK_VOL" "$SERVER_DISK_SIZE" --format qcow2 >/dev/null 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)" log "created $SERVER_DISK_VOL ($SERVER_DISK_SIZE thin qcow2)"
fi fi
if ! vol_exists "$CLIENT2_DISK_VOL"; then
virsh vol-create-as "$POOL" "$CLIENT2_DISK_VOL" 64G --format qcow2 >/dev/null
log "created $CLIENT2_DISK_VOL (64G thin, blank)"
fi
if ! vol_exists "$CLIENT_DISK_VOL"; then if ! vol_exists "$CLIENT_DISK_VOL"; then
virsh vol-create-as "$POOL" "$CLIENT_DISK_VOL" 1G --format qcow2 >/dev/null virsh vol-create-as "$POOL" "$CLIENT_DISK_VOL" 1G --format qcow2 >/dev/null
log "created $CLIENT_DISK_VOL (1G blank)" log "created $CLIENT_DISK_VOL (1G blank)"
@@ -157,8 +161,49 @@ XML
log "defined $CLIENT_VM" log "defined $CLIENT_VM"
fi fi
# --- second client: Secure Boot off, e1000e + SATA (WinPE in-box drivers) ---
if ! dom_exists "$CLIENT2_VM"; then
cat >"$TMP/client2.xml" <<XML
<domain type='kvm'>
<name>$CLIENT2_VM</name>
<memory unit='MiB'>$CLIENT_MEM_MIB</memory>
<vcpu>2</vcpu>
<os firmware='efi'>
<type arch='x86_64' machine='q35'>hvm</type>
<firmware>
<feature enabled='no' name='secure-boot'/>
</firmware>
</os>
<features><acpi/><apic/></features>
<cpu mode='host-passthrough'/>
<clock offset='localtime'/>
<devices>
<interface type='network'>
<source network='$IMAGING_NET'/>
<mac address='$CLIENT2_MAC'/>
<model type='e1000e'/>
<boot order='1'/>
</interface>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='$POOL_DIR/$CLIENT2_DISK_VOL'/>
<target dev='sda' bus='sata'/>
<boot order='2'/>
</disk>
<serial type='pty'/>
<console type='pty'/>
<graphics type='vnc' listen='127.0.0.1' autoport='yes'/>
<video><model type='vga'/></video>
<memballoon model='virtio'/>
</devices>
</domain>
XML
virsh define "$TMP/client2.xml" >/dev/null
log "defined $CLIENT2_VM"
fi
# Never attach a pxetest VM anywhere but default NAT / pxetest-imaging. # Never attach a pxetest VM anywhere but default NAT / pxetest-imaging.
for vm in "$SERVER_VM" "$CLIENT_VM"; do for vm in "$SERVER_VM" "$CLIENT_VM" "$CLIENT2_VM"; do
while read -r _ type src _ _; do while read -r _ type src _ _; do
case "$type:$src" in case "$type:$src" in
network:"$GE_NET") [ "$vm" = "$SERVER_VM" ] || die "$vm is on $GE_NET" ;; network:"$GE_NET") [ "$vm" = "$SERVER_VM" ] || die "$vm is on $GE_NET" ;;

View File

@@ -5,13 +5,13 @@
# Usage: tests/vm/destroy.sh # Usage: tests/vm/destroy.sh
source "$(dirname "$0")/common.sh" source "$(dirname "$0")/common.sh"
for vm in "$CLIENT_VM" "$SERVER_VM"; do for vm in "$CLIENT_VM" "$CLIENT2_VM" "$SERVER_VM"; do
dom_exists "$vm" || continue dom_exists "$vm" || continue
[ "$(dom_state "$vm")" = "shut off" ] || virsh destroy "$vm" >/dev/null [ "$(dom_state "$vm")" = "shut off" ] || virsh destroy "$vm" >/dev/null
virsh undefine "$vm" --nvram >/dev/null virsh undefine "$vm" --nvram >/dev/null
log "undefined $vm" log "undefined $vm"
done done
for v in "$SERVER_DISK_VOL" "$USB_VOL" "$CLIENT_DISK_VOL"; do for v in "$SERVER_DISK_VOL" "$USB_VOL" "$CLIENT_DISK_VOL" "$CLIENT2_DISK_VOL"; do
vol_exists "$v" || continue vol_exists "$v" || continue
virsh vol-delete --pool "$POOL" "$v" >/dev/null virsh vol-delete --pool "$POOL" "$v" >/dev/null
log "deleted volume $v" log "deleted volume $v"

View File

@@ -1,16 +1,19 @@
#!/bin/bash #!/bin/bash
# Start pxetest VMs. Idempotent (a running VM is left alone). # Start pxetest VMs. Idempotent (a running VM is left alone).
# Usage: tests/vm/start.sh [server|client|all] (default: server) # Usage: tests/vm/start.sh [server|client|client2|all] (default: server)
# Start the client only once the server's playbook has finished; both # client = Secure Boot client (virtio NIC: reaches the iPXE menu and WinPE)
# together use 6 GiB. # client2 = Secure Boot OFF client, e1000e NIC (WinPE gets a network)
# Start a client only once the server's playbook has finished, and only one
# client at a time: server + one client = 6 GiB.
source "$(dirname "$0")/common.sh" source "$(dirname "$0")/common.sh"
which="${1:-server}" which="${1:-server}"
case "$which" in case "$which" in
server) vms=("$SERVER_VM") ;; server) vms=("$SERVER_VM") ;;
client) vms=("$CLIENT_VM") ;; client) vms=("$CLIENT_VM") ;;
client2|nosb) vms=("$CLIENT2_VM") ;;
all) vms=("$SERVER_VM" "$CLIENT_VM") ;; all) vms=("$SERVER_VM" "$CLIENT_VM") ;;
*) die "usage: $0 [server|client|all]" ;; *) die "usage: $0 [server|client|client2|all]" ;;
esac esac
net_exists "$IMAGING_NET" || die "run create.sh first" net_exists "$IMAGING_NET" || die "run create.sh first"
@@ -19,6 +22,15 @@ assert_imaging_isolated
for vm in "${vms[@]}"; do for vm in "${vms[@]}"; do
dom_exists "$vm" || die "$vm not defined; run create.sh" dom_exists "$vm" || die "$vm not defined; run create.sh"
# RAM: one client at a time.
case "$vm" in
"$CLIENT_VM") other="$CLIENT2_VM" ;;
"$CLIENT2_VM") other="$CLIENT_VM" ;;
*) other="" ;;
esac
if [ -n "$other" ] && [ "$(dom_state "$other")" = "running" ]; then
die "$other is running; stop it first (tests/vm/stop.sh) to stay within RAM"
fi
if [ "$vm" = "$SERVER_VM" ]; then if [ "$vm" = "$SERVER_VM" ]; then
vol_exists "$USB_VOL" || die "no $USB_VOL; run build-image.sh" vol_exists "$USB_VOL" || die "no $USB_VOL; run build-image.sh"
fi fi

View File

@@ -12,14 +12,14 @@ echo "network $IMAGING_NET: $n"
if net_active "$IMAGING_NET"; then if net_active "$IMAGING_NET"; then
(assert_imaging_isolated && echo " isolation: OK (no forward, no ip, no host IPv4, only vnet ports)") (assert_imaging_isolated && echo " isolation: OK (no forward, no ip, no host IPv4, only vnet ports)")
fi fi
for v in "$SERVER_DISK_VOL" "$USB_VOL" "$CLIENT_DISK_VOL"; do for v in "$SERVER_DISK_VOL" "$USB_VOL" "$CLIENT_DISK_VOL" "$CLIENT2_DISK_VOL"; do
if vol_exists "$v"; then if vol_exists "$v"; then
echo "volume $v: $(virsh vol-info --pool "$POOL" "$v" | awk '/Capacity|Allocation/{printf "%s %s %s ", $1, $2, $3}')" echo "volume $v: $(virsh vol-info --pool "$POOL" "$v" | awk '/Capacity|Allocation/{printf "%s %s %s ", $1, $2, $3}')"
else else
echo "volume $v: absent" echo "volume $v: absent"
fi fi
done done
for vm in "$SERVER_VM" "$CLIENT_VM"; do for vm in "$SERVER_VM" "$CLIENT_VM" "$CLIENT2_VM"; do
if dom_exists "$vm"; then if dom_exists "$vm"; then
echo "domain $vm: $(dom_state "$vm")" echo "domain $vm: $(dom_state "$vm")"
virsh domiflist "$vm" | tail -n +3 | grep -v '^$' | sed 's/^/ /' virsh domiflist "$vm" | tail -n +3 | grep -v '^$' | sed 's/^/ /'

View File

@@ -1,15 +1,16 @@
#!/bin/bash #!/bin/bash
# Shut pxetest VMs down (ACPI, then force after a timeout). Keeps them # Shut pxetest VMs down (ACPI, then force after a timeout). Keeps them
# defined. Idempotent. # defined. Idempotent.
# Usage: tests/vm/stop.sh [server|client|all] (default: all) # Usage: tests/vm/stop.sh [server|client|client2|all] (default: all)
source "$(dirname "$0")/common.sh" source "$(dirname "$0")/common.sh"
which="${1:-all}" which="${1:-all}"
case "$which" in case "$which" in
server) vms=("$SERVER_VM") ;; server) vms=("$SERVER_VM") ;;
client) vms=("$CLIENT_VM") ;; client) vms=("$CLIENT_VM") ;;
all) vms=("$CLIENT_VM" "$SERVER_VM") ;; client2|nosb) vms=("$CLIENT2_VM") ;;
*) die "usage: $0 [server|client|all]" ;; all) vms=("$CLIENT_VM" "$CLIENT2_VM" "$SERVER_VM") ;;
*) die "usage: $0 [server|client|client2|all]" ;;
esac esac
for vm in "${vms[@]}"; do for vm in "${vms[@]}"; do
dom_exists "$vm" || continue dom_exists "$vm" || continue