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>
242 lines
8.9 KiB
Bash
Executable File
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 "═══════════════════════════════════════════════"
|