156 lines
6.7 KiB
Bash
156 lines
6.7 KiB
Bash
#!/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:-alpine-docker-host}"
|
|
DOCKERFILE="${DOCKERFILE:-Dockerfile.alpine-host}"
|
|
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}' (${DOCKERFILE})..."
|
|
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 ""
|