diff --git a/scripts/build-qemu.sh b/scripts/build-qemu.sh new file mode 100644 index 0000000..caca4fd --- /dev/null +++ b/scripts/build-qemu.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# ─────────────────────────────────────────────────────────── +# build-qemu.sh — Dockerfile → Bootable QEMU Disk +# +# Pipeline: +# 1. Build the Docker image (ansible-node) +# 2. Export the container rootfs as a tarball +# 3. Extract kernel + initramfs for direct -kernel boot +# 4. Create an ext4 disk image, label ANSIBLE_ROOT +# 5. Populate with rootfs contents +# 6. Optionally convert raw → qcow2 (if qemu-img available) +# +# Output (written to ./output/): +# ansible-node.qcow2 (or .raw) — root filesystem disk +# vmlinuz-virt — Linux kernel +# initramfs-virt — initramfs +# +# Prerequisites: +# docker, sudo, mkfs.ext4, optional: qemu-img +# +# Usage: +# ./scripts/build-qemu.sh # default 2GB +# DISK_SIZE_MB=4096 ./scripts/build-qemu.sh # custom size +# ─────────────────────────────────────────────────────────── +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +OUTPUT_DIR="$PROJECT_DIR/output" +IMAGE_NAME="${IMAGE_NAME:-ansible-node}" +DISK_SIZE_MB="${DISK_SIZE_MB:-2048}" + +# ── Colour helpers ────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${BLUE}[*]${NC} $*"; } +ok() { echo -e "${GREEN}[✓]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +err() { echo -e "${RED}[✗]${NC} $*"; } + +# ── Preflight checks ──────────────────────────────────────── +for cmd in docker sudo mkfs.ext4; do + if ! command -v "$cmd" &>/dev/null; then + err "Missing required tool: $cmd" + exit 1 + fi +done + +# ── Banner ────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════════════${NC}" +echo -e "${BOLD} Ansible Control Node — QEMU Image Builder${NC}" +echo -e "${BOLD}══════════════════════════════════════════════${NC}" +echo "" +info "Image name: ${IMAGE_NAME}" +info "Disk size: ${DISK_SIZE_MB}MB" +info "Output dir: ${OUTPUT_DIR}" +echo "" + +mkdir -p "$OUTPUT_DIR" + +# ── Step 1: Build Docker image ────────────────────────────── +info "Step 1/6: Building Docker image '${IMAGE_NAME}'..." +docker build \ + -t "$IMAGE_NAME" \ + -f "$PROJECT_DIR/Dockerfile" \ + "$PROJECT_DIR" +ok "Docker image built" + +# ── Step 2: Export rootfs ─────────────────────────────────── +info "Step 2/6: Exporting container rootfs..." +ROOTFS_TAR="$PROJECT_DIR/.ansible-node-rootfs.tar" +CID=$(docker create "$IMAGE_NAME") +docker export "$CID" -o "$ROOTFS_TAR" +docker rm "$CID" >/dev/null +ROOTFS_SIZE=$(du -sh "$ROOTFS_TAR" | cut -f1) +ok "Rootfs exported (${ROOTFS_SIZE})" + +# ── Step 3: Extract kernel + initramfs ────────────────────── +info "Step 3/6: Extracting kernel and initramfs..." +TMP_BOOT="$(mktemp -d)" +tar -xf "$ROOTFS_TAR" -C "$TMP_BOOT" boot/ 2>/dev/null + +# Find kernel/initramfs (handle different naming patterns) +KERNEL_SRC=$(find "$TMP_BOOT/boot" -name 'vmlinuz-*' 2>/dev/null | head -1) +INITRD_SRC=$(find "$TMP_BOOT/boot" -name 'initramfs-*' 2>/dev/null | head -1) + +if [ -z "$KERNEL_SRC" ] || [ -z "$INITRD_SRC" ]; then + err "Could not find kernel/initramfs in rootfs." + err "Expected files in /boot/ from linux-virt package." + ls -la "$TMP_BOOT/boot/" 2>/dev/null || echo "(no /boot directory)" + rm -rf "$TMP_BOOT" "$ROOTFS_TAR" + exit 1 +fi + +KERNEL_NAME=$(basename "$KERNEL_SRC") +INITRD_NAME=$(basename "$INITRD_SRC") + +cp "$KERNEL_SRC" "$OUTPUT_DIR/vmlinuz-virt" +cp "$INITRD_SRC" "$OUTPUT_DIR/initramfs-virt" +rm -rf "$TMP_BOOT" + +ok "Kernel: ${KERNEL_NAME}" +ok "Initrd: ${INITRD_NAME}" + +# ── Step 4: Create raw disk image ─────────────────────────── +info "Step 4/6: Creating disk image (${DISK_SIZE_MB}MB)..." +RAW_DISK="$OUTPUT_DIR/ansible-node.raw" +dd if=/dev/zero of="$RAW_DISK" bs=1M count="$DISK_SIZE_MB" status=progress 2>/dev/null +mkfs.ext4 -q -L ANSIBLE_ROOT "$RAW_DISK" +ok "ext4 filesystem created (label: ANSIBLE_ROOT)" + +# ── Step 5: Mount and populate rootfs ─────────────────────── +info "Step 5/6: Populating root filesystem..." +MNT="$(mktemp -d)" +sudo mount -o loop "$RAW_DISK" "$MNT" +sudo tar -xf "$ROOTFS_TAR" -C "$MNT" +sudo umount "$MNT" +rmdir "$MNT" +ok "Rootfs written to disk" + +# ── Step 6: Convert to qcow2 (optional) ───────────────────── +info "Step 6/6: Finalizing..." +if command -v qemu-img &>/dev/null; then + QCOW2_DISK="$OUTPUT_DIR/ansible-node.qcow2" + qemu-img convert -f raw -O qcow2 "$RAW_DISK" "$QCOW2_DISK" + rm "$RAW_DISK" + ok "Converted to qcow2: ansible-node.qcow2" + FINAL_DISK="$QCOW2_DISK" + FINAL_FMT="qcow2" +else + warn "qemu-img not found — keeping raw image" + ok "Raw image: ansible-node.raw" + FINAL_DISK="$RAW_DISK" + FINAL_FMT="raw" +fi + +# ── Cleanup ───────────────────────────────────────────────── +rm -f "$ROOTFS_TAR" + +# ── Summary ───────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════════════${NC}" +echo -e "${GREEN}${BOLD} Build complete!${NC}" +echo -e "${BOLD}══════════════════════════════════════════════${NC}" +echo "" +echo -e " Disk: ${BOLD}${FINAL_DISK}${NC} (${FINAL_FMT})" +echo -e " Kernel: ${BOLD}${OUTPUT_DIR}/vmlinuz-virt${NC}" +echo -e " Initrd: ${BOLD}${OUTPUT_DIR}/initramfs-virt${NC}" +echo "" +echo -e " Launch: ${BOLD}./scripts/run-qemu.sh${NC}" +echo "" +ls -lh "$OUTPUT_DIR/" +echo ""