#!/bin/bash source ~/.bashrc # Create Proxmox Cloud-Init Template with Best Practices # # This script creates an optimized cloud-init template for Proxmox VE # following best practices for template management and cloud-init configuration. # # Reference: https://pve.proxmox.com/pve-docs/qm.1.html set -e # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' RED='\033[0;31m' NC='\033[0m' # 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="" TEMPLATE_NAME="" IMAGE="" STORAGE="local-lvm" MEMORY=2048 CORES=2 BRIDGE="vmbr0" CIUSER="ubuntu" SSHKEY="" SSHKEY_FILE="" IPCONFIG="ip=dhcp" NAMESERVER="" SEARCHDOMAIN="" DESCRIPTION="" TAGS="template,cloud-init" NODE="" SKIP_VERIFICATION=false OPTIMIZE_TEMPLATE=true # 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) TEMPLATE_NAME="$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 ;; --ciuser) CIUSER="$2" shift 2 ;; --sshkey) SSHKEY="$2" shift 2 ;; --sshkey-file) SSHKEY_FILE="$2" shift 2 ;; --ipconfig) IPCONFIG="$2" shift 2 ;; --nameserver) NAMESERVER="$2" shift 2 ;; --searchdomain) SEARCHDOMAIN="$2" shift 2 ;; --description) DESCRIPTION="$2" shift 2 ;; --tags) TAGS="$2" shift 2 ;; --node) NODE="$2" shift 2 ;; --skip-verification) SKIP_VERIFICATION=true shift ;; --no-optimize) OPTIMIZE_TEMPLATE=false 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 Cloud-Init Template with Best Practices Usage: $0 [OPTIONS] Required Options: --vmid ID VM ID (e.g., 9000) --name NAME Template name (e.g., "ubuntu-24.04-cloudinit") --image PATH Full path to cloud image file Optional Options: --storage STORAGE Storage pool (default: local-lvm) --memory MB Memory in MB (default: 2048, minimal for template) --cores NUM CPU cores (default: 2) --bridge BRIDGE Network bridge (default: vmbr0) Cloud-Init Configuration: --ciuser USER Cloud-Init username (default: ubuntu) --sshkey KEY SSH public key (or use --sshkey-file) --sshkey-file FILE Read SSH key from file --ipconfig CONFIG IP configuration (default: ip=dhcp) --nameserver DNS DNS servers (space-separated) --searchdomain DOMAIN Search domains Template Options: --description TEXT Template description --tags TAGS Tags (comma-separated, default: "template,cloud-init") --node NODE Target Proxmox node --skip-verification Skip template verification after creation --no-optimize Skip template optimization steps Examples: # Create template from Ubuntu cloud image $0 --vmid 9000 --name "ubuntu-24.04-cloudinit" \\ --image /var/lib/vz/template/iso/ubuntu-24.04-server-cloudimg-amd64.img \\ --sshkey-file ~/.ssh/id_rsa.pub # Create template with custom configuration $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 2048 --cores 2 \\ --ciuser ubuntu --sshkey-file ~/.ssh/id_rsa.pub \\ --description "Ubuntu 24.04 LTS Cloud-Init Template" EOF } # Validate required arguments validate_args() { if [ -z "$VMID" ]; then log_error "VMID is required. Use --vmid option." exit 1 fi if [ -z "$TEMPLATE_NAME" ]; then log_error "Template 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 # Load SSH key from file if specified if [ -n "$SSHKEY_FILE" ]; then if [ ! -f "$SSHKEY_FILE" ]; then log_error "SSH key file not found: $SSHKEY_FILE" exit 1 fi SSHKEY="$(cat "$SSHKEY_FILE")" log_info "Loaded SSH key from: $SSHKEY_FILE" fi # Validate SSH key format if provided if [ -n "$SSHKEY" ]; then if ! echo "$SSHKEY" | grep -qE "^ssh-(rsa|ed25519|ecdsa)"; then log_warn "SSH key format may be invalid (should start with ssh-rsa, ssh-ed25519, or ecdsa)" fi fi } # Validate template after creation verify_template() { log_step "Verifying template configuration" local config config=$(qm config $VMID 2>&1) if [ $? -ne 0 ]; then log_error "Failed to read template configuration" return 1 fi local errors=0 # Check Cloud-Init is configured if ! echo "$config" | grep -q "ide2.*cloudinit"; then log_warn "Cloud-Init drive not found in template" errors=$((errors + 1)) fi # Check serial console is enabled if ! echo "$config" | grep -q "serial0.*socket"; then log_warn "Serial console not enabled (recommended for cloud-init)" errors=$((errors + 1)) fi # Check SSH key is configured if [ -n "$SSHKEY" ]; then if ! echo "$config" | grep -q "sshkey"; then log_warn "SSH key not found in template configuration" errors=$((errors + 1)) fi fi # Check UEFI is enabled if ! echo "$config" | grep -q "bios.*ovmf"; then log_warn "UEFI not enabled (recommended for modern images)" fi if [ $errors -eq 0 ]; then log_info "✓ Template configuration verified" return 0 else log_warn "Template has $errors configuration warnings" return 1 fi } # Clone template for testing test_template_clone() { if [ "$SKIP_VERIFICATION" = true ]; then return fi log_step "Testing template by creating a temporary clone" local test_vmid=$((VMID + 1000)) # Use a different VMID range local test_name="${TEMPLATE_NAME}-test-$$" # Find available VMID while qm list | grep -q "^\s*$test_vmid\s"; do test_vmid=$((test_vmid + 1)) done log_info "Creating test clone: VMID $test_vmid" # Create linked clone if ! qm clone $VMID $test_vmid --name "$test_name" > /dev/null 2>&1; then log_error "Failed to create test clone" return 1 fi log_info "✓ Test clone created successfully (VMID: $test_vmid)" # Clean up test clone read -p "Delete test clone $test_vmid? (Y/n): " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Nn]$ ]]; then log_info "Deleting test clone..." qm destroy $test_vmid --purge log_info "✓ Test clone deleted" else log_info "Test clone preserved. Manual cleanup required: qm destroy $test_vmid --purge" fi return 0 } # Create template using the main script create_template() { log_step "Creating template using create-vm-from-image.sh" # Build command local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local create_script="${script_dir}/create-vm-from-image.sh" if [ ! -f "$create_script" ]; then log_error "create-vm-from-image.sh not found at: $create_script" exit 1 fi local cmd="$create_script" cmd="$cmd --vmid $VMID" cmd="$cmd --name \"$TEMPLATE_NAME\"" cmd="$cmd --image \"$IMAGE\"" cmd="$cmd --storage $STORAGE" cmd="$cmd --memory $MEMORY" cmd="$cmd --cores $CORES" cmd="$cmd --bridge $BRIDGE" cmd="$cmd --cloud-init" cmd="$cmd --uefi" cmd="$cmd --serial" cmd="$cmd --template" cmd="$cmd --cpu host" cmd="$cmd --cache none" cmd="$cmd --discard" # Add node if specified if [ -n "$NODE" ]; then cmd="$cmd --node $NODE" fi # Add Cloud-Init configuration if [ -n "$CIUSER" ]; then cmd="$cmd --ciuser $CIUSER" fi if [ -n "$SSHKEY" ]; then cmd="$cmd --sshkey \"$SSHKEY\"" fi if [ -n "$IPCONFIG" ]; then cmd="$cmd --ipconfig \"$IPCONFIG\"" fi if [ -n "$NAMESERVER" ]; then cmd="$cmd --nameserver \"$NAMESERVER\"" fi if [ -n "$SEARCHDOMAIN" ]; then cmd="$cmd --searchdomain \"$SEARCHDOMAIN\"" fi if [ -n "$DESCRIPTION" ]; then cmd="$cmd --description \"$DESCRIPTION\"" fi if [ -n "$TAGS" ]; then cmd="$cmd --tags \"$TAGS\"" fi log_info "Executing: $cmd" echo "" # Execute the main script eval "$cmd" if [ $? -ne 0 ]; then log_error "Failed to create template" exit 1 fi } # Add template metadata add_template_metadata() { log_step "Adding template metadata" local metadata_desc if [ -n "$DESCRIPTION" ]; then metadata_desc="$DESCRIPTION" else metadata_desc="Cloud-Init Template - Created $(date +%Y-%m-%d)" fi # Update description qm set $VMID --description "$metadata_desc" # Ensure tags include template if [[ ! "$TAGS" =~ template ]]; then TAGS="template,$TAGS" fi # Update tags qm set $VMID --tags "$TAGS" log_info "✓ Template metadata added" } # Main function main() { echo "=========================================" echo "Create Proxmox Cloud-Init Template" echo "=========================================" echo "" parse_args "$@" validate_args echo "" log_info "Template Configuration:" log_info " VMID: $VMID" log_info " Name: $TEMPLATE_NAME" log_info " Image: $IMAGE" log_info " Storage: $STORAGE" log_info " Memory: ${MEMORY}MB (template minimal)" log_info " Cores: $CORES" log_info " Bridge: $BRIDGE" log_info " Cloud-Init User: $CIUSER" [ -n "$SSHKEY" ] && log_info " SSH Key: Configured" [ -n "$DESCRIPTION" ] && log_info " Description: $DESCRIPTION" [ -n "$TAGS" ] && log_info " Tags: $TAGS" echo "" read -p "Continue with template creation? (y/N): " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Aborted by user" exit 0 fi echo "" # Create template create_template # Add metadata add_template_metadata # Verify template if [ "$SKIP_VERIFICATION" = false ]; then echo "" verify_template fi # Test clone if verification enabled if [ "$SKIP_VERIFICATION" = false ]; then echo "" read -p "Test template by creating a temporary clone? (Y/n): " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Nn]$ ]]; then test_template_clone fi fi echo "" log_info "=========================================" log_info "Template Creation Complete!" log_info "=========================================" echo "" log_info "Template Details:" qm config $VMID | head -20 echo "" log_info "Clone template with:" log_info " qm clone $VMID --name \"\"" echo "" log_info "Full clone:" log_info " qm clone $VMID --full --name \"\"" echo "" log_info "After cloning, configure Cloud-Init:" log_info " qm set --ciuser $CIUSER" log_info " qm set --sshkey \"\"" log_info " qm set --ipconfig0 ip=/24,gw=" } # Run main function main "$@"