mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 10:52:04 +00:00
28ca689130
* feat: add GPU discovery package (mulga-992) Adds spinifex/gpu package with sysfs-based GPU enumeration, IOMMU group member lookup, and a PCI device ID model table. Vendor-specific enrichment via nvidia-smi is attempted opportunistically and falls back to the built-in table when unavailable. All discovery and IOMMU logic is covered by unit tests using mock sysfs trees. * feat: add VFIO bind/unbind and GPU pool manager (mulga-993) Adds gpu.BindVFIO/UnbindVFIO using sysfs driver_override for per-device driver switching, and gpu.Manager which owns the claim/release lifecycle for the GPU pool including full IOMMU group binding, rollback on partial failure, and marking entries unavailable when rebind fails after VM termination. * feat: add G5 instance type generator with GPU model support (mulga-994) - Add GPUModel type, NVIDIAa10g constant, and GPUModelForVendorDevice lookup - Add generateGPUTypes to emit g5.* types with GpuInfo populated - Add IsGPUType helper for consumers filtering GPU-capable types - Update DetectAndGenerate to accept gpuModels []GPUModel parameter * feat: integrate GPU passthrough into daemon lifecycle (mulga-995) - Add gpu_passthrough config flag; discover GPUs and build manager at startup - Gate g5 instance types on GPU pool availability via canAllocateCount - Claim GPU (VFIO bind) in LaunchInstance; inject vfio-pci QEMU device - Release GPU (VFIO unbind, rebind to host driver) in stopInstance - Re-claim GPU on daemon restart when QEMU process is still alive - Add UseUEFI / OVMF support for x86_64 VMs (optional, falls back to SeaBIOS) - Fix Claim to use stored OriginalDriver when device is already vfio-pci bound * feat: add GPU fields to NodeStatus and spx top nodes (mulga-996) - Add TotalGPUs/AllocGPUs to NodeStatusResponse (zero on non-GPU nodes) - Populate from gpuManager in handleNodeStatus - Show GPU (used/total) column in spx top nodes; "-" when no GPUs present * feat: Ubuntu 24.04 GPU AMI build pipeline (mulga-997) Extends build-system-image.sh to support Ubuntu alongside Alpine. Key differences handled: partition table (parted resizepart p1), bind mounts for DKMS (/proc /sys /dev), apt vs apk, systemctl vs rc-update. DISTRO defaults to "alpine" for backward compatibility. Adds ubuntu-gpu.conf manifest and ubuntu-gpu-setup.sh chroot script that installs nvidia-headless-550-server with DKMS pre-built against a pinned linux-image-generic kernel, blacklists nouveau, and prevents unattended-upgrades from invalidating the pre-built module. * Fixed GPU passthrough and added test script * feat: always-probe GPU at startup, surface capability in NodeStatusResponse - Add gpuProbeResult struct and probeGPU() which runs at daemon startup regardless of gpu_passthrough config. Checks sysfs for GPUs, IOMMU groups, and vfio_pci module presence. - Store gpuProbe on Daemon struct; split NewDaemon into probe (always) and activate (opt-in) phases. - Populate GPUCapable, GPUPassthrough, GPUModels on NodeStatusResponse so operators can see hardware state before enabling passthrough. - Update 'spx top' GPU column: '-' = no hardware, '0/N*' = capable but passthrough not active, 'used/total' = passthrough active. * feat: SIGHUP hot-reload for GPU passthrough config (mulga-1001) - Export GenerateGPUTypes for use by the reload path - Add ResourceManager.reloadGPUTypes: mutates instanceTypes map in-place (preserves references held by instanceService) and updates NATS subscriptions without full daemon restart - Separate SIGHUP from SIGTERM/SIGINT: setupReload() listens for SIGHUP in a context-aware loop; setupShutdown no longer intercepts SIGHUP - Add reloadConfig() + applyGPUConfig(): re-reads spinifex.toml on signal, handles false→true (re-probe + activate) and true→false (refuse if instances running, otherwise deactivate) transitions * feat: spx admin gpu status/enable/disable commands (mulga-1000) - admin.SetGPUPassthrough: Go implementation of the idempotent TOML write (replaces the inline Python heredoc in spx-test-gpu.sh) - spx admin gpu status: shows GPU hardware, IOMMU/vfio state, passthrough activation, and advertised g5 instance types via NATS NodeStatusResponse - spx admin gpu enable: checks prerequisites, writes TOML, sends SIGHUP, polls until daemon confirms GPUPassthrough=true - spx admin gpu disable: refuses while GPU instances are running, otherwise writes TOML, sends SIGHUP, polls until confirmed - spx-test-gpu.sh: replace Python TOML heredoc with spx admin gpu enable * fixed admin * fix: filter Intel iGPU from passthrough pool, use lspci for consumer GPU names Discover() now skips non-NVIDIA/AMD vendors so Intel iGPUs never enter the passthrough pool. enrichFromLspci() uses the system PCI ID database via lspci -vmm to populate the model name for any card not in the hardcoded table, without needing nvidia-smi or driver tools. * fix: grant CAP_SYS_ADMIN and vfio device access to spinifex-daemon GPU passthrough requires writing to /sys/bus/pci/drivers/vfio-pci/unbind on instance stop. The kernel gates this on CAP_SYS_ADMIN regardless of file permissions. QEMU subprocesses also need char-vfio rw access to open /dev/vfio/N for the passed-through device. * fixing gpu bining permissions * working state of gpu passthrough on phyiscal box * installer grub image fixes * made gpu-passthrough more user facing * fixed banner * fixed banner * banner fix * gpu passthrough docs * preflight tweak * vpcd fixes to set br-wan to promisc for public IP DHCP processes * fixed vpcd br-wan promisc and veth pair * ssh keygen fix * fixes after running /review-branch * feat: re-wire GPU claim and device injection after vm.Manager refactor GPU passthrough claim (VFIO bind) and QEMU device injection were lost when dev's vm.Manager refactor (#185/#186) moved StartInstance/ CreateQMPClient into vm/lifecycle.go. Re-add both in their correct new homes: - daemon_handlers_instance.go: claim GPU before vmMgr.Run() in both RunInstances and StartStoppedInstance handlers; release on failure - vm/lifecycle.go startQEMU(): inject vfio-pci device with x-vga flag derived from instance.GPUXVGAEnabled when GPUPCIAddress is set * fix: Create spinifex user ~/.ssh and keys in firstboot --------- Co-authored-by: Ben Duncan <5178243+benduncan@users.noreply.github.com> Co-authored-by: Julian Sommer <115516918+juliansommer@users.noreply.github.com>
274 lines
8.9 KiB
Bash
Executable File
274 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# smoke-test.sh — Bare-bones smoke test for a running Spinifex node.
|
|
#
|
|
# Imports an SSH keypair, imports the Ubuntu AMI, and launches a single
|
|
# instance to verify the platform is functional end-to-end.
|
|
#
|
|
# Assumes services are running and AWS_PROFILE=spinifex is configured
|
|
# by a prior 'spx admin init'.
|
|
#
|
|
# GPU passthrough test (opt-in):
|
|
# TEST_GPU=1 scripts/smoke-test.sh
|
|
# TEST_GPU=1 GPU_VENDOR_ID=10de GPU_DEVICE_ID=2236 scripts/smoke-test.sh
|
|
#
|
|
# GPU env vars:
|
|
# TEST_GPU=1 enable GPU passthrough checks
|
|
# GPU_VENDOR_ID PCI vendor ID (default: auto-detect first NVIDIA VGA/3D device)
|
|
# GPU_DEVICE_ID PCI device ID (default: auto-detect)
|
|
# GPU_FAMILY instance family to test (default: g5)
|
|
set -euo pipefail
|
|
|
|
TEST_GPU="${TEST_GPU:-0}"
|
|
GPU_VENDOR_ID="${GPU_VENDOR_ID:-}"
|
|
GPU_DEVICE_ID="${GPU_DEVICE_ID:-}"
|
|
GPU_FAMILY="${GPU_FAMILY:-g5}"
|
|
|
|
export AWS_PROFILE=spinifex
|
|
|
|
# --- Wait for EC2 daemon to subscribe to NATS ---
|
|
# Port 3000 (UI) becomes ready before the daemon finishes initialising.
|
|
# Poll describe-key-pairs until it doesn't return InternalError.
|
|
echo "==> Waiting for EC2 daemon to be ready"
|
|
DAEMON_TIMEOUT=60
|
|
DAEMON_ELAPSED=0
|
|
while [ $DAEMON_ELAPSED -lt $DAEMON_TIMEOUT ]; do
|
|
if aws ec2 describe-key-pairs --output text >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
DAEMON_ELAPSED=$((DAEMON_ELAPSED + 2))
|
|
done
|
|
if [ $DAEMON_ELAPSED -ge $DAEMON_TIMEOUT ]; then
|
|
echo "❌ EC2 daemon not ready after ${DAEMON_TIMEOUT}s"
|
|
exit 1
|
|
fi
|
|
echo " EC2 daemon ready after ${DAEMON_ELAPSED}s"
|
|
|
|
# --- SSH key ---
|
|
SSH_KEY="$HOME/.ssh/spinifex-key"
|
|
if [ ! -f "$SSH_KEY.pub" ]; then
|
|
echo "==> Generating SSH key pair"
|
|
mkdir -p "$HOME/.ssh"
|
|
ssh-keygen -t ed25519 -f "$SSH_KEY" -N ""
|
|
fi
|
|
|
|
echo "==> Importing SSH key"
|
|
! aws ec2 import-key-pair --key-name spinifex-key \
|
|
--public-key-material "fileb://$SSH_KEY.pub"
|
|
aws ec2 describe-key-pairs
|
|
|
|
# --- GPU pre-flight (TEST_GPU=1 only) ---
|
|
if [[ "$TEST_GPU" == "1" ]]; then
|
|
echo "==> [GPU] Checking IOMMU"
|
|
if ! dmesg | grep -qi "DMAR.*IOMMU enabled\|iommu.*Translated\|Adding to iommu group\|AMD-Vi.*enabled"; then
|
|
echo "❌ IOMMU not detected — add intel_iommu=on (or amd_iommu=on) and iommu=pt to kernel cmdline, then reboot"
|
|
exit 1
|
|
fi
|
|
echo " IOMMU active"
|
|
|
|
echo "==> [GPU] Detecting NVIDIA GPU"
|
|
if [[ -z "$GPU_VENDOR_ID" || -z "$GPU_DEVICE_ID" ]]; then
|
|
GPU_LINE=$(lspci -nn | grep -i nvidia | grep -iE "VGA|3D" | head -1)
|
|
if [[ -z "$GPU_LINE" ]]; then
|
|
echo "❌ No NVIDIA GPU found via lspci — is the card seated and detectable?"
|
|
lspci | grep -i nvidia >&2 || true
|
|
exit 1
|
|
fi
|
|
echo " Detected: $GPU_LINE"
|
|
PCI_IDS=$(echo "$GPU_LINE" | grep -oP '\[\K[0-9a-f]{4}:[0-9a-f]{4}(?=\])')
|
|
GPU_VENDOR_ID=$(echo "$PCI_IDS" | cut -d: -f1)
|
|
GPU_DEVICE_ID=$(echo "$PCI_IDS" | cut -d: -f2)
|
|
fi
|
|
echo " GPU PCI ID: ${GPU_VENDOR_ID}:${GPU_DEVICE_ID}"
|
|
|
|
echo "==> [GPU] Verifying ${GPU_FAMILY} instance types advertised"
|
|
G5_TYPES=$(aws ec2 describe-instance-types \
|
|
--query "InstanceTypes[?starts_with(InstanceType, \`${GPU_FAMILY}\`)].InstanceType" \
|
|
--output text 2>/dev/null || true)
|
|
if [[ -z "$G5_TYPES" ]]; then
|
|
echo "❌ No ${GPU_FAMILY}.* instance types returned by describe-instance-types"
|
|
exit 1
|
|
fi
|
|
echo " ${GPU_FAMILY} types available: $G5_TYPES"
|
|
fi
|
|
|
|
# --- Import AMI ---
|
|
# --- Import AMI ---
|
|
echo "==> Importing AMI"
|
|
|
|
LOCAL_IMAGE="$HOME/images/ubuntu-26.04.img"
|
|
ARCH=$(uname -m)
|
|
|
|
case "$ARCH" in
|
|
x86_64) IMG_ARCH="x86_64"; IMAGE_NAME="ubuntu-26.04-x86_64" ;;
|
|
aarch64|arm64) IMG_ARCH="arm64"; IMAGE_NAME="ubuntu-26.04-arm64" ;;
|
|
*)
|
|
echo " Warning: unknown arch $ARCH, defaulting to x86_64"
|
|
IMG_ARCH="x86_64"; IMAGE_NAME="ubuntu-26.04-x86_64"
|
|
;;
|
|
esac
|
|
|
|
AMI_NAME="ami-${IMAGE_NAME}"
|
|
|
|
EXISTING_AMI_ID=$(aws ec2 describe-images \
|
|
--query "Images[?Name=='${AMI_NAME}'] | [0].ImageId" \
|
|
--output text)
|
|
|
|
if [ -n "$EXISTING_AMI_ID" ] && [ "$EXISTING_AMI_ID" != "None" ]; then
|
|
echo " AMI already exists, skipping import: $AMI_NAME ($EXISTING_AMI_ID)"
|
|
else
|
|
if [ -f "$LOCAL_IMAGE" ]; then
|
|
echo " Using local image: $LOCAL_IMAGE"
|
|
sudo /usr/local/bin/spx admin images import \
|
|
--file "$LOCAL_IMAGE" --distro ubuntu --version 26.04 --arch "$IMG_ARCH"
|
|
else
|
|
echo " Downloading image: $IMAGE_NAME"
|
|
sudo /usr/local/bin/spx admin images import --name "$IMAGE_NAME"
|
|
fi
|
|
fi
|
|
|
|
# --- Launch smoke-test instance ---
|
|
echo "==> Launching smoke-test instance"
|
|
if [[ "$TEST_GPU" == "1" ]]; then
|
|
INSTANCE_TYPE="${GPU_FAMILY}.xlarge"
|
|
elif grep -q 'AuthenticAMD' /proc/cpuinfo; then
|
|
INSTANCE_TYPE="t3a.small"
|
|
else
|
|
INSTANCE_TYPE="t3.small"
|
|
fi
|
|
|
|
AMI_ID=$(aws ec2 describe-images \
|
|
--query "Images[?Name=='${AMI_NAME}'] | [0].ImageId" \
|
|
--output text)
|
|
|
|
if [ -z "$AMI_ID" ] || [ "$AMI_ID" = "None" ]; then
|
|
echo "❌ No AMI found"
|
|
exit 1
|
|
fi
|
|
|
|
SUBNET_ID=$(aws ec2 describe-subnets --query 'Subnets[?MapPublicIpOnLaunch==`true`].SubnetId | [0]' --output text)
|
|
if [ -z "$SUBNET_ID" ] || [ "$SUBNET_ID" = "None" ]; then
|
|
echo "❌ No subnet found"
|
|
exit 1
|
|
fi
|
|
|
|
echo " AMI: $AMI_ID type: $INSTANCE_TYPE subnet: $SUBNET_ID"
|
|
|
|
INSTANCE_ID=$(aws ec2 run-instances \
|
|
--image-id "$AMI_ID" \
|
|
--instance-type "$INSTANCE_TYPE" \
|
|
--key-name spinifex-key \
|
|
--subnet-id "$SUBNET_ID" \
|
|
--count 1 \
|
|
--query 'Instances[0].InstanceId' --output text)
|
|
|
|
if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "None" ] || [ "$INSTANCE_ID" = "null" ]; then
|
|
echo "❌ run-instances returned no InstanceId"
|
|
exit 1
|
|
fi
|
|
echo " Instance ID: $INSTANCE_ID"
|
|
|
|
# --- Wait for running state ---
|
|
echo "==> Waiting for instance to reach running state"
|
|
COUNT=0
|
|
STATE="unknown"
|
|
while [ $COUNT -lt 60 ]; do
|
|
DESCRIBE=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" 2>/dev/null) || {
|
|
sleep 2; COUNT=$((COUNT + 1)); continue
|
|
}
|
|
STATE=$(echo "$DESCRIBE" | jq -r '.Reservations[0].Instances[0].State.Name // "not-found"')
|
|
[ "$STATE" = "running" ] && break
|
|
if [ "$STATE" = "terminated" ]; then
|
|
echo "❌ Instance terminated unexpectedly"
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
COUNT=$((COUNT + 1))
|
|
done
|
|
if [ "$STATE" != "running" ]; then
|
|
echo "❌ Instance failed to reach running state (last: $STATE)"
|
|
exit 1
|
|
fi
|
|
echo " Instance is running"
|
|
|
|
# --- Wait for public IP ---
|
|
echo "==> Waiting for public IP assignment (up to 300s)"
|
|
SSH_INST_PORT=22
|
|
SSH_INST_HOST=""
|
|
for _i in $(seq 1 300); do
|
|
_ip=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
|
|
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text 2>/dev/null || true)
|
|
if [ -n "$_ip" ] && [ "$_ip" != "None" ] && [ "$_ip" != "null" ]; then
|
|
SSH_INST_HOST="$_ip"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ -z "$SSH_INST_HOST" ]; then
|
|
echo "❌ No public IP assigned after 300s — external networking not working"
|
|
exit 1
|
|
fi
|
|
echo " Public IP: $SSH_INST_HOST"
|
|
|
|
# --- Wait for SSH ---
|
|
echo "==> Waiting for SSH on $SSH_INST_HOST:$SSH_INST_PORT"
|
|
SSH_READY=0
|
|
for _i in $(seq 1 300); do
|
|
if ssh -o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null \
|
|
-o ConnectTimeout=2 \
|
|
-o BatchMode=yes \
|
|
-p "$SSH_INST_PORT" \
|
|
-i "$SSH_KEY" \
|
|
ec2-user@"$SSH_INST_HOST" 'echo ready' >/dev/null 2>&1; then
|
|
SSH_READY=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ $SSH_READY -eq 0 ]; then
|
|
echo "❌ SSH not ready after 300s"
|
|
exit 1
|
|
fi
|
|
echo " SSH is ready"
|
|
|
|
# --- Verify instance identity via SSH ---
|
|
echo "==> Verifying instance"
|
|
SSH_OUT=$(ssh -o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null \
|
|
-o ConnectTimeout=5 \
|
|
-o BatchMode=yes \
|
|
-p "$SSH_INST_PORT" \
|
|
-i "$SSH_KEY" \
|
|
ec2-user@"$SSH_INST_HOST" 'id && hostname' 2>&1)
|
|
echo " $SSH_OUT"
|
|
if ! echo "$SSH_OUT" | grep -q "ec2-user"; then
|
|
echo "❌ Expected ec2-user in SSH output"
|
|
exit 1
|
|
fi
|
|
|
|
# --- GPU guest verification (TEST_GPU=1 only) ---
|
|
if [[ "$TEST_GPU" == "1" ]]; then
|
|
echo "==> [GPU] Verifying GPU visible inside guest"
|
|
GPU_VISIBLE=0
|
|
for _i in $(seq 1 12); do
|
|
if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
-o ConnectTimeout=5 -o BatchMode=yes \
|
|
-p "$SSH_INST_PORT" -i "$SSH_KEY" \
|
|
ec2-user@"$SSH_INST_HOST" \
|
|
'lspci 2>/dev/null | grep -i nvidia' 2>/dev/null; then
|
|
GPU_VISIBLE=1
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
if [[ $GPU_VISIBLE -eq 0 ]]; then
|
|
echo "❌ NVIDIA GPU not visible in guest via lspci"
|
|
exit 1
|
|
fi
|
|
echo " GPU visible in guest"
|
|
echo "✅ Smoke test passed (GPU passthrough) — instance $INSTANCE_ID launched with GPU and verified"
|
|
else
|
|
echo "✅ Smoke test passed — instance $INSTANCE_ID launched, running, and SSH-verified"
|
|
fi
|