set -euo pipefail
tag=${1:-${TAG:-latest}}
: "${PVE_TOKEN:?PVE_TOKEN is not set: a Proxmox API token, user@realm!tokenid=secret (docs/VM_TEST.md)}"
pve=${PVE_URL:-https://proxmox.in.coldairnetworks.com}/api2/json
node=${PVE_NODE:-zeus}
vmid=${VMID:-107}
storage=${PVE_STORAGE:-local}
iso=quine-linux-live-test.iso
timeout=${BOOT_TIMEOUT:-900}
bucket=${MINIO_BUCKET:-quine-linux}
public=https://s3.quineglobal.com/$bucket
log() { echo "== $*" >&2; }
api() {
local method=$1 path=$2 out code
shift 2
out=$(curl -sS -w '\n%{http_code}' -X "$method" -H "Authorization: PVEAPIToken=$PVE_TOKEN" "$pve$path" "$@") || return 1
code=${out##*$'\n'}
out=${out%$'\n'*}
if [ "${code:0:1}" != 2 ]; then
echo "Proxmox: $method $path: HTTP $code $(jq -r '.message // .errors // empty' <<<"$out" 2>/dev/null)" >&2
return 1
fi
jq -c '.data' <<<"$out"
}
wait_task() {
local upid=$1 status
while :; do
status=$(api GET "/nodes/$node/tasks/$(jq -rn --arg u "$upid" '$u|@uri')/status")
[ "$(jq -r .status <<<"$status")" = stopped ] && break
sleep 3
done
[ "$(jq -r .exitstatus <<<"$status")" = OK ] || {
echo "::error::Proxmox task failed: $(jq -r .exitstatus <<<"$status") ($upid)" >&2
exit 1
}
}
checksum=
if [ -n "${ISO_URL:-}" ]; then
url=$ISO_URL
elif [ "$tag" = latest ]; then
command -v rclone >/dev/null || { echo "::error::TAG=latest needs rclone and MinIO's credentials (or set ISO_URL)" >&2; exit 1; }
url=$(rclone link --expire 3h "store:$bucket/live/latest/live.iso")
else
name=quine-linux-$tag-live-x86_64.iso
url=$public/releases/$tag/$name
checksum=$(curl -fsS "$public/releases/$tag/SHA256SUMS" | awk -v n="$name" '$2 == n || $2 == "*"n {print $1}')
[ -n "$checksum" ] || { echo "::error::$name isn't in $tag's SHA256SUMS" >&2; exit 1; }
fi
log "VM $vmid on $node: booting $tag"
boot=$(api GET "/nodes/$node/qemu/$vmid/config" | jq -r '.boot // ""')
case "$boot" in
order=ide2*) ;;
*) echo "::error::VM $vmid must boot from its CD first (boot: order=ide2;...), not '$boot'. Set it in Proxmox; the token can't (docs/VM_TEST.md)." >&2; exit 1 ;;
esac
if [ "$(api GET "/nodes/$node/qemu/$vmid/status/current" | jq -r .status)" = running ]; then
log "stopping VM $vmid"
wait_task "$(api POST "/nodes/$node/qemu/$vmid/status/stop" | jq -r .)"
fi
log "replacing $storage:iso/$iso"
api PUT "/nodes/$node/qemu/$vmid/config" --data-urlencode "ide2=none,media=cdrom" >/dev/null
api DELETE "/nodes/$node/storage/$storage/content/$storage:iso/$iso" >/dev/null 2>&1 || true
args=(--data-urlencode "url=$url" --data-urlencode "content=iso" --data-urlencode "filename=$iso")
[ -z "$checksum" ] || args+=(--data-urlencode "checksum=$checksum" --data-urlencode "checksum-algorithm=sha256")
wait_task "$(api POST "/nodes/$node/storage/$storage/download-url" "${args[@]}" | jq -r .)"
api PUT "/nodes/$node/qemu/$vmid/config" \
--data-urlencode "ide2=$storage:iso/$iso,media=cdrom" \
--data-urlencode "agent=enabled=1" >/dev/null
log "starting VM $vmid"
wait_task "$(api POST "/nodes/$node/qemu/$vmid/status/start" | jq -r .)"
started=$SECONDS
log "waiting up to ${timeout}s for the live system's guest agent"
until osinfo=$(api GET "/nodes/$node/qemu/$vmid/agent/get-osinfo" 2>/dev/null) && [ -n "$osinfo" ] && [ "$osinfo" != null ]; do
if [ $((SECONDS - started)) -ge "$timeout" ]; then
echo "::error::VM $vmid didn't come up in ${timeout}s: no answer from its guest agent. Look at its console in Proxmox." >&2
exit 1
fi
sleep 10
done
result=$(jq -r --arg tag "$tag" --arg s "$((SECONDS - started))" '"\($tag) booted on VM '"$vmid"' in \($s)s: \(.result["pretty-name"] // .result.name) (kernel \(.result["kernel-release"]))"' <<<"$osinfo")
log "$result"
[ -z "${GITHUB_STEP_SUMMARY:-}" ] || echo "$result" >>"$GITHUB_STEP_SUMMARY"