#!/bin/bash source ~/.bashrc # Create Proxmox VM from QCOW2/RAW Image - Comprehensive Automation Script # # This script automates the complete workflow for creating a VM from any disk image # in Proxmox VE using the qm command-line interface. # # Reference: https://pve.proxmox.com/pve-docs/qm.1.html # # Usage: # ./scripts/create-vm-from-image.sh --vmid 9000 --name "ubuntu-24.04" \ # --image /path/to/image.img --storage local-lvm set -e # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' RED='\033[0;31m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "${BLUE}[STEP]${NC} $1" } # Default values VMID="" VMNAME="" IMAGE="" STORAGE="local-lvm" MEMORY=4096 CORES=2 BRIDGE="vmbr0" VLAN_TAG="" ENABLE_CLOUD_INIT=false ENABLE_UEFI=false ENABLE_TEMPLATE=false ENABLE_SERIAL=false CIUSER="" CIPASSWORD="" SSHKEY="" IPCONFIG="" NAMESERVER="" SEARCHDOMAIN="" CPU_TYPE="host" ENABLE_AGENT=true IOTHREAD=true CACHE_MODE="none" ENABLE_DISCARD=false BALLOON=0 DESCRIPTION="" TAGS="" NODE="" DRY_RUN=false # Load environment variables from .env if available if [ -f .env ]; then set -a source <(grep -v '^#' .env | grep -v '^$' | sed 's/#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep '=') set +a fi # Parse command line arguments parse_args() { while [[ $# -gt 0 ]]; do case $1 in --vmid) VMID="$2" shift 2 ;; --name) VMNAME="$2" shift 2 ;; --image) IMAGE="$2" shift 2 ;; --storage) STORAGE="$2" shift 2 ;; --memory) MEMORY="$2" shift 2 ;; --cores) CORES="$2" shift 2 ;; --bridge) BRIDGE="$2" shift 2 ;; --vlan) VLAN_TAG="$2" shift 2 ;; --cloud-init) ENABLE_CLOUD_INIT=true shift ;; --uefi) ENABLE_UEFI=true shift ;; --template) ENABLE_TEMPLATE=true shift ;; --serial) ENABLE_SERIAL=true shift ;; --ciuser) CIUSER="$2" shift 2 ;; --cipassword) CIPASSWORD="$2" shift 2 ;; --sshkey) SSHKEY="$2" shift 2 ;; --sshkey-file) if [ -f "$2" ]; then SSHKEY="$(cat "$2")" else log_error "SSH key file not found: $2" exit 1 fi shift 2 ;; --ipconfig) IPCONFIG="$2" shift 2 ;; --nameserver) NAMESERVER="$2" shift 2 ;; --searchdomain) SEARCHDOMAIN="$2" shift 2 ;; --cpu) CPU_TYPE="$2" shift 2 ;; --no-agent) ENABLE_AGENT=false shift ;; --no-iothread) IOTHREAD=false shift ;; --cache) CACHE_MODE="$2" shift 2 ;; --discard) ENABLE_DISCARD=true shift ;; --balloon) BALLOON="$2" shift 2 ;; --description) DESCRIPTION="$2" shift 2 ;; --tags) TAGS="$2" shift 2 ;; --node) NODE="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --help) show_help exit 0 ;; *) log_error "Unknown option: $1" show_help exit 1 ;; esac done } # Show help message show_help() { cat << EOF Create Proxmox VM from QCOW2/RAW Image Usage: $0 [OPTIONS] Required Options: --vmid ID VM ID (e.g., 9000) --name NAME VM name (e.g., "ubuntu-24.04-cloudinit") --image PATH Full path to image file Optional Options: --storage STORAGE Storage pool (default: local-lvm) --memory MB Memory in MB (default: 4096) --cores NUM CPU cores (default: 2) --bridge BRIDGE Network bridge (default: vmbr0) --vlan TAG VLAN tag number Cloud-Init Options: --cloud-init Enable Cloud-Init support --ciuser USER Cloud-Init username --cipassword PASS Cloud-Init password (not recommended) --sshkey KEY SSH public key (or use --sshkey-file) --sshkey-file FILE Read SSH key from file --ipconfig CONFIG IP configuration (e.g., "ip=192.168.1.100/24,gw=192.168.1.1") --nameserver DNS DNS servers (space-separated) --searchdomain DOMAIN Search domains VM Configuration: --uefi Enable UEFI/OVMF (recommended for modern images) --cpu TYPE CPU type (default: host, options: host, kvm64, etc.) --no-agent Disable QEMU Guest Agent --no-iothread Disable IO thread --cache MODE Disk cache mode (none, writeback, writethrough) --discard Enable discard (for thin provisioning) --balloon MB Memory balloon size in MB Other Options: --template Convert to template after creation --serial Enable serial console --description TEXT VM description --tags TAGS Tags (comma-separated, e.g., "dev,web") --node NODE Target Proxmox node --dry-run Show commands without executing --help Show this help message Examples: # Basic VM creation $0 --vmid 9000 --name "ubuntu-24.04" \\ --image /var/lib/vz/template/iso/ubuntu-24.04-server-cloudimg-amd64.img # Full cloud-init VM $0 --vmid 9000 --name "ubuntu-24.04-cloudinit" \\ --image /var/lib/vz/template/iso/ubuntu-24.04-server-cloudimg-amd64.img \\ --storage local-lvm --memory 4096 --cores 2 \\ --cloud-init --uefi --serial \\ --ciuser ubuntu --sshkey-file ~/.ssh/id_rsa.pub \\ --ipconfig "ip=192.168.1.100/24,gw=192.168.1.1" # Create and convert to template $0 --vmid 9000 --name "ubuntu-template" \\ --image /var/lib/vz/template/iso/ubuntu-24.04-server-cloudimg-amd64.img \\ --cloud-init --uefi --template \\ --ciuser ubuntu --sshkey-file ~/.ssh/id_rsa.pub EOF } # Validate required arguments validate_args() { if [ -z "$VMID" ]; then log_error "VMID is required. Use --vmid option." exit 1 fi if [ -z "$VMNAME" ]; then log_error "VM name is required. Use --name option." exit 1 fi if [ -z "$IMAGE" ]; then log_error "Image path is required. Use --image option." exit 1 fi if [ ! -f "$IMAGE" ]; then log_error "Image file not found: $IMAGE" exit 1 fi # Validate VMID is numeric if ! [[ "$VMID" =~ ^[0-9]+$ ]]; then log_error "VMID must be numeric: $VMID" exit 1 fi # Check if VMID already exists if qm list | grep -q "^\s*$VMID\s"; then log_error "VM with ID $VMID already exists" exit 1 fi # Validate storage exists if ! pvesm status | grep -q "^$STORAGE\s"; then log_warn "Storage '$STORAGE' not found in pvesm status" log_info "Available storage:" pvesm status log_warn "Continuing anyway..." fi } # Validate image validate_image() { log_step "Validating image: $IMAGE" # Check image format if ! command -v qemu-img &> /dev/null; then log_warn "qemu-img not found, skipping image validation" return fi local image_info image_info=$(qemu-img info "$IMAGE" 2>&1) if [ $? -ne 0 ]; then log_error "Failed to read image: $IMAGE" log_error "$image_info" exit 1 fi log_info "Image format: $(echo "$image_info" | grep "file format" | awk '{print $3}')" log_info "Virtual size: $(echo "$image_info" | grep "virtual size" | awk -F'[()]' '{print $2}')" } # Create VM shell create_vm_shell() { log_step "Creating VM shell (ID: $VMID, Name: $VMNAME)" local cmd="qm create $VMID --name \"$VMNAME\" --memory $MEMORY --cores $CORES" # Add node if specified if [ -n "$NODE" ]; then cmd="$cmd --target $NODE" fi # Configure network if [ -n "$VLAN_TAG" ]; then cmd="$cmd --net0 virtio,bridge=$BRIDGE,tag=$VLAN_TAG" else cmd="$cmd --net0 virtio,bridge=$BRIDGE" fi # Configure CPU cmd="$cmd --cpu $CPU_TYPE" # Enable agent if [ "$ENABLE_AGENT" = true ]; then cmd="$cmd --agent 1" fi # Add description if provided if [ -n "$DESCRIPTION" ]; then cmd="$cmd --description \"$DESCRIPTION\"" fi # Add tags if provided if [ -n "$TAGS" ]; then cmd="$cmd --tags $TAGS" fi log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ VM shell created" else log_info "[DRY RUN] Would execute: $cmd" fi } # Import disk import_disk() { log_step "Importing disk from image: $IMAGE" local cmd="qm importdisk $VMID \"$IMAGE\" $STORAGE" log_info "Command: $cmd" log_info "This may take several minutes depending on image size..." if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ Disk imported" # Get the volume name (usually vm--disk-0) local volume_name="vm-${VMID}-disk-0" log_info "Imported volume: $volume_name" else log_info "[DRY RUN] Would execute: $cmd" fi } # Attach disk attach_disk() { log_step "Attaching imported disk" local volume_name="vm-${VMID}-disk-0" local cmd="qm set $VMID --scsihw virtio-scsi-pci --scsi0 ${STORAGE}:${volume_name}" # Add IO thread if enabled if [ "$IOTHREAD" = true ]; then cmd="$cmd --iothread 1" fi # Add cache mode cmd="$cmd --cache $CACHE_MODE" # Add discard if enabled if [ "$ENABLE_DISCARD" = true ]; then cmd="$cmd --discard on" fi log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ Disk attached" else log_info "[DRY RUN] Would execute: $cmd" fi } # Configure boot configure_boot() { log_step "Configuring boot settings" local cmd="qm set $VMID --boot order=scsi0" # Configure BIOS/UEFI if [ "$ENABLE_UEFI" = true ]; then cmd="$cmd --bios ovmf --efidisk0 ${STORAGE}:1,format=raw" log_info "UEFI/OVMF enabled" else cmd="$cmd --bios seabios" log_info "BIOS (SeaBIOS) enabled" fi log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ Boot configured" else log_info "[DRY RUN] Would execute: $cmd" fi } # Configure Cloud-Init configure_cloud_init() { log_step "Configuring Cloud-Init" # Add Cloud-Init drive local cmd="qm set $VMID --ide2 ${STORAGE}:cloudinit" # Enable serial console if requested if [ "$ENABLE_SERIAL" = true ] || [ "$ENABLE_CLOUD_INIT" = true ]; then cmd="$cmd --serial0 socket --vga serial0" fi log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" else log_info "[DRY RUN] Would execute: $cmd" fi # Configure Cloud-Init user if [ -n "$CIUSER" ]; then cmd="qm set $VMID --ciuser $CIUSER" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" else log_info "[DRY RUN] Would execute: $cmd" fi fi # Configure password (if provided, but not recommended) if [ -n "$CIPASSWORD" ]; then cmd="qm set $VMID --cipassword \"$CIPASSWORD\"" log_warn "Setting password via Cloud-Init (not recommended, use SSH keys instead)" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" else log_info "[DRY RUN] Would execute: $cmd" fi fi # Configure SSH key if [ -n "$SSHKEY" ]; then cmd="qm set $VMID --sshkey \"$SSHKEY\"" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ SSH key configured" else log_info "[DRY RUN] Would execute: $cmd" fi fi # Configure IP if [ -n "$IPCONFIG" ]; then cmd="qm set $VMID --ipconfig0 $IPCONFIG" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" else log_info "[DRY RUN] Would execute: $cmd" fi fi # Configure DNS if [ -n "$NAMESERVER" ]; then cmd="qm set $VMID --nameserver \"$NAMESERVER\"" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" else log_info "[DRY RUN] Would execute: $cmd" fi fi # Configure search domain if [ -n "$SEARCHDOMAIN" ]; then cmd="qm set $VMID --searchdomain \"$SEARCHDOMAIN\"" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" else log_info "[DRY RUN] Would execute: $cmd" fi fi if [ "$DRY_RUN" = false ]; then log_info "✓ Cloud-Init configured" fi } # Configure memory balloon configure_balloon() { if [ "$BALLOON" -gt 0 ]; then log_step "Configuring memory balloon: ${BALLOON}MB" local cmd="qm set $VMID --balloon $BALLOON" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ Memory balloon configured" else log_info "[DRY RUN] Would execute: $cmd" fi fi } # Start VM start_vm() { log_step "Starting VM" local cmd="qm start $VMID" log_info "Command: $cmd" if [ "$DRY_RUN" = false ]; then eval "$cmd" log_info "✓ VM started" # Show status sleep 2 qm status $VMID else log_info "[DRY RUN] Would execute: $cmd" fi } # Convert to template convert_to_template() { if [ "$ENABLE_TEMPLATE" = false ]; then return fi log_step "Converting VM to template" log_warn "VM must be shut down before converting to template" if [ "$DRY_RUN" = false ]; then # Check if VM is running local status status=$(qm status $VMID 2>&1 | grep "status:" | awk '{print $2}') if [ "$status" = "running" ]; then log_info "VM is running. Shutting down..." qm shutdown $VMID log_info "Waiting for shutdown (this may take a minute)..." local max_wait=60 local waited=0 while [ $waited -lt $max_wait ]; do status=$(qm status $VMID 2>&1 | grep "status:" | awk '{print $2}') if [ "$status" != "running" ]; then break fi sleep 2 waited=$((waited + 2)) echo -n "." done echo "" fi # Convert to template qm template $VMID log_info "✓ VM converted to template" else log_info "[DRY RUN] Would execute: qm shutdown $VMID && qm template $VMID" fi } # Main function main() { echo "=========================================" echo "Create Proxmox VM from Image" echo "=========================================" echo "" parse_args "$@" if [ "$DRY_RUN" = true ]; then log_warn "DRY RUN MODE - No changes will be made" echo "" fi validate_args validate_image echo "" log_info "VM Configuration:" log_info " VMID: $VMID" log_info " Name: $VMNAME" log_info " Image: $IMAGE" log_info " Storage: $STORAGE" log_info " Memory: ${MEMORY}MB" log_info " Cores: $CORES" log_info " Bridge: $BRIDGE" [ -n "$VLAN_TAG" ] && log_info " VLAN: $VLAN_TAG" [ "$ENABLE_CLOUD_INIT" = true ] && log_info " Cloud-Init: Enabled" [ "$ENABLE_UEFI" = true ] && log_info " UEFI: Enabled" [ "$ENABLE_TEMPLATE" = true ] && log_info " Convert to Template: Yes" echo "" if [ "$DRY_RUN" = false ]; then read -p "Continue with VM creation? (y/N): " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Aborted by user" exit 0 fi echo "" fi create_vm_shell import_disk attach_disk configure_boot if [ "$ENABLE_CLOUD_INIT" = true ]; then configure_cloud_init fi configure_balloon if [ "$ENABLE_TEMPLATE" = false ]; then start_vm else log_info "Skipping VM start (will be converted to template)" fi convert_to_template echo "" log_info "=========================================" log_info "VM Creation Complete!" log_info "=========================================" if [ "$ENABLE_TEMPLATE" = false ] && [ "$DRY_RUN" = false ]; then echo "" log_info "VM Status:" qm status $VMID echo "" log_info "View VM console: qm terminal $VMID" log_info "View VM config: qm config $VMID" fi } # Run main function main "$@"