From 36425a48eb302687e38092d0ff2799cf13746a40 Mon Sep 17 00:00:00 2001 From: Ben Duncan <5178243+benduncan@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:38:10 +1000 Subject: [PATCH] refactor: rewrite reset-dev-env.sh against production layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reset now targets /etc/spinifex, /var/lib/spinifex, /var/log/spinifex, /run/spinifex (the privilege-separated layout installed by setup.sh) and drives re-install from a source build via dev-install.sh instead of the legacy in-tree $HOME/spinifex + start-dev.sh flow. Capture region, AZ, external_mode, pool range, gateway, and prefix from the existing spinifex.toml before the wipe so the reset restores the current topology rather than applying defaults. Fresh boxes fall back to ap-southeast-2 and a pool derived from the WAN subnet. Drop the macvlan branch entirely — bridge-only from here on. Operators on single-NIC hosts must run reset from the console (documented in the script header). dev-install.sh gains DEV_INSTALL_SKIP_INIT so reset can own the post- setup.sh steps (setup-ovn, spx admin init with preserved args, CA trust, service start, LB image import). Also honors SPINIFEX_REGION / SPINIFEX_AZ for non-reset callers, and scopes systemctl reset-failed to spinifex-* so unrelated failed units aren't silently cleared. Closes mulga-983. --- scripts/dev-install.sh | 23 +- scripts/reset-dev-env.sh | 438 +++++++++++++++++++++------------------ 2 files changed, 251 insertions(+), 210 deletions(-) diff --git a/scripts/dev-install.sh b/scripts/dev-install.sh index 8cc6cbda6..6d9c7f89e 100755 --- a/scripts/dev-install.sh +++ b/scripts/dev-install.sh @@ -4,6 +4,14 @@ # (user creation, directories, systemd units), initializes the cluster, # and starts services via systemd. # +# Environment variables: +# SPINIFEX_REGION Region for spx admin init (default: ap-southeast-2) +# SPINIFEX_AZ AZ for spx admin init (default: ${SPINIFEX_REGION}a) +# DEV_INSTALL_SKIP_INIT Internal flag used by reset-dev-env.sh. When 1, +# stops after setup.sh and skips setup-ovn, init, +# CA trust, service start, and LB image import — +# caller owns those steps. Do not set manually. +# # Usage: ./scripts/dev-install.sh set -euo pipefail @@ -31,7 +39,7 @@ tar czf /tmp/spinifex-local.tar.gz -C "$STAGING" . echo "=== Cleaning stale state from previous installs ===" # Stop any running services before modifying files sudo systemctl stop spinifex.target 2>/dev/null || true -sudo systemctl reset-failed 2>/dev/null || true +sudo systemctl reset-failed 'spinifex-*' 2>/dev/null || true # Remove stale files owned by the dev user in production paths. # Previous dev-mode installs (admin init without sudo, start-dev.sh) leave @@ -54,11 +62,20 @@ echo "=== Running setup.sh (creates users, dirs, systemd units) ===" sudo INSTALL_SPINIFEX_TARBALL=/tmp/spinifex-local.tar.gz INSTALL_SPINIFEX_SKIP_NEWGRP=1 bash "$PROJECT_ROOT/scripts/setup.sh" rm -f /tmp/spinifex-local.tar.gz +if [ "${DEV_INSTALL_SKIP_INIT:-0}" = "1" ]; then + echo "=== DEV_INSTALL_SKIP_INIT=1 — stopping after setup.sh ===" + echo "Caller owns: setup-ovn, spx admin init, CA trust, service start, LB image." + exit 0 +fi + echo "=== Setting up OVN ===" sudo /usr/local/share/spinifex/setup-ovn.sh --management -echo "=== Initializing ===" -sudo spx admin init --force --region ap-southeast-2 --az ap-southeast-2a --node node1 --nodes 1 +SPINIFEX_REGION="${SPINIFEX_REGION:-ap-southeast-2}" +SPINIFEX_AZ="${SPINIFEX_AZ:-${SPINIFEX_REGION}a}" + +echo "=== Initializing (region=$SPINIFEX_REGION az=$SPINIFEX_AZ) ===" +sudo spx admin init --force --region "$SPINIFEX_REGION" --az "$SPINIFEX_AZ" --node node1 --nodes 1 echo "=== Installing CA certificate ===" sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt diff --git a/scripts/reset-dev-env.sh b/scripts/reset-dev-env.sh index a09f8373c..b8cc76d5e 100755 --- a/scripts/reset-dev-env.sh +++ b/scripts/reset-dev-env.sh @@ -1,15 +1,45 @@ #!/bin/bash - -# Reset dev environment (will purge all data) -# This script is for single-node dev environments only. +# reset-dev-env.sh — Single-node dev environment reset. +# +# Stops services, wipes production state (/etc/spinifex, /var/lib/spinifex, +# /var/log/spinifex, /run/spinifex), rebuilds from source, reinstalls via +# dev-install.sh, and relaunches a smoke-test instance. +# +# Preserves node/network settings from the existing /etc/spinifex/spinifex.toml +# so the reset restores the current topology (region, AZ, external mode, +# pool range) rather than applying defaults. On a fresh box with no config, +# falls back to ap-southeast-2 + pool mode derived from the WAN subnet. +# +# WARNING: setup-ovn.sh converts the WAN NIC into an OVS bridge. On hosts +# where the WAN NIC is also the SSH NIC, SSH will drop mid-run. Run from +# the console or via a separate management NIC. +# +# Single-node only. Refuses on multi-node clusters. set -euo pipefail -CONFIG_FILE="$HOME/spinifex/config/spinifex.toml" +# --- Paths (production layout from setup.sh) --- +ETC_DIR=/etc/spinifex +DATA_DIR=/var/lib/spinifex +LOG_DIR=/var/log/spinifex +RUN_DIR=/run/spinifex +CONFIG_FILE="$ETC_DIR/spinifex.toml" + +# --- Script context --- +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Resolve invoking user's HOME. $HOME is unsafe under sudo (resolves to /root). +INVOKING_USER="${SUDO_USER:-$(id -un)}" +INVOKING_HOME=$(getent passwd "$INVOKING_USER" | cut -d: -f6) +if [ -z "$INVOKING_HOME" ]; then + echo "❌ Could not resolve home directory for user: $INVOKING_USER" + exit 1 +fi # --- Guard: refuse to run on multi-node clusters --- -if [ -f "$CONFIG_FILE" ]; then - NODE_COUNT=$(grep -cE '^\[nodes\.[^.]+\]' "$CONFIG_FILE") +if sudo test -f "$CONFIG_FILE"; then + NODE_COUNT=$(sudo grep -cE '^\[nodes\.[^.]+\]' "$CONFIG_FILE" 2>/dev/null || echo 0) if [ "$NODE_COUNT" -gt 1 ]; then echo "❌ Multi-node cluster detected ($NODE_COUNT nodes in $CONFIG_FILE)." echo " This script only supports single-node dev environments." @@ -18,266 +48,260 @@ if [ -f "$CONFIG_FILE" ]; then fi fi -# Save region from existing config before we delete everything -REGION="ap-southeast-2" -if [ -f "$CONFIG_FILE" ]; then - SAVED_REGION=$(grep -E '^\s*region\s*=' "$CONFIG_FILE" | tail -1 | sed 's/.*=\s*"\(.*\)"/\1/') - if [ -n "$SAVED_REGION" ]; then - REGION="$SAVED_REGION" - fi +# --- Capture settings from existing config --- +# Parse before the wipe. Missing fields fall back to defaults below. +REGION="" +AZ="" +EXT_MODE="" +POOL_START="" +POOL_END="" +EXT_GATEWAY="" +EXT_PREFIX="" + +if sudo test -f "$CONFIG_FILE"; then + # Copy config to a temp file we can read without sudo for cleaner parsing. + TMP_CFG=$(mktemp) + trap 'rm -f "$TMP_CFG"' EXIT + sudo cat "$CONFIG_FILE" > "$TMP_CFG" + + # toml_scalar — grabs first matching scalar from the + # first matching section. Strips surrounding quotes if present. + toml_scalar() { + local section_re="$1" + local key="$2" + awk -v sec="$section_re" -v key="$key" ' + $0 ~ sec { in_sec=1; next } + /^\[/ { in_sec=0 } + in_sec && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { + sub(/^[^=]*=[[:space:]]*/, "") + gsub(/[[:space:]]*#.*$/, "") + gsub(/^"/, ""); gsub(/"$/, "") + print; exit + }' "$TMP_CFG" + } + + REGION=$(toml_scalar '^\[nodes\.' 'region') + AZ=$(toml_scalar '^\[nodes\.' 'az') + EXT_MODE=$(toml_scalar '^\[network\]' 'external_mode') + POOL_START=$(toml_scalar '^\[\[network\.external_pools\]\]' 'range_start') + POOL_END=$(toml_scalar '^\[\[network\.external_pools\]\]' 'range_end') + EXT_GATEWAY=$(toml_scalar '^\[\[network\.external_pools\]\]' 'gateway') + EXT_PREFIX=$(toml_scalar '^\[\[network\.external_pools\]\]' 'prefix_len') +fi + +# Defaults +REGION="${REGION:-ap-southeast-2}" +AZ="${AZ:-${REGION}a}" +EXT_MODE="${EXT_MODE:-pool}" + +echo "Preserving: region=$REGION az=$AZ external_mode=$EXT_MODE" +if [ "$EXT_MODE" = "pool" ] && [ -n "$POOL_START" ]; then + echo " pool: $POOL_START - $POOL_END gw=$EXT_GATEWAY prefix=$EXT_PREFIX" fi -echo "Using region: $REGION" # --- Shutdown services --- -echo "Shutting down services" +echo "==> Stopping services" +sudo systemctl stop spinifex.target 2>/dev/null || true +sudo systemctl reset-failed 'spinifex-*' 2>/dev/null || true +sudo pkill -x qemu-system-x86_64 2>/dev/null || true +sudo pkill -x qemu-system-aarch64 2>/dev/null || true -if ! ./scripts/stop-dev.sh; then - echo "❌ Failed to stop services. Aborting reset to prevent data loss." - exit 1 -fi - -# Verify no spinifex-related processes are still running -PROCESS_PATTERN='(bin/spx|spinifex-ui|nats-server|predastore|viperblock|vpcd)' -remaining=$(pgrep -af "$PROCESS_PATTERN" | grep -v "reset-dev-env.sh" || true) -if [ -n "$remaining" ]; then - echo "Waiting for remaining processes to exit..." - timeout=30 - elapsed=0 - while pgrep -af "$PROCESS_PATTERN" | grep -qv "reset-dev-env.sh" 2>/dev/null; do - if [ $elapsed -ge $timeout ]; then - echo "❌ Processes still running after ${timeout}s:" - pgrep -af "$PROCESS_PATTERN" | grep -v "reset-dev-env.sh" || true - echo " Kill them manually and re-run this script." - exit 1 - fi - sleep 1 - elapsed=$((elapsed + 1)) - done -fi - -# Verify no QEMU VMs are running -if pgrep -x qemu-system-x86_64 > /dev/null 2>&1; then - echo "❌ QEMU instances still running. Cannot reset while VMs are active." - echo " Run './scripts/stop-dev.sh' or kill them manually." - exit 1 -fi - -echo "All services confirmed stopped" - -# --- Clean OVS/OVN --- -echo "Removing OVS bridges and config" +# Wait for QEMU to fully exit before tearing down viperblock state. +timeout=30 +elapsed=0 +while pgrep -x 'qemu-system-x86_64|qemu-system-aarch64' > /dev/null 2>&1; do + if [ "$elapsed" -ge "$timeout" ]; then + echo "❌ QEMU still running after ${timeout}s:" + pgrep -af 'qemu-system-' || true + echo " Kill them manually and re-run this script." + exit 1 + fi + sleep 1 + elapsed=$((elapsed + 1)) +done +# --- Clean OVS / OVN --- +echo "==> Cleaning OVS bridges" if command -v ovs-vsctl >/dev/null 2>&1; then - # Ensure OVS is running so we can clean up sudo systemctl start openvswitch-switch 2>/dev/null || true sleep 1 - - # Delete all OVS bridges (br-int, br-external, etc.) for br in $(sudo ovs-vsctl list-br 2>/dev/null); do echo " Deleting bridge: $br" sudo ovs-vsctl --if-exists del-br "$br" done - - # Clear OVN external_ids sudo ovs-vsctl --if-exists clear Open_vSwitch . external_ids 2>/dev/null || true - echo " Cleared OVS external_ids" - - # Stop OVS again after cleanup sudo systemctl stop openvswitch-switch 2>/dev/null || true fi -# Clean OVN databases (both Northbound and Southbound) -# Delete the DB files outright — setup-ovn.sh will restart ovn-central with fresh -# empty databases. This eliminates stale SB state (chassis entries, port bindings, -# datapath bindings) that accumulates across resets and causes ovn-controller to -# enter a commit failure loop ("OVNSB commit failed, force recompute"). -echo "Removing OVN database files" +# Delete OVN DB files outright — setup-ovn.sh will restart ovn-central with +# fresh empty DBs. Eliminates stale SB state (chassis entries, port bindings) +# that accumulates across resets and triggers ovn-controller commit loops. +echo "==> Removing OVN database files" sudo systemctl stop ovn-central 2>/dev/null || true sudo systemctl stop ovn-controller 2>/dev/null || true if [ -d /var/lib/ovn ]; then sudo rm -f /var/lib/ovn/ovnnb_db.db /var/lib/ovn/ovnsb_db.db - echo " Deleted /var/lib/ovn/ovn{nb,sb}_db.db" -else - echo " /var/lib/ovn not found, skipping OVN DB cleanup" fi -# Remove veth pair created by setup-ovn.sh (veth mode — Linux bridge ↔ OVS bridge) +# veth pair created by setup-ovn.sh (Linux bridge ↔ OVS bridge) if ip link show veth-wan-br >/dev/null 2>&1; then echo " Deleting veth pair: veth-wan-br ↔ veth-wan-ovs" sudo ip link del veth-wan-br 2>/dev/null || true fi -# Remove macvlan interfaces created by setup-ovn.sh -for iface in $(ip -o link show type macvlan 2>/dev/null | awk -F': ' '{print $2}' | grep '^spx-ext-'); do - echo " Deleting macvlan: $iface" - sudo ip link del "$iface" 2>/dev/null || true -done +# --- Wipe production state --- +echo "==> Wiping $ETC_DIR $DATA_DIR $LOG_DIR $RUN_DIR" +sudo rm -rf "$ETC_DIR" "$DATA_DIR" "$LOG_DIR" "$RUN_DIR" -# --- Wipe data --- -echo "Removing ~/spinifex" -rm -rf ~/spinifex - -# --- Re-initialize --- -# Detect WAN interface (default route). -WAN_IFACE=$(ip -4 route show default | awk '{print $5}' | head -1) -WAN_GW=$(ip -4 route show default | awk '{print $3}' | head -1) - -echo "Detected WAN interface: ${WAN_IFACE:-none}, gateway: ${WAN_GW:-none}" - -# Check if WAN interface is already a bridge (tofu-cluster / production setup). -# If so, setup-ovn.sh auto-detects it. If it's a physical NIC, we need to -# decide between direct bridge and macvlan based on SSH safety. -WAN_IS_BRIDGE=false -if [ -n "$WAN_IFACE" ]; then - if ip -d link show "$WAN_IFACE" 2>/dev/null | grep -q "bridge"; then - WAN_IS_BRIDGE=true - fi -fi - -# Build setup-ovn.sh flags based on detected topology -SETUP_OVN_FLAGS="" -if [ "$WAN_IS_BRIDGE" = true ]; then - # WAN is already a bridge (e.g. br-wan from cloud-init) — setup-ovn.sh - # auto-detects it, no explicit flags needed. - echo " WAN is a bridge: $WAN_IFACE (auto-detected)" -elif [ -n "$WAN_IFACE" ]; then - # WAN is a physical NIC. Detect SSH NIC to decide bridge strategy. - SSH_NIC="" - if [ -n "${SSH_CONNECTION:-}" ]; then - SSH_IP=$(echo "$SSH_CONNECTION" | awk '{print $3}') - SSH_NIC=$(ip -o -4 addr show | awk -v ip="$SSH_IP" '$0 ~ ip"/" {print $2}' | head -1) - fi - if [ -z "$SSH_NIC" ]; then - SSH_NIC="$WAN_IFACE" - fi - - if [ "$WAN_IFACE" != "$SSH_NIC" ]; then - SETUP_OVN_FLAGS="--wan-bridge=br-wan --wan-iface=$WAN_IFACE" - echo " Bridge mode: direct (WAN=$WAN_IFACE != SSH=$SSH_NIC)" - else - SETUP_OVN_FLAGS="--macvlan --wan-iface=$WAN_IFACE" - echo " Bridge mode: macvlan (WAN=$WAN_IFACE == SSH=$SSH_NIC)" - fi -fi - -# Chassis ID — let setup-ovn.sh auto-detect from hostname. This ensures the -# system-id matches what ovn-controller registers in the SBDB, which is what -# vpcd discovers at startup for gateway scheduling. - -echo "Re-initializing OVN" -./scripts/setup-ovn.sh --management $SETUP_OVN_FLAGS - -echo "Initializing platform" -ADMIN_INIT_ARGS="--region $REGION --az ${REGION}a --node node1 --nodes 1" - -# External networking mode: set EXTERNAL_MODE=nat for NAT/DHCP mode (outbound-only), -# or leave unset / EXTERNAL_MODE=pool for pool mode (per-VM public IPs). -EXTERNAL_MODE="${EXTERNAL_MODE:-pool}" - -if [ -n "$WAN_IFACE" ] && [ -n "$WAN_GW" ]; then - WAN_IP=$(ip -4 -o addr show "$WAN_IFACE" 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1) - WAN_PREFIX=$(ip -4 -o addr show "$WAN_IFACE" 2>/dev/null | awk '{print $4}' | cut -d/ -f2 | head -1) - if [ -z "$WAN_PREFIX" ]; then WAN_PREFIX=24; fi - - if [ "$EXTERNAL_MODE" = "nat" ]; then - # NAT mode: outbound-only via shared SNAT. Use a single IP from the - # high end of the WAN subnet as the gateway IP. No per-VM public IPs. - IFS='.' read -r o1 o2 o3 o4 <<< "$WAN_GW" - GATEWAY_IP="${GATEWAY_IP:-${o1}.${o2}.${o3}.200}" - echo " External mode: nat (gateway IP: $GATEWAY_IP, WAN gateway: $WAN_GW)" - ADMIN_INIT_ARGS="$ADMIN_INIT_ARGS --external-mode=nat --gateway-ip=${GATEWAY_IP} --external-gateway=${WAN_GW} --external-prefix-len=${WAN_PREFIX}" - else - # Pool mode: per-VM public IPs from a static range - IFS='.' read -r o1 o2 o3 o4 <<< "$WAN_GW" - POOL_START="${o1}.${o2}.${o3}.200" - POOL_END="${o1}.${o2}.${o3}.250" - echo " External pool: $POOL_START - $POOL_END (gateway: $WAN_GW)" - ADMIN_INIT_ARGS="$ADMIN_INIT_ARGS --external-mode=pool --external-pool=${POOL_START}-${POOL_END} --external-gateway=${WAN_GW} --external-prefix-len=${WAN_PREFIX}" - fi -fi - -# Re-initialize with --force to regenerate credentials, certs, config, and -# update ~/.aws/credentials. Must carry the same args so external networking -# config isn't lost. -echo "Re-initializing platform..." -./bin/spx admin init --force $ADMIN_INIT_ARGS - -# Generate SSH key if it doesn't exist -if [ ! -f ~/.ssh/spinifex-key.pub ]; then - echo "Generating SSH key pair..." - ssh-keygen -t ed25519 -f ~/.ssh/spinifex-key -N "" -fi - -# Trust the Spinifex CA certificate so TLS connections to predastore/awsgw work -if [ -f "$HOME/spinifex/config/ca.pem" ]; then - echo "Adding Spinifex CA certificate to system trust store..." - sudo cp "$HOME/spinifex/config/ca.pem" /usr/local/share/ca-certificates/spinifex-ca.crt +# Drop the old CA cert from the system trust store. The new init writes a +# fresh CA which we re-install below. +if [ -f /usr/local/share/ca-certificates/spinifex-ca.crt ]; then + echo "==> Removing stale CA from trust store" + sudo rm -f /usr/local/share/ca-certificates/spinifex-ca.crt sudo update-ca-certificates fi -# Enable pprof for development -#PPROF_ENABLED=1 PPROF_OUTPUT=/tmp/spinifex-vm.prof ./scripts/start-dev.sh --build -./scripts/start-dev.sh --build +# --- Rebuild + setup.sh scaffolding (no init / start) --- +echo "==> Rebuilding and reinstalling (dev-install.sh, setup-only mode)" +DEV_INSTALL_SKIP_INIT=1 "$SCRIPT_DIR/dev-install.sh" + +# --- Detect WAN topology (bridge only — no macvlan) --- +WAN_IFACE=$(ip -4 route show default | awk '{print $5}' | head -1) +WAN_GW=$(ip -4 route show default | awk '{print $3}' | head -1) +WAN_PREFIX_HOST="" +if [ -n "$WAN_IFACE" ]; then + WAN_PREFIX_HOST=$(ip -4 -o addr show "$WAN_IFACE" 2>/dev/null | awk '{print $4}' | cut -d/ -f2 | head -1) +fi +WAN_PREFIX_HOST="${WAN_PREFIX_HOST:-24}" + +SETUP_OVN_FLAGS="" +if [ -n "$WAN_IFACE" ]; then + if ip -d link show "$WAN_IFACE" 2>/dev/null | grep -q "bridge"; then + echo "==> WAN is a bridge: $WAN_IFACE (setup-ovn.sh auto-detects)" + else + SETUP_OVN_FLAGS="--wan-bridge=br-wan --wan-iface=$WAN_IFACE" + echo "==> WAN is physical: $WAN_IFACE (direct bridge br-wan)" + fi +fi + +echo "==> Running setup-ovn.sh" +# shellcheck disable=SC2086 # intentional word-splitting for flag list +sudo /usr/local/share/spinifex/setup-ovn.sh --management $SETUP_OVN_FLAGS + +# --- Assemble external-networking args for spx admin init --- +EXTERNAL_INIT_ARGS="" +case "$EXT_MODE" in + nat) + # NAT gateway IP isn't captured from config (not stored in a + # predictable location); derive from WAN subnet — same rule as + # a fresh install. Operators who hand-edited the gateway IP will + # need to re-apply it after reset. + if [ -n "$WAN_GW" ]; then + IFS='.' read -r o1 o2 o3 _ <<< "$WAN_GW" + GATEWAY_IP="${o1}.${o2}.${o3}.200" + EXTERNAL_INIT_ARGS="--external-mode=nat --gateway-ip=$GATEWAY_IP --external-gateway=$WAN_GW --external-prefix-len=${EXT_PREFIX:-$WAN_PREFIX_HOST}" + echo "==> External mode: nat (gateway IP $GATEWAY_IP)" + fi + ;; + pool|*) + if [ -n "$POOL_START" ] && [ -n "$POOL_END" ]; then + EXTERNAL_INIT_ARGS="--external-mode=pool --external-pool=${POOL_START}-${POOL_END} --external-gateway=${EXT_GATEWAY:-$WAN_GW} --external-prefix-len=${EXT_PREFIX:-$WAN_PREFIX_HOST}" + echo "==> External mode: pool (preserved $POOL_START-$POOL_END)" + elif [ -n "$WAN_GW" ]; then + # Fresh box: derive pool from WAN subnet. + IFS='.' read -r o1 o2 o3 _ <<< "$WAN_GW" + POOL_START="${o1}.${o2}.${o3}.200" + POOL_END="${o1}.${o2}.${o3}.250" + EXTERNAL_INIT_ARGS="--external-mode=pool --external-pool=${POOL_START}-${POOL_END} --external-gateway=$WAN_GW --external-prefix-len=$WAN_PREFIX_HOST" + echo "==> External mode: pool (default $POOL_START-$POOL_END)" + fi + ;; +esac + +# --- Initialize platform --- +echo "==> Initializing platform (region=$REGION az=$AZ)" +# shellcheck disable=SC2086 # intentional word-splitting for arg list +sudo /usr/local/bin/spx admin init --force \ + --region "$REGION" --az "$AZ" --node node1 --nodes 1 \ + $EXTERNAL_INIT_ARGS + +# --- Install CA cert into system trust store --- +echo "==> Installing CA certificate" +sudo cp "$ETC_DIR/ca.pem" /usr/local/share/ca-certificates/spinifex-ca.crt +sudo update-ca-certificates + +# --- Start services --- +echo "==> Starting spinifex.target" +sudo systemctl start spinifex.target + +# --- Build + import LB image (needs services running) --- +echo "==> Building and importing LB image" +cd "$PROJECT_ROOT" && make build-lb-agent +"$PROJECT_ROOT/scripts/build-system-image.sh" "$PROJECT_ROOT/scripts/images/lb.conf" --import --quiet + +# --- SSH key (generated as invoking user, not root) --- +SSH_KEY="$INVOKING_HOME/.ssh/spinifex-key" +if [ ! -f "$SSH_KEY.pub" ]; then + echo "==> Generating SSH key pair ($SSH_KEY)" + sudo -u "$INVOKING_USER" mkdir -p "$INVOKING_HOME/.ssh" + sudo -u "$INVOKING_USER" ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" +fi export AWS_PROFILE=spinifex -# Import SSH key -echo "Importing SSH key" -aws ec2 import-key-pair --key-name "spinifex-key" --public-key-material fileb://~/.ssh/spinifex-key.pub +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 -# Import AMI -echo "Importing AMI" - -LOCAL_IMAGE="$HOME/images/ubuntu-24.04.img" -if [ -f "$LOCAL_IMAGE" ]; then - echo "Using local image: $LOCAL_IMAGE" - ARCH=$(uname -m) - if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then - IMG_ARCH="arm64" - else +# --- Import AMI --- +echo "==> Importing AMI" +LOCAL_IMAGE="$INVOKING_HOME/images/ubuntu-24.04.img" +ARCH=$(uname -m) +case "$ARCH" in + x86_64) IMG_ARCH="x86_64" - fi - ./bin/spx admin images import --file "$LOCAL_IMAGE" --distro ubuntu --version 24.04 --arch "$IMG_ARCH" -else - # Download by name based on architecture - ARCH=$(uname -m) - if [ "$ARCH" = "x86_64" ]; then IMAGE_NAME="ubuntu-24.04-x86_64" - elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + ;; + aarch64|arm64) + IMG_ARCH="arm64" IMAGE_NAME="ubuntu-24.04-arm64" - else - echo "Warning: Unknown architecture $ARCH, defaulting to x86_64" + ;; + *) + echo " Warning: unknown arch $ARCH, defaulting to x86_64" + IMG_ARCH="x86_64" IMAGE_NAME="ubuntu-24.04-x86_64" - fi - echo "Downloading image: $IMAGE_NAME" - ./bin/spx admin images import --name "$IMAGE_NAME" + ;; +esac + +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 24.04 --arch "$IMG_ARCH" +else + echo " Downloading image: $IMAGE_NAME" + sudo /usr/local/bin/spx admin images import --name "$IMAGE_NAME" fi aws ec2 describe-images -# --- Launch a smoke-test instance --- -echo "Launching smoke-test instance..." - -# Pick instance type based on CPU vendor +# --- Smoke-test instance --- +echo "==> Launching smoke-test instance" if grep -q 'AuthenticAMD' /proc/cpuinfo; then INSTANCE_TYPE="t3a.small" else INSTANCE_TYPE="t3.small" fi -# Get the AMI we just imported AMI_ID=$(aws ec2 describe-images --query "Images[0].ImageId" --output text) if [ -z "$AMI_ID" ] || [ "$AMI_ID" = "None" ]; then echo "❌ No AMI found, skipping instance launch" exit 0 fi -# Find a public subnet (first available) SUBNET_ID=$(aws ec2 describe-subnets \ --filters "Name=map-public-ip-on-launch,Values=true" \ --query "Subnets[0].SubnetId" --output text 2>/dev/null) -# Fallback: just grab the first subnet if no public-tagged one exists if [ -z "$SUBNET_ID" ] || [ "$SUBNET_ID" = "None" ]; then SUBNET_ID=$(aws ec2 describe-subnets --query "Subnets[0].SubnetId" --output text) fi