#!/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 ------------------------------------------- - | 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. Is the repo cloned?" echo " git clone --recurse-submodules https://git.dannyhaslund.dk/danny8632/darkforge.git ~/darkforge" exit 1 fi ARGS="$*" 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@ (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@:~/darkforge/tests/report.* ./" echo "═══════════════════════════════════════════════"