Skip to content

Instantly share code, notes, and snippets.

@Hackiri
Last active October 26, 2025 23:11
Show Gist options
  • Save Hackiri/f3709f3173a818c04518e604617e5c5a to your computer and use it in GitHub Desktop.
Save Hackiri/f3709f3173a818c04518e604617e5c5a to your computer and use it in GitHub Desktop.
create cloud init template proxmox
#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
# Disabled to allow script to continue on individual image failures
# set -e
# Function to check for required utilities
function check_utilities() {
local utilities=("qm" "wget" "xz" "sha256sum" "ssh-keygen")
for util in "${utilities[@]}"; do
command -v "$util" >/dev/null 2>&1 || { echo "$util not found. Please install it."; exit 1; }
done
}
# Function to set up SSH keys
function setup_ssh_keys() {
local default_ssh_key_dir="${HOME}/ssh-keys"
local ssh_key_dir="$default_ssh_key_dir"
while true; do
# Ensure the SSH key directory exists
if [[ ! -d "${ssh_key_dir}" ]]; then
echo "SSH key directory not found at ${ssh_key_dir}. Creating directory..."
mkdir -p "${ssh_key_dir}"
chmod 700 "${ssh_key_dir}"
fi
# Determine the SSH public key file
ssh_keyfile=$(ls "${ssh_key_dir}"/*.pub 2>/dev/null | head -n 1)
if [[ -z "${ssh_keyfile}" ]]; then
echo "No SSH public key found in ${ssh_key_dir}."
read -p "Do you want to generate a new SSH key pair in this directory? (yes/no): " generate_key
if [[ "${generate_key}" == "yes" ]]; then
ssh-keygen -t rsa -b 4096 -f "${ssh_key_dir}/id_rsa" -N ""
ssh_keyfile="${ssh_key_dir}/id_rsa.pub"
chmod 600 "${ssh_key_dir}/id_rsa"
chmod 644 "${ssh_key_dir}/id_rsa.pub"
echo "SSH key pair generated at ${ssh_key_dir}/id_rsa and ${ssh_keyfile}."
break
else
read -p "Do you want to specify a different directory for your SSH keys? (yes/no): " change_dir
if [[ "${change_dir}" == "yes" ]]; then
read -p "Please enter the full path to your SSH key directory: " ssh_key_dir
else
echo "Cannot proceed without an SSH public key. Exiting."
exit 1
fi
fi
else
echo "Using existing SSH public key: ${ssh_keyfile}"
break
fi
done
# Set permissions on the SSH key files and directory
chmod 700 "${ssh_key_dir}"
chmod 600 "${ssh_key_dir}"/id_* 2>/dev/null || true
chmod 644 "${ssh_key_dir}"/*.pub 2>/dev/null || true
}
# Function to create cloud-init vendor config for qemu-guest-agent installation
function create_vendor_config() {
local vm_id="$1"
local snippets_dir="/var/lib/vz/snippets"
local vendor_file="${snippets_dir}/${vm_id}-vendor.yaml"
# Ensure snippets directory exists
mkdir -p "${snippets_dir}"
# Create vendor config that installs qemu-guest-agent on first boot
cat > "${vendor_file}" << 'EOF'
#cloud-config
# Vendor config - runs on first boot only
package_update: true
package_upgrade: false
packages:
- qemu-guest-agent
runcmd:
- systemctl enable qemu-guest-agent
- systemctl start qemu-guest-agent
EOF
echo "${vendor_file}"
}
# Function to create or update a template
# Args:
# $1: VM ID
# $2: VM Name
# $3: Image file name
function create_template() {
local vm_id="$1"
local vm_name="$2"
local image_file="$3"
echo "Processing template ${vm_name} (${vm_id})"
# Compute the checksum of the image file
local image_checksum
image_checksum=$(sha256sum "${image_file}" | awk '{print $1}')
local checksum_file="checksums/${vm_id}.sha256"
# Check if the template already exists
if qm status "${vm_id}" &>/dev/null; then
echo "Template with VM ID ${vm_id} already exists."
# Check if a checksum file exists
if [[ -f "${checksum_file}" ]]; then
local stored_checksum
stored_checksum=$(cat "${checksum_file}")
echo "Comparing current image checksum with stored checksum..."
if [[ "${image_checksum}" == "${stored_checksum}" ]]; then
echo "Template is up to date. Skipping template creation."
# Remove the image file if it was downloaded
rm -f "${image_file}"
return
else
echo "Image checksum has changed. Deleting and updating template..."
# Delete the existing template and its disks
qm destroy "${vm_id}" --destroy-unreferenced-disks yes
fi
else
echo "No stored checksum found for VM ID ${vm_id}. Deleting and updating template..."
# Delete the existing template and its disks
qm destroy "${vm_id}" --destroy-unreferenced-disks yes
fi
else
echo "Template or checksum file does not exist. Proceeding to download and create/update template."
fi
# Download the image
echo "Downloading ${image_file}..."
if ! wget -N "${image_url}"; then
echo "ERROR: Failed to download ${image_url}"
return 1
fi
# If the image is compressed, decompress it
if [[ "${image_file}" == *.xz ]]; then
local decompressed_file="${image_file%.xz}"
if [[ ! -f "${decompressed_file}" || "${image_file}" -nt "${decompressed_file}" ]]; then
echo "Decompressing ${image_file}..."
xz -d -v -f "${image_file}"
image_file="${decompressed_file}"
else
echo "Decompressed file ${decompressed_file} is up to date."
image_file="${decompressed_file}"
fi
fi
# Ensure the image file exists and is not empty
if [[ ! -s "${image_file}" ]]; then
echo "ERROR: Downloaded image file ${image_file} is missing or empty."
return 1
fi
# Compute the checksum of the new image file
local image_checksum
image_checksum=$(sha256sum "${image_file}" | awk '{print $1}')
# Check if template exists and delete if necessary
if qm status "${vm_id}" &>/dev/null; then
echo "Deleting existing template with VM ID ${vm_id}..."
qm destroy "${vm_id}" --destroy-unreferenced-disks yes
fi
echo "Creating template ${vm_name} (${vm_id})"
# Create new VM
qm create "${vm_id}" --name "${vm_name}" --ostype l26
# Set networking to default bridge
qm set "${vm_id}" --net0 virtio,bridge=vmbr0
# Set display to serial
qm set "${vm_id}" --serial0 socket --vga serial0
# Set memory, CPU, and type defaults
qm set "${vm_id}" --memory 1024 --cores 4 --cpu host
# Import the disk
qm set "${vm_id}" --scsi0 "${storage}:0,import-from=${PWD}/${image_file},discard=on"
# Set SCSI hardware as default boot disk using virtio SCSI single
qm set "${vm_id}" --boot order=scsi0 --scsihw virtio-scsi-single
# Enable QEMU guest agent
qm set "${vm_id}" --agent enabled=1,fstrim_cloned_disks=1
# Add cloud-init device
qm set "${vm_id}" --ide2 "${storage}:cloudinit"
# Set cloud-init network configuration
qm set "${vm_id}" --ipconfig0 "ip=dhcp,ip6=auto"
# Import the SSH keyfile
if [[ -f "${ssh_keyfile}" ]]; then
qm set "${vm_id}" --sshkeys "${ssh_keyfile}"
else
echo "ERROR: SSH key file not found at ${ssh_keyfile}"
return 1
fi
# Add the user
qm set "${vm_id}" --ciuser "${username}"
# Configure cloud-init vendor config to install qemu-guest-agent on first boot
echo "Configuring cloud-init vendor config for qemu-guest-agent installation..."
local vendor_file
vendor_file=$(create_vendor_config "${vm_id}")
if [[ -f "${vendor_file}" ]]; then
qm set "${vm_id}" --cicustom "vendor=local:snippets/${vm_id}-vendor.yaml"
echo "✓ Cloud-init vendor config set: qemu-guest-agent will be installed on first boot"
else
echo "WARNING: Failed to create vendor config. Guest agent will need manual installation."
fi
# Resize the disk to 8G
qm disk resize "${vm_id}" scsi0 8G || true
# Convert the VM into a template
qm template "${vm_id}"
# Save the checksum
mkdir -p checksums
echo "${image_checksum}" > "${checksum_file}"
# Remove the image file when done
rm -f "${image_file}"
}
# Check for required utilities
check_utilities
# User-configurable variables
export username="hackiri" # Replace with your desired username
export storage="Pool01" # Replace with your Proxmox storage name (use RBD storage)
# Validate variables
if [[ -z "${username}" || "${username}" == "your_username_here" ]]; then
echo "Please set a valid username in the script."
exit 1
fi
if ! pvesm status | grep -q "^${storage}\s"; then
echo "Storage '${storage}' not found. Please check your Proxmox storage configuration."
exit 1
fi
# Set up SSH keys
setup_ssh_keys
# Array of images to download and create templates from
declare -a images=(
# Format: "VM_ID|VM_NAME|IMAGE_URL"
# ============================================
# DEBIAN
# ============================================
# Debian 11 (Bullseye) - Oldstable
"901|debian-11-template|https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-amd64.qcow2"
# Debian 12 (Bookworm) - Stable
"902|debian-12-template|https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2"
# Debian 13 (Trixie) - Testing
"903|debian-13-template|https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-genericcloud-amd64-daily.qcow2"
# Debian Sid (Unstable)
"909|debian-sid-template|https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-amd64-daily.qcow2"
# ============================================
# UBUNTU
# ============================================
# Ubuntu 20.04 LTS (Focal Fossa) - EOL April 2025
"910|ubuntu-20.04-template|https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img"
# Ubuntu 22.04 LTS (Jammy Jellyfish) - EOL April 2027
"911|ubuntu-22.04-template|https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
# Ubuntu 24.04 LTS (Noble Numbat) - EOL April 2029
"912|ubuntu-24.04-template|https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
# Ubuntu 25.04 (Plucky Puffin) - Latest non-LTS (EOL January 2026)
"913|ubuntu-25.04-template|https://cloud-images.ubuntu.com/releases/25.04/release/ubuntu-25.04-server-cloudimg-amd64.img"
# ============================================
# FEDORA
# ============================================
# Fedora 41
"920|fedora-41-template|https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2"
# Fedora 42 - Latest
"921|fedora-42-template|https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2"
# ============================================
# ROCKY LINUX (RHEL Clone)
# ============================================
# Rocky Linux 8
"930|rocky-8-template|https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud.latest.x86_64.qcow2"
# Rocky Linux 9 - Latest
"931|rocky-9-template|https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2"
# ============================================
# ALMA LINUX (RHEL Clone)
# ============================================
# AlmaLinux 8
"935|almalinux-8-template|https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2"
# AlmaLinux 9 - Latest
"936|almalinux-9-template|https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2"
# ============================================
# ALPINE LINUX
# ============================================
# Alpine Linux 3.19
"940|alpine-3.19-template|https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/cloud/nocloud_alpine-3.19.1-x86_64-bios-cloudinit-r0.qcow2"
# Alpine Linux 3.20 - Latest
"941|alpine-3.20-template|https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/cloud/nocloud_alpine-3.20.3-x86_64-bios-cloudinit-r0.qcow2"
# ============================================
# OPENSUSE
# ============================================
# openSUSE Leap 15.6 (Stable)
"950|opensuse-leap-15.6-template|https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.x86_64-Cloud.qcow2"
# openSUSE Tumbleweed (Rolling)
"951|opensuse-tumbleweed-template|https://download.opensuse.org/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2"
# ============================================
# ARCH LINUX
# ============================================
# Arch Linux (Rolling)
"960|arch-linux-template|https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
# ============================================
# CENTOS STREAM
# ============================================
# CentOS Stream 9
"970|centos-stream-9-template|https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2"
)
# Track statistics
total=0
success=0
failed=0
skipped=0
echo "========================================"
echo "Starting template creation process..."
echo "Total images to process: ${#images[@]}"
echo "========================================"
# Loop through the images array
for entry in "${images[@]}"; do
IFS='|' read -r vm_id vm_name image_url <<< "${entry}"
((total++))
echo ""
echo "[$total/${#images[@]}] Processing ${vm_name}..."
# Check if template already exists and is current
checksum_file="checksums/${vm_id}.sha256"
if qm status "${vm_id}" &>/dev/null && [[ -f "${checksum_file}" ]]; then
echo "Template ${vm_name} (VM ID: ${vm_id}) already exists with checksum. Skipping."
((skipped++))
continue
fi
# Extract the filename from the URL
image_file="${image_url##*/}"
# Download the image with timestamping
echo "Downloading ${image_file}..."
if ! wget -N "${image_url}"; then
echo "ERROR: Failed to download ${image_url}"
((failed++))
continue
fi
# If the image is compressed, decompress it
if [[ "${image_file}" == *.xz ]]; then
decompressed_file="${image_file%.xz}"
if [[ ! -f "${decompressed_file}" || "${image_file}" -nt "${decompressed_file}" ]]; then
echo "Decompressing ${image_file}..."
xz -d -v -f "${image_file}"
image_file="${decompressed_file}"
else
echo "Decompressed file ${decompressed_file} is up to date."
image_file="${decompressed_file}"
fi
fi
# Ensure the image file exists and is not empty
if [[ ! -s "${image_file}" ]]; then
echo "ERROR: Downloaded image file ${image_file} is missing or empty."
((failed++))
continue
fi
# Create or update the template
if create_template "${vm_id}" "${vm_name}" "${image_file}"; then
echo "SUCCESS: Template ${vm_name} created"
((success++))
else
echo "ERROR: Failed to create template ${vm_name}"
((failed++))
# Clean up the image file even if template creation failed
rm -f "${image_file}"
fi
done
# Final cleanup - remove any leftover image files
echo ""
echo "Cleaning up downloaded images..."
rm -f *.qcow2 *.img *.raw 2>/dev/null
echo "Cleanup complete."
echo ""
echo "========================================"
echo "Template Creation Summary"
echo "========================================"
echo "Total processed: $total"
echo "Successful: $success"
echo "Skipped (already exist): $skipped"
echo "Failed: $failed"
echo "========================================"
@Hackiri
Copy link
Author

Hackiri commented Oct 26, 2025

add cloud-init vendor configuration
Installs qemu-guest-agent package
Enables and starts the service
Runs only on first boot (vendor config behavior

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment