mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +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>
436 lines
14 KiB
Bash
Executable File
436 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# build-system-image.sh — Build a minimal system AMI from a manifest
|
|
#
|
|
# Supports Alpine Linux and Ubuntu 24.04 as base distros.
|
|
# Creates a pre-baked image with custom packages, binaries, and setup scripts
|
|
# installed, ready for import as a Spinifex AMI.
|
|
#
|
|
# Requirements: qemu-nbd, qemu-img, sudo (for mount/chroot), curl
|
|
# Usage: ./scripts/build-system-image.sh <manifest.conf> [--import]
|
|
# --import Also import the image as an AMI via spx admin images import
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
usage() {
|
|
echo "Usage: $0 <manifest.conf> [--import]"
|
|
echo ""
|
|
echo " manifest.conf Path to image manifest (see scripts/images/ for examples)"
|
|
echo " --import Import the built image as an AMI"
|
|
exit 1
|
|
}
|
|
|
|
if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
|
|
usage
|
|
fi
|
|
|
|
MANIFEST="$1"
|
|
shift
|
|
|
|
if [[ ! -f "$MANIFEST" ]]; then
|
|
echo "ERROR: Manifest not found: $MANIFEST"
|
|
exit 1
|
|
fi
|
|
|
|
DO_IMPORT=false
|
|
QUIET=false
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--import) DO_IMPORT=true ;;
|
|
--quiet) QUIET=true ;;
|
|
esac
|
|
done
|
|
|
|
# Source the manifest
|
|
# shellcheck source=/dev/null
|
|
source "$MANIFEST"
|
|
|
|
# Default distro is alpine for backward compatibility
|
|
DISTRO="${DISTRO:-alpine}"
|
|
|
|
# Validate required manifest fields
|
|
for field in IMAGE_NAME IMAGE_SIZE; do
|
|
if [[ -z "${!field:-}" ]]; then
|
|
echo "ERROR: Manifest missing required field: $field"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Derived paths
|
|
BUILD_DIR="/tmp/${IMAGE_NAME}-image-build"
|
|
NBD_DEV="/dev/nbd0"
|
|
MOUNT_DIR="${BUILD_DIR}/mnt"
|
|
|
|
case "$DISTRO" in
|
|
alpine)
|
|
if [[ -z "${ALPINE_VERSION:-}" ]]; then
|
|
echo "ERROR: Manifest missing required field: ALPINE_VERSION"
|
|
exit 1
|
|
fi
|
|
SOURCE_IMAGE="generic_alpine-${ALPINE_VERSION}-x86_64-bios-cloudinit-r0.qcow2"
|
|
SOURCE_URL="https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION%.*}/releases/cloud/${SOURCE_IMAGE}"
|
|
OUTPUT_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-alpine.qcow2"
|
|
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-alpine.raw"
|
|
DISTRO_VERSION="${ALPINE_VERSION}"
|
|
ROOT_PART="${NBD_DEV}"
|
|
;;
|
|
ubuntu)
|
|
if [[ -z "${UBUNTU_VERSION:-}" ]]; then
|
|
echo "ERROR: Manifest missing required field: UBUNTU_VERSION"
|
|
exit 1
|
|
fi
|
|
SOURCE_IMAGE="ubuntu-${UBUNTU_VERSION}-minimal-cloudimg-amd64.img"
|
|
SOURCE_URL="https://cloud-images.ubuntu.com/minimal/releases/noble/release/${SOURCE_IMAGE}"
|
|
OUTPUT_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-ubuntu.qcow2"
|
|
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-ubuntu.raw"
|
|
DISTRO_VERSION="${UBUNTU_VERSION}"
|
|
ROOT_PART="${NBD_DEV}p1"
|
|
;;
|
|
*)
|
|
echo "ERROR: Unknown DISTRO: $DISTRO (supported: alpine, ubuntu)"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
cleanup() {
|
|
echo "Cleaning up..."
|
|
# Ubuntu chroot has bind mounts that must be released first
|
|
sudo umount "${MOUNT_DIR}/dev/pts" 2>/dev/null || true
|
|
sudo umount "${MOUNT_DIR}/dev" 2>/dev/null || true
|
|
sudo umount "${MOUNT_DIR}/sys" 2>/dev/null || true
|
|
sudo umount "${MOUNT_DIR}/proc" 2>/dev/null || true
|
|
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
|
|
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
|
|
exec 9>&- 2>/dev/null || true # release nbd lock if held
|
|
echo "Done."
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# In quiet mode, redirect build output to /dev/null (import output still shown)
|
|
if [[ "$QUIET" == true ]]; then
|
|
exec 3>&1 # save original stdout
|
|
exec 1>/dev/null # suppress build output
|
|
fi
|
|
|
|
echo "=== System Image Builder ==="
|
|
echo "Image: ${IMAGE_NAME} — ${IMAGE_DESCRIPTION:-}"
|
|
echo "Distro: ${DISTRO} ${DISTRO_VERSION}"
|
|
echo "Size: ${IMAGE_SIZE}"
|
|
echo "Build: ${BUILD_DIR}"
|
|
echo ""
|
|
|
|
# Step 0: Check prerequisites
|
|
if ! command -v qemu-nbd &>/dev/null; then
|
|
echo "ERROR: qemu-nbd not found. Install qemu-utils."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v qemu-img &>/dev/null; then
|
|
echo "ERROR: qemu-img not found. Install qemu-utils."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$DISTRO" == "ubuntu" ]] && ! command -v parted &>/dev/null; then
|
|
echo "ERROR: parted not found. Install parted."
|
|
exit 1
|
|
fi
|
|
|
|
# Build binaries if BUILD_COMMANDS is set
|
|
if [[ -n "${BUILD_COMMANDS:-}" ]]; then
|
|
echo "Building binaries: ${BUILD_COMMANDS}"
|
|
if ! (cd "$PROJECT_DIR" && eval "$BUILD_COMMANDS"); then
|
|
echo "ERROR: BUILD_COMMANDS failed: ${BUILD_COMMANDS}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Verify binaries exist; Alpine also requires static linking (uses musl)
|
|
if [[ -n "${INSTALL_BINARIES:-}" ]]; then
|
|
IFS=' ' read -ra BINARY_PAIRS <<< "$INSTALL_BINARIES"
|
|
for pair in "${BINARY_PAIRS[@]}"; do
|
|
src="${pair%%:*}"
|
|
src_path="${PROJECT_DIR}/${src}"
|
|
if [[ ! -f "$src_path" ]]; then
|
|
echo "ERROR: Binary not found: $src_path"
|
|
exit 1
|
|
fi
|
|
if [[ "$DISTRO" == "alpine" ]] && ! file "$src_path" | grep -q "statically linked"; then
|
|
echo "ERROR: $src_path is not statically linked (Alpine uses musl — dynamic glibc binaries will fail)"
|
|
echo " Rebuild with: CGO_ENABLED=0 go build ..."
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Serialize the entire image build with flock — concurrent builds on the same
|
|
# host (e.g. CI single-node + multi-node jobs) share /dev/nbd0 and BUILD_DIR.
|
|
NBD_LOCK="/tmp/build-system-image.lock"
|
|
echo "Acquiring build lock..."
|
|
exec 9>"$NBD_LOCK"
|
|
flock 9
|
|
echo "Lock acquired"
|
|
|
|
# If the raw image was built recently (< 10 min), skip the entire build.
|
|
# This avoids duplicate work when concurrent CI jobs build the same image.
|
|
if [[ -f "$OUTPUT_RAW" ]] && [[ $(( $(date +%s) - $(stat -c %Y "$OUTPUT_RAW") )) -lt 600 ]]; then
|
|
echo "=== Skipping build — $OUTPUT_RAW is fresh (< 10 min old) ==="
|
|
|
|
# Restore stdout if suppressed, then jump to import
|
|
if [[ "$QUIET" == true ]]; then
|
|
exec 1>&3 3>&-
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Build complete (cached) ==="
|
|
echo " raw: $OUTPUT_RAW ($(du -h "$OUTPUT_RAW" | cut -f1))"
|
|
|
|
if [[ "$DO_IMPORT" == true ]]; then
|
|
echo "Importing as AMI..."
|
|
IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64)
|
|
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
|
IMPORT_ARGS+=(--tag "$SYSTEM_TAG")
|
|
fi
|
|
(cd "$PROJECT_DIR" && ./bin/spx admin images import "${IMPORT_ARGS[@]}")
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p "$BUILD_DIR" "$MOUNT_DIR"
|
|
|
|
# Step 1: Download base cloud image
|
|
if [[ -f "${BUILD_DIR}/${SOURCE_IMAGE}" ]]; then
|
|
echo "Base image already downloaded."
|
|
else
|
|
echo "Downloading ${DISTRO} ${DISTRO_VERSION} cloud image..."
|
|
if ! curl --fail -L -o "${BUILD_DIR}/${SOURCE_IMAGE}" "$SOURCE_URL"; then
|
|
rm -f "${BUILD_DIR}/${SOURCE_IMAGE}"
|
|
echo "ERROR: Failed to download image from $SOURCE_URL"
|
|
exit 1
|
|
fi
|
|
# Verify the download is a valid qcow2/img
|
|
if ! qemu-img info "${BUILD_DIR}/${SOURCE_IMAGE}" &>/dev/null; then
|
|
rm -f "${BUILD_DIR}/${SOURCE_IMAGE}"
|
|
echo "ERROR: Downloaded file is not a valid disk image"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Step 2: Copy image for customization
|
|
rm -f "$OUTPUT_IMAGE"
|
|
echo "Copying image for customization..."
|
|
cp "${BUILD_DIR}/${SOURCE_IMAGE}" "$OUTPUT_IMAGE"
|
|
|
|
# Resize the image to provide room for packages
|
|
qemu-img resize "$OUTPUT_IMAGE" "$IMAGE_SIZE"
|
|
|
|
# Step 3: Connect via qemu-nbd
|
|
echo "Connecting image via qemu-nbd..."
|
|
sudo modprobe nbd max_part=4 2>/dev/null || true
|
|
if [[ ! -e "${NBD_DEV}" ]]; then
|
|
echo "ERROR: ${NBD_DEV} does not exist. Is the nbd kernel module loaded? Try: sudo modprobe nbd"
|
|
exit 1
|
|
fi
|
|
sudo qemu-nbd --disconnect "${NBD_DEV}" 2>/dev/null || true
|
|
sudo qemu-nbd --connect="${NBD_DEV}" "$OUTPUT_IMAGE"
|
|
|
|
# Wait for the nbd device to be ready
|
|
for i in $(seq 1 10); do
|
|
if sudo blockdev --getsize64 "${NBD_DEV}" &>/dev/null; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
sleep 1
|
|
|
|
# Ubuntu images have a partition table; wait for partition devices then resize
|
|
# the partition to fill the expanded image before resizing the filesystem.
|
|
if [[ "$DISTRO" == "ubuntu" ]]; then
|
|
for i in $(seq 1 10); do
|
|
if [[ -e "${ROOT_PART}" ]]; then break; fi
|
|
sleep 1
|
|
done
|
|
echo "Resizing partition..."
|
|
# After qemu-img resize the GPT backup header sits at the old end of disk.
|
|
# Move it to the new end before parted tries to resize, otherwise parted
|
|
# refuses with "Unable to satisfy all constraints on the partition."
|
|
sudo sgdisk --move-second-header "${NBD_DEV}" 2>/dev/null || true
|
|
sudo parted --script "${NBD_DEV}" resizepart 1 100%
|
|
fi
|
|
|
|
# Alpine cloud images have ext4 directly on the block device (no partition table).
|
|
echo "Checking filesystem..."
|
|
sudo e2fsck -f -y "${ROOT_PART}" || {
|
|
ec=$?
|
|
if [[ $ec -gt 1 ]]; then
|
|
echo "ERROR: e2fsck failed with exit code $ec on ${ROOT_PART}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
echo "Resizing filesystem..."
|
|
if ! sudo resize2fs "${ROOT_PART}"; then
|
|
echo "ERROR: resize2fs failed on ${ROOT_PART}"
|
|
exit 1
|
|
fi
|
|
|
|
# Step 4: Mount and customize
|
|
echo "Mounting root filesystem..."
|
|
sudo mount "${ROOT_PART}" "$MOUNT_DIR"
|
|
|
|
# Set up resolv.conf for DNS inside chroot.
|
|
# Ubuntu cloud images symlink /etc/resolv.conf → /run/systemd/resolve/stub-resolv.conf
|
|
# which doesn't exist yet. Remove the symlink and write a real file.
|
|
sudo rm -f "${MOUNT_DIR}/etc/resolv.conf"
|
|
sudo cp /etc/resolv.conf "${MOUNT_DIR}/etc/resolv.conf"
|
|
|
|
# Ubuntu chroot requires /proc /sys /dev bind mounts for systemd and DKMS
|
|
if [[ "$DISTRO" == "ubuntu" ]]; then
|
|
sudo mount --bind /proc "${MOUNT_DIR}/proc"
|
|
sudo mount --bind /sys "${MOUNT_DIR}/sys"
|
|
sudo mount --bind /dev "${MOUNT_DIR}/dev"
|
|
sudo mount --bind /dev/pts "${MOUNT_DIR}/dev/pts"
|
|
fi
|
|
|
|
# Install packages
|
|
if [[ "$DISTRO" == "alpine" ]] && [[ -n "${APK_PACKAGES:-}" ]]; then
|
|
echo "Installing packages: ${APK_PACKAGES}..."
|
|
sudo chroot "$MOUNT_DIR" /bin/sh -c "
|
|
set -e
|
|
# Enable community repo
|
|
sed -i 's|^#\(.*community\)|\1|' /etc/apk/repositories 2>/dev/null || true
|
|
|
|
# Ensure community repo is present
|
|
if ! grep -q community /etc/apk/repositories; then
|
|
MIRROR=\$(grep main /etc/apk/repositories | head -1 | sed 's|/main|/community|')
|
|
echo \"\$MIRROR\" >> /etc/apk/repositories
|
|
fi
|
|
|
|
apk update
|
|
apk add ${APK_PACKAGES}
|
|
"
|
|
elif [[ "$DISTRO" == "ubuntu" ]] && [[ -n "${APT_PACKAGES:-}" ]]; then
|
|
echo "Installing packages: ${APT_PACKAGES}..."
|
|
sudo chroot "$MOUNT_DIR" /bin/bash -c "
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends ${APT_PACKAGES}
|
|
"
|
|
fi
|
|
|
|
# Enable services
|
|
if [[ -n "${ENABLE_SERVICES:-}" ]]; then
|
|
echo "Enabling services: ${ENABLE_SERVICES}..."
|
|
IFS=' ' read -ra SERVICES <<< "$ENABLE_SERVICES"
|
|
for svc in "${SERVICES[@]}"; do
|
|
if [[ "$DISTRO" == "alpine" ]]; then
|
|
if ! sudo chroot "$MOUNT_DIR" /bin/sh -c "rc-update add ${svc} default"; then
|
|
echo "ERROR: Failed to enable service '${svc}' — does it exist in the image?"
|
|
exit 1
|
|
fi
|
|
else
|
|
if ! sudo chroot "$MOUNT_DIR" /bin/bash -c "systemctl enable ${svc}"; then
|
|
echo "ERROR: Failed to enable service '${svc}'"
|
|
exit 1
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Step 5: Copy binaries into the image (before setup script, which may reference them)
|
|
if [[ -n "${INSTALL_BINARIES:-}" ]]; then
|
|
echo "Installing binaries..."
|
|
IFS=' ' read -ra BINARY_PAIRS <<< "$INSTALL_BINARIES"
|
|
for pair in "${BINARY_PAIRS[@]}"; do
|
|
src="${pair%%:*}"
|
|
dst="${pair#*:}"
|
|
src_path="${PROJECT_DIR}/${src}"
|
|
echo " ${src} -> ${dst}"
|
|
sudo cp "$src_path" "${MOUNT_DIR}${dst}"
|
|
sudo chmod 755 "${MOUNT_DIR}${dst}"
|
|
done
|
|
fi
|
|
|
|
# Run custom setup script inside chroot (after binaries are installed)
|
|
if [[ -n "${SETUP_SCRIPT:-}" ]]; then
|
|
setup_path="${PROJECT_DIR}/${SETUP_SCRIPT}"
|
|
if [[ ! -f "$setup_path" ]]; then
|
|
echo "ERROR: Setup script not found: $setup_path"
|
|
exit 1
|
|
fi
|
|
echo "Running setup script: ${SETUP_SCRIPT}..."
|
|
sudo cp "$setup_path" "${MOUNT_DIR}/tmp/setup.sh"
|
|
sudo chmod 755 "${MOUNT_DIR}/tmp/setup.sh"
|
|
if [[ "$DISTRO" == "ubuntu" ]]; then
|
|
sudo chroot "$MOUNT_DIR" /bin/bash /tmp/setup.sh
|
|
else
|
|
sudo chroot "$MOUNT_DIR" /tmp/setup.sh
|
|
fi
|
|
sudo rm -f "${MOUNT_DIR}/tmp/setup.sh"
|
|
fi
|
|
|
|
# Step 6: Clean up and unmount
|
|
echo "Cleaning up image..."
|
|
if [[ "$DISTRO" == "alpine" ]]; then
|
|
sudo chroot "$MOUNT_DIR" /bin/sh -c '
|
|
apk cache clean 2>/dev/null || true
|
|
rm -rf /var/cache/apk/* /tmp/*
|
|
'
|
|
else
|
|
sudo chroot "$MOUNT_DIR" /bin/bash -c '
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get clean
|
|
rm -rf /var/lib/apt/lists/* /tmp/*
|
|
'
|
|
fi
|
|
|
|
# Restore the systemd-resolved symlink (Ubuntu default); cloud-init sets DNS on boot.
|
|
sudo rm -f "${MOUNT_DIR}/etc/resolv.conf"
|
|
sudo ln -sf /run/systemd/resolve/stub-resolv.conf "${MOUNT_DIR}/etc/resolv.conf"
|
|
|
|
echo "Unmounting..."
|
|
if [[ "$DISTRO" == "ubuntu" ]]; then
|
|
sudo umount "${MOUNT_DIR}/dev/pts"
|
|
sudo umount "${MOUNT_DIR}/dev"
|
|
sudo umount "${MOUNT_DIR}/sys"
|
|
sudo umount "${MOUNT_DIR}/proc"
|
|
fi
|
|
sudo umount "$MOUNT_DIR"
|
|
sudo qemu-nbd --disconnect "${NBD_DEV}"
|
|
|
|
# Step 7: Convert to raw for import
|
|
echo "Converting to raw format..."
|
|
qemu-img convert -f qcow2 -O raw "$OUTPUT_IMAGE" "$OUTPUT_RAW"
|
|
|
|
# Restore stdout if suppressed
|
|
if [[ "$QUIET" == true ]]; then
|
|
exec 1>&3 3>&-
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Build complete ==="
|
|
echo " Image: ${IMAGE_NAME}"
|
|
echo " qcow2: $OUTPUT_IMAGE ($(du -h "$OUTPUT_IMAGE" | cut -f1))"
|
|
echo " raw: $OUTPUT_RAW ($(du -h "$OUTPUT_RAW" | cut -f1))"
|
|
echo ""
|
|
|
|
if [[ "$DO_IMPORT" == true ]]; then
|
|
echo "Importing as AMI..."
|
|
IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64)
|
|
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
|
IMPORT_ARGS+=(--tag "$SYSTEM_TAG")
|
|
fi
|
|
(cd "$PROJECT_DIR" && sudo -u spinifex-storage ./bin/spx admin images import "${IMPORT_ARGS[@]}")
|
|
else
|
|
echo "To import as AMI, run:"
|
|
echo " cd $PROJECT_DIR && ./bin/spx admin images import \\"
|
|
echo " --file $OUTPUT_RAW \\"
|
|
if [[ -n "${SYSTEM_TAG:-}" ]]; then
|
|
echo " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64 \\"
|
|
echo " --tag ${SYSTEM_TAG}"
|
|
else
|
|
echo " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64"
|
|
fi
|
|
fi
|