Files
darkforge/tests/proxmox/create-vm.sh
Danny 83760025b6 Fix darkforge-test heredoc indentation in cloud-init
The cat heredoc inside cloud-init runcmd was indented, causing the
shebang line to become "    #!/bin/bash" (with leading spaces) which
makes the script fail to execute as a proper interpreter.

Fixed by removing indentation from the heredoc body. Also improved
the error message to explain that the clone likely failed during
provisioning and show the manual clone command.

Added tmux kill-session before starting new session to avoid
"duplicate session" errors on re-run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:10:03 +01:00

242 lines
8.9 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# DarkForge — Proxmox VM Creation Script
# ============================================================================
# Run this on the Proxmox host to create an Arch Linux test VM.
#
# Requirements:
# - Proxmox VE 8.x or 9.x
# - ~100GB free on a storage pool
# - Internet access
#
# Usage:
# bash create-vm.sh # use defaults
# bash create-vm.sh --vmid 200 # custom VM ID
# bash create-vm.sh --storage local-lvm # custom storage
# ============================================================================
set -euo pipefail
# --- Configuration (override via environment or flags) -----------------------
VMID="${VMID:-900}"
VM_NAME="${VM_NAME:-darkforge-test}"
STORAGE="${STORAGE:-local-lvm}"
DISK_SIZE="${DISK_SIZE:-100G}"
RAM="${RAM:-28672}" # 16GB
CORES="${CORES:-18}"
BRIDGE="${BRIDGE:-vmbr0}"
# Arch Linux cloud image
ARCH_IMG_URL="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
ARCH_IMG_FILE="/var/lib/vz/template/iso/arch-cloudimg.qcow2"
# Git repo to clone inside the VM
DARKFORGE_REPO="gitea@git.dannyhaslund.dk:danny8632/darkforge.git"
# Fallback if SSH key isn't available in the VM
DARKFORGE_REPO_HTTPS="https://git.dannyhaslund.dk/danny8632/darkforge.git"
# Parse args
for arg in "$@"; do
case "$arg" in
--vmid=*) VMID="${arg#*=}" ;;
--storage=*) STORAGE="${arg#*=}" ;;
--cores=*) CORES="${arg#*=}" ;;
--ram=*) RAM="${arg#*=}" ;;
--bridge=*) BRIDGE="${arg#*=}" ;;
esac
done
echo "═══════════════════════════════════════════════"
echo " DarkForge Test VM Creator"
echo " VMID: ${VMID} | Cores: ${CORES} | RAM: ${RAM}MB"
echo " Storage: ${STORAGE} | Disk: ${DISK_SIZE}"
echo "═══════════════════════════════════════════════"
echo ""
# --- Check if VM already exists ----------------------------------------------
if qm status "${VMID}" &>/dev/null; then
echo "VM ${VMID} already exists."
read -p "Destroy and recreate? [y/N] " confirm
if [[ "${confirm}" =~ ^[Yy]$ ]]; then
qm stop "${VMID}" 2>/dev/null || true
sleep 3
qm destroy "${VMID}" --purge
echo "Old VM destroyed."
else
echo "Aborted."
exit 0
fi
fi
# --- Download Arch cloud image if not cached ---------------------------------
if [ ! -f "${ARCH_IMG_FILE}" ]; then
echo ">>> Downloading Arch Linux cloud image..."
mkdir -p "$(dirname "${ARCH_IMG_FILE}")"
wget -q --show-progress -O "${ARCH_IMG_FILE}" "${ARCH_IMG_URL}"
echo ">>> Downloaded: ${ARCH_IMG_FILE}"
else
echo ">>> Using cached image: ${ARCH_IMG_FILE}"
fi
# --- Create the VM -----------------------------------------------------------
echo ">>> Creating VM ${VMID}..."
qm create "${VMID}" \
--name "${VM_NAME}" \
--ostype l26 \
--machine q35 \
--bios ovmf \
--cpu host \
--cores "${CORES}" \
--memory "${RAM}" \
--net0 "virtio,bridge=${BRIDGE}" \
--agent enabled=1 \
--serial0 socket \
--vga serial0
# Add EFI disk (required for OVMF BIOS)
qm set "${VMID}" --efidisk0 "${STORAGE}:1,efitype=4m,pre-enrolled-keys=0"
# Import the cloud image as the boot disk
echo ">>> Importing cloud image as boot disk..."
qm importdisk "${VMID}" "${ARCH_IMG_FILE}" "${STORAGE}" --format qcow2 2>/dev/null
qm set "${VMID}" --scsi0 "${STORAGE}:vm-${VMID}-disk-1,size=${DISK_SIZE}"
qm set "${VMID}" --boot order=scsi0
qm set "${VMID}" --scsihw virtio-scsi-single
# --- Configure cloud-init ----------------------------------------------------
echo ">>> Configuring cloud-init..."
qm set "${VMID}" --ide2 "${STORAGE}:cloudinit"
# Don't use --ciuser/--cipassword — they conflict with the snippet on Arch.
# We handle ALL user creation in the cloud-init snippet runcmd instead.
qm set "${VMID}" --ipconfig0 "ip=dhcp"
# Enable nested virtualization (for QEMU-in-QEMU boot tests)
qm set "${VMID}" --args "-cpu host,+vmx"
# Resize the disk to our desired size
echo ">>> Resizing disk to ${DISK_SIZE}..."
qm resize "${VMID}" scsi0 "${DISK_SIZE}" 2>/dev/null || true
# --- Generate cloud-init user-data snippet -----------------------------------
# NOTE: The Arch Linux cloud image has quirks with user/password handling.
# We do EVERYTHING via runcmd to guarantee it works regardless of cloud-init
# version or Proxmox's cloud-init integration behavior.
SNIPPET_DIR="/var/lib/vz/snippets"
mkdir -p "${SNIPPET_DIR}"
cat > "${SNIPPET_DIR}/darkforge-test-init.yaml" << 'CLOUDINIT'
#cloud-config
# Disable the default user module to avoid conflicts
# We create our user manually via runcmd below
users: []
ssh_pwauth: true
write_files:
- path: /etc/ssh/sshd_config.d/99-darkforge.conf
content: |
PasswordAuthentication yes
PermitRootLogin yes
runcmd:
# --- USER SETUP (do this first, before anything else) ----------------------
# Set root password so we always have a fallback login
- echo 'root:darkforge' | chpasswd
# Create the darkforge user if it doesn't exist
- id darkforge &>/dev/null || useradd -m -G wheel -s /bin/bash darkforge
- echo 'darkforge:darkforge' | chpasswd
# Give darkforge sudo/wheel access without password
- echo 'darkforge ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/darkforge
- chmod 440 /etc/sudoers.d/darkforge
# Enable and restart sshd
- systemctl enable sshd
- systemctl restart sshd
# --- DISK RESIZE -----------------------------------------------------------
- growpart /dev/sda 2 || growpart /dev/vda 2 || true
- resize2fs /dev/sda2 || resize2fs /dev/vda2 || btrfs filesystem resize max / || true
# --- PACKAGE INSTALL -------------------------------------------------------
- pacman-key --init
- pacman-key --populate archlinux
- pacman -Syu --noconfirm
- pacman -S --noconfirm --needed base-devel git wget curl rust cargo qemu-full edk2-ovmf squashfs-tools xorriso dosfstools mtools python bc rsync openssh tmux
# --- CLONE PROJECT ---------------------------------------------------------
- |
su - darkforge -c '
cd /home/darkforge
git clone --recurse-submodules https://git.dannyhaslund.dk/danny8632/darkforge.git 2>/dev/null || \
git clone --recurse-submodules https://github.com/danny8632/darkforge.git 2>/dev/null || \
echo "CLONE FAILED — manually clone the repo after login"
'
# --- INSTALL CONVENIENCE COMMAND -------------------------------------------
# NOTE: heredoc inside cloud-init runcmd must NOT be indented or the
# shebang gets leading spaces and the script won't execute properly.
- |
cat > /usr/local/bin/darkforge-test << 'DTEOF'
#!/bin/bash
SCRIPT="/home/darkforge/darkforge/tests/proxmox/run-in-vm.sh"
if [ ! -f "$SCRIPT" ]; then
echo "ERROR: Test script not found at: $SCRIPT"
echo ""
echo "The git clone probably failed during provisioning."
echo "Clone manually:"
echo " git clone --recurse-submodules https://git.dannyhaslund.dk/danny8632/darkforge.git ~/darkforge"
echo ""
echo "Then run: darkforge-test"
exit 1
fi
ARGS="$*"
# Kill any existing tmux session first
tmux kill-session -t darkforge 2>/dev/null || true
exec tmux new-session -d -s darkforge \
"bash ${SCRIPT} --tmux ${ARGS}; echo ''; echo 'Tests finished. Press Enter to close.'; read" \; \
attach-session -t darkforge
DTEOF
chmod +x /usr/local/bin/darkforge-test
# --- SIGNAL DONE -----------------------------------------------------------
- touch /home/darkforge/.provisioned
- chown darkforge:darkforge /home/darkforge/.provisioned
CLOUDINIT
qm set "${VMID}" --cicustom "user=local:snippets/darkforge-test-init.yaml"
# --- Start the VM ------------------------------------------------------------
echo ""
echo ">>> Starting VM ${VMID}..."
qm start "${VMID}"
echo ""
echo "═══════════════════════════════════════════════"
echo " VM ${VMID} created and starting."
echo ""
echo " Cloud-init will install packages and clone the repo."
echo " Wait ~5 min for provisioning, then SSH in to run tests."
echo ""
echo " Get the VM IP:"
echo " qm guest cmd ${VMID} network-get-interfaces | grep -oP '\"ip-address\":\\s*\"\\K[0-9.]+'"
echo ""
echo " SSH in:"
echo " ssh darkforge@<IP> (password: darkforge)"
echo ""
echo " Run tests in a tmux session (detachable):"
echo " darkforge-test # starts tests in tmux"
echo " darkforge-test --quick # fast mode (30 min)"
echo ""
echo " Detach from tmux: Ctrl+B then D"
echo " Reattach later: tmux attach -t darkforge"
echo ""
echo " Collect report:"
echo " scp darkforge@<IP>:~/darkforge/tests/report.* ./"
echo "═══════════════════════════════════════════════"