refactor(ci): parallel provisioning, file caching, provision/test/cleanup split

Restructures the integration test workflow from a monolithic sequential
job into separate provision → test → cleanup stages:

- provision: creates ALL nested PVE VMs in a single terraform apply
  (parallel), waits for APIs, creates tokens, passes outputs to tests
- test: matrix [pve9, pve8] consumes provision outputs, no provisioning
- cleanup: always runs, API-only teardown for all VMs

Terraform refactored to for_each with pve_instances map variable,
enabling parallel ISO upload and VM creation.

New caching scripts reduce redundant downloads:
- ensure-base-iso.sh: downloads PVE ISOs to /opt/pve-isos if missing
- ensure-cloud-images.sh: caches cloud image + OVA with 7-day TTL
- prepare-auto-iso.sh: --cache-dir flag with hash-based skip

Runner no longer needs manual ISO provisioning (zero-touch setup).
cleanup-images bumped to min-versions-to-keep: 3 to survive overlapping
runs. All jobs gated against dependabot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-23 16:05:20 -05:00
parent d19c6f0c4b
commit a424dd18ac
9 changed files with 561 additions and 221 deletions
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Downloads a PVE base ISO to the cache directory if not already present.
#
# Usage: ensure-base-iso.sh <iso-filename> <cache-dir>
# iso-filename: e.g. proxmox-ve_9.1-1.iso
# cache-dir: e.g. /opt/pve-isos
set -euo pipefail
ISO_FILENAME="${1:?Usage: ensure-base-iso.sh <iso-filename> <cache-dir>}"
CACHE_DIR="${2:?Cache directory required}"
CACHED_PATH="${CACHE_DIR}/${ISO_FILENAME}"
if [ -f "${CACHED_PATH}" ] && [ -s "${CACHED_PATH}" ]; then
echo "Base ISO already cached: ${CACHED_PATH} ($(du -h "${CACHED_PATH}" | cut -f1))"
exit 0
fi
# Ensure cache directory exists and is writable
mkdir -p "${CACHE_DIR}"
DOWNLOAD_URL="http://download.proxmox.com/iso/${ISO_FILENAME}"
TMP_PATH="${CACHED_PATH}.downloading"
echo "Downloading PVE base ISO: ${DOWNLOAD_URL}"
echo " Target: ${CACHED_PATH}"
# Download to temp file, then atomic move
if curl -fSL --progress-bar -o "${TMP_PATH}" "${DOWNLOAD_URL}"; then
mv "${TMP_PATH}" "${CACHED_PATH}"
echo "Downloaded: ${CACHED_PATH} ($(du -h "${CACHED_PATH}" | cut -f1))"
else
rm -f "${TMP_PATH}"
echo "ERROR: Failed to download ${DOWNLOAD_URL}" >&2
exit 1
fi
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Downloads cloud image and OVA to the cache directory if not already present
# or if the cached copy is older than 7 days.
#
# Usage: ensure-cloud-images.sh <cache-dir>
#
# Outputs (for use in GITHUB_OUTPUT):
# CLOUD_IMAGE_PATH=<path>
# OVA_PATH=<path>
set -euo pipefail
CACHE_DIR="${1:?Usage: ensure-cloud-images.sh <cache-dir>}"
MAX_AGE_DAYS=7
CLOUD_IMAGE_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
CLOUD_IMAGE_FILENAME="noble-server-cloudimg-amd64.qcow2"
OVA_URL="https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.ova"
OVA_FILENAME="ubuntu-24.04-server-cloudimg-amd64.ova"
mkdir -p "${CACHE_DIR}"
download_if_stale() {
local url="$1"
local filepath="$2"
local description="$3"
if [ -f "${filepath}" ] && [ -s "${filepath}" ]; then
# Check age
local age_days
age_days=$(( ( $(date +%s) - $(stat -c %Y "${filepath}" 2>/dev/null || stat -f %m "${filepath}" 2>/dev/null) ) / 86400 ))
if [ "${age_days}" -lt "${MAX_AGE_DAYS}" ]; then
echo "${description} cached and fresh (${age_days}d old): ${filepath}"
return 0
fi
echo "${description} is ${age_days}d old, re-downloading..."
else
echo "Downloading ${description}..."
fi
local tmp_path="${filepath}.downloading"
if curl -fSL --progress-bar -o "${tmp_path}" "${url}"; then
mv "${tmp_path}" "${filepath}"
echo "Downloaded ${description}: $(du -h "${filepath}" | cut -f1)"
else
rm -f "${tmp_path}"
# If we have a stale copy, keep using it
if [ -f "${filepath}" ]; then
echo "WARNING: Download failed, using stale cached copy" >&2
return 0
fi
echo "ERROR: Failed to download ${description}" >&2
return 1
fi
}
download_if_stale "${CLOUD_IMAGE_URL}" "${CACHE_DIR}/${CLOUD_IMAGE_FILENAME}" "Ubuntu cloud image"
download_if_stale "${OVA_URL}" "${CACHE_DIR}/${OVA_FILENAME}" "Ubuntu OVA"
echo "CLOUD_IMAGE_PATH=${CACHE_DIR}/${CLOUD_IMAGE_FILENAME}"
echo "OVA_PATH=${CACHE_DIR}/${OVA_FILENAME}"
@@ -2,13 +2,34 @@
# Prepare a PVE auto-install ISO with the answer file and first-boot script baked in.
# Uses --fetch-from iso mode so only a single CD-ROM is needed, no HTTP server required.
#
# Usage: prepare-auto-iso.sh <base-iso> <answer-file> [first-boot-script] [output-iso]
# Usage: prepare-auto-iso.sh <base-iso> <answer-file> [first-boot-script] [output-iso] [--cache-dir <dir>]
#
# When --cache-dir is specified, the script caches the generated ISO keyed by a hash
# of the inputs (base ISO path + answer file content + first-boot script content).
# If the cache is warm and inputs haven't changed, the cached ISO is copied to the
# output path without regeneration.
set -euo pipefail
# Parse arguments
BASE_ISO="$1"
ANSWER_FILE="$2"
FIRST_BOOT="${3:-}"
OUTPUT_ISO="${4:-${BASE_ISO%.iso}-auto.iso}"
CACHE_DIR=""
# Check for --cache-dir flag in remaining args
shift 4 2>/dev/null || true
while [ $# -gt 0 ]; do
case "$1" in
--cache-dir)
CACHE_DIR="$2"
shift 2
;;
*)
shift
;;
esac
done
for f in "$BASE_ISO" "$ANSWER_FILE"; do
if [ ! -f "$f" ]; then
@@ -22,6 +43,33 @@ if [ -n "$FIRST_BOOT" ] && [ -f "$FIRST_BOOT" ]; then
FIRST_BOOT_ARGS=(--on-first-boot "$FIRST_BOOT")
fi
# --- Cache check ---
if [ -n "$CACHE_DIR" ]; then
mkdir -p "$CACHE_DIR"
# Build a hash of all inputs to detect changes
INPUT_HASH=$(cat "$ANSWER_FILE" ${FIRST_BOOT:+"$FIRST_BOOT"} | sha256sum | cut -d' ' -f1)
BASE_HASH=$(sha256sum "$BASE_ISO" | cut -d' ' -f1)
CACHE_KEY="${BASE_HASH:0:16}_${INPUT_HASH:0:16}"
CACHED_ISO="${CACHE_DIR}/$(basename "$OUTPUT_ISO")"
CACHED_HASH_FILE="${CACHED_ISO}.inputhash"
if [ -f "$CACHED_ISO" ] && [ -s "$CACHED_ISO" ] && [ -f "$CACHED_HASH_FILE" ]; then
STORED_HASH=$(cat "$CACHED_HASH_FILE")
if [ "$STORED_HASH" = "$CACHE_KEY" ]; then
echo "Cached auto-install ISO is up to date (hash: ${CACHE_KEY})"
if [ "$CACHED_ISO" != "$OUTPUT_ISO" ]; then
cp "$CACHED_ISO" "$OUTPUT_ISO"
fi
echo "Using cached: $OUTPUT_ISO"
exit 0
fi
echo "Cache stale (stored: ${STORED_HASH}, current: ${CACHE_KEY}), regenerating..."
else
echo "No cached auto-install ISO found, generating..."
fi
fi
echo "Preparing auto-install ISO..."
echo " Base ISO: $BASE_ISO"
echo " Answer file: $ANSWER_FILE"
@@ -38,3 +86,12 @@ proxmox-auto-install-assistant prepare-iso \
"$BASE_ISO"
echo "Created: $OUTPUT_ISO"
# --- Update cache ---
if [ -n "$CACHE_DIR" ]; then
if [ "$CACHED_ISO" != "$OUTPUT_ISO" ]; then
cp "$OUTPUT_ISO" "$CACHED_ISO"
fi
echo "$CACHE_KEY" > "$CACHED_HASH_FILE"
echo "Cached auto-install ISO (hash: ${CACHE_KEY})"
fi
@@ -1,26 +1,17 @@
#!/usr/bin/env bash
# Prepares the test environment on the nested PVE node.
# Only performs operations that have no PVE API equivalent, plus
# downloads test artifacts for the integration tests to upload.
# Only performs operations that have no PVE API equivalent.
#
# Usage: prepare-test-environment.sh <nested-pve-ip> <root-password> <output-dir>
# Usage: prepare-test-environment.sh <nested-pve-ip> <root-password>
#
# Operations:
# - Enable snippets+import content types on local storage (pvesm set)
# - Upload cloud-init user-data snippet (SCP — no snippet upload API)
# - Download Ubuntu cloud image to <output-dir> for upload tests
set -euo pipefail
NESTED_IP="${1:?Usage: prepare-test-environment.sh <ip> <password> <output-dir>}"
NESTED_IP="${1:?Usage: prepare-test-environment.sh <ip> <password>}"
ROOT_PASS="$2"
OUTPUT_DIR="${3:?Output directory required}"
CLOUD_IMAGE_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
# PVE upload API validates extensions per content type — content=import
# does not accept .img. The Ubuntu cloud image is qcow2 format, so we
# rename it to .qcow2 for compatibility with the upload endpoint.
CLOUD_IMAGE_FILENAME="noble-server-cloudimg-amd64.qcow2"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
SSH_CMD="sshpass -p ${ROOT_PASS} ssh ${SSH_OPTS} root@${NESTED_IP}"
@@ -47,27 +38,4 @@ YAML
${SCP_CMD} "${USERDATA}" "root@${NESTED_IP}:/var/lib/vz/snippets/test-vm-userdata.yml"
rm -f "${USERDATA}"
# Download cloud image for integration tests to upload via Send-PveFile
CLOUD_IMAGE_PATH="${OUTPUT_DIR}/${CLOUD_IMAGE_FILENAME}"
if [ ! -f "${CLOUD_IMAGE_PATH}" ]; then
echo "Downloading Ubuntu cloud image..."
curl -fSL -o "${CLOUD_IMAGE_PATH}" "${CLOUD_IMAGE_URL}"
else
echo "Cloud image already cached at ${CLOUD_IMAGE_PATH}"
fi
# Download Ubuntu cloud OVA for Import-PveOva testing
OVA_URL="https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.ova"
OVA_FILENAME="ubuntu-24.04-server-cloudimg-amd64.ova"
OVA_PATH="${OUTPUT_DIR}/${OVA_FILENAME}"
if [ ! -f "${OVA_PATH}" ]; then
echo "Downloading Ubuntu cloud OVA (this may take a few minutes)..."
curl -fSL -o "${OVA_PATH}" "${OVA_URL}"
echo "Downloaded OVA ($(du -h "${OVA_PATH}" | cut -f1))"
else
echo "OVA already cached at ${OVA_PATH}"
fi
echo "CLOUD_IMAGE_PATH=${CLOUD_IMAGE_PATH}"
echo "OVA_PATH=${OVA_PATH}"
echo "Environment preparation complete."