mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-16 01:22:00 +00:00
43fa6eae59
* 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 * 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 * installer grub image fixes * made gpu-passthrough more user facing * vpcd fixes to set br-wan to promisc for public IP DHCP processes * fixed vpcd br-wan promisc and veth pair * ssh keygen fix * Fix: AMD GPU edge cases * script for running 8x instances on SMCI computer * fix: increase disk space on instances * feat: multi-GPU passthrough support for g7e.12xlarge (2× MI350X) - vm.VM: GPUPCIAddress string → GPUPCIAddresses []string for multi-GPU - instancetypes: add g7e.12xlarge (2 GPUs), GPUCountForType(), AMDmi350x model - instancetypes/generate: use GPUCountForType for GpuInfo.Count and TotalGpuMemoryInMiB - gpu/manager: Release() loops all pool entries for instanceID (not just first) - vm/lifecycle: loop over GPUPCIAddresses, emit gpu0/gpu1/... QEMU devices - daemon/handlers: claim N GPUs in loop for both RunInstances and StartStoppedInstance - daemon/vm_adapters: ReclaimByAddress loops GPUPCIAddresses; ReleaseGPU checks len > 0 - scripts/launch-gpu-fleet: fixed fleet — 6× g7e.4xlarge + 1× g7e.12xlarge * feat: per-instance-type disk size in GPU fleet (300 GB single, 600 GB dual) * fix: multi-gpu instance types * fix: kill QEMU synchronously when QMP handshake fails to free VFIO device When startQEMU succeeds but newQMPClientWithHandshake times out, the instance has not yet been inserted into the manager map so vmMgr.Delete in the rollback path is a no-op. QEMU keeps running and holds the VFIO group open. The GPU pool entry gets freed, the next Claim returns the same PCI address, and every subsequent QEMU fails with 'device or resource busy'. Fix: SIGKILL QEMU and wait for it to exit before returning the QMP error, ensuring the VFIO device is released before the caller recycles the GPU pool slot. * fix: quarantine GPU after QEMU launch failure to prevent re-assignment cascade When QEMU crashes during startup (e.g. pci_irq_handler assertion on a specific GPU), the pool Release frees that address and the next NATS request immediately re-claims the same broken device. Add MarkFailed() to gpu.Manager which sets Available=false for a PCI address after a launch failure, preventing re-assignment until the daemon restarts. Call MarkFailed for each GPU address in both RunInstances and StartStoppedInstance rollback paths after the Release succeeds. * fixed instance fleet launch script * remove known hosts and stagger provisioning * improvements to launch script * more upgrades * single instance gpu instance provisioning * fixed single instance provision * smci computer demo dashboard * fixes to launch script * updated smci dashboard_server * fixed demo dashboard * updated dashboard.py * feat: GPU Passthrough fixes to passthrough process - builds proper AMD or NVIDA ready images, generalised launch script * feat: gpu-passthrough updating image builder to include gpu-ready ubuntu images * fixing image builder process * fixed image builder to remove /tmp/ file before import to free space * fix: Updated COMMANDS.md to reflect new GPU management admin commands * Update docs, update internal launch script to work with new SGs, test tweaks * Updated Docs, fixed internal launch-gpu.sh script, test tweaks * fxing CI race due to GPU load
280 lines
9.3 KiB
Bash
Executable File
280 lines
9.3 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
|
|
|
|
# AWS commands run as the invoking user so their ~/.aws/config (spinifex profile
|
|
# + CA bundle) is used when the script is called via sudo.
|
|
INVOKING_USER="${SUDO_USER:-$(id -un)}"
|
|
INVOKING_HOME=$(getent passwd "$INVOKING_USER" | cut -d: -f6)
|
|
aws_as_user() { sudo -u "$INVOKING_USER" env HOME="$INVOKING_HOME" AWS_PROFILE=spinifex aws "$@"; }
|
|
|
|
# --- 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_as_user 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_as_user ec2 import-key-pair --key-name spinifex-key \
|
|
--public-key-material "fileb://$SSH_KEY.pub" || true
|
|
aws_as_user 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_as_user 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_as_user 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_as_user 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_as_user 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_as_user 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_as_user 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_as_user 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 -iE "nvidia|amdgpu|instinct"' 2>/dev/null; then
|
|
GPU_VISIBLE=1
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
if [[ $GPU_VISIBLE -eq 0 ]]; then
|
|
echo "❌ 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
|