Skip to content

Instantly share code, notes, and snippets.

@hunzo
Created May 6, 2026 03:32
Show Gist options
  • Select an option

  • Save hunzo/86e669d75717d04f97246261882fd680 to your computer and use it in GitHub Desktop.

Select an option

Save hunzo/86e669d75717d04f97246261882fd680 to your computer and use it in GitHub Desktop.
proxmox image cloud-init
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "${DEBUG:-0}" == "1" ]]; then
set -x
fi
VMID="${VMID:-9001}"
STORAGE="${STORAGE:-local-lvm}"
RELEASE="${RELEASE:-noble}"
IMAGE_SIZE="${IMAGE_SIZE:-32G}"
IMAGE_DIR="${IMAGE_DIR:-/var/lib/vz/template/iso}"
SNIPPET_DIR="${SNIPPET_DIR:-/var/lib/vz/snippets}"
SNIPPET_STORAGE="${SNIPPET_STORAGE:-local}"
SNIPPET_NAME="${SNIPPET_NAME:-ubuntu.yaml}"
SSH_KEY_FILE="${SSH_KEY_FILE:-./id_ed25519.pub}"
VM_NAME="${VM_NAME:-ubuntu-${RELEASE}-template}"
NET0="${NET0:-virtio,bridge=vmbr0,mtu=1}"
TAGS="${TAGS:-${RELEASE},cloudinit,ansible}"
IMAGE_TEMPLATE="${IMAGE_TEMPLATE:-${IMAGE_DIR}/${RELEASE}-server-cloudimg-amd64.img}"
IMAGE_URL="${IMAGE_URL:-https://cloud-images.ubuntu.com/${RELEASE}/current/${RELEASE}-server-cloudimg-amd64.img}"
SNIPPET_PATH="${SNIPPET_DIR}/${SNIPPET_NAME}"
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Missing required command: ${command_name}" >&2
exit 1
fi
}
require_file() {
local file_path="$1"
local description="$2"
if [[ ! -f "$file_path" ]]; then
echo "Missing ${description}: ${file_path}" >&2
exit 1
fi
}
download_file() {
local source_url="$1"
local destination_path="$2"
if command -v curl >/dev/null 2>&1; then
curl -fL --retry 3 -o "$destination_path" "$source_url"
return
fi
if command -v wget >/dev/null 2>&1; then
wget -O "$destination_path" "$source_url"
return
fi
echo "Missing required command: curl or wget" >&2
exit 1
}
ensure_cloud_image() {
if [[ -f "$IMAGE_TEMPLATE" ]]; then
return
fi
local partial_image="${IMAGE_TEMPLATE}.part"
echo "Cloud image not found: ${IMAGE_TEMPLATE}" >&2
echo "Downloading cloud image from: ${IMAGE_URL}" >&2
mkdir -p "$IMAGE_DIR"
rm -f "$partial_image"
download_file "$IMAGE_URL" "$partial_image"
mv "$partial_image" "$IMAGE_TEMPLATE"
}
prompt_for_username() {
local username
read -r -p "Username: " username
if [[ -z "$username" ]]; then
echo "Username is required." >&2
exit 1
fi
printf '%s' "$username"
}
prompt_for_password() {
if [[ -n "${CLEARTEXT_PASSWORD:-}" ]]; then
printf '%s' "$CLEARTEXT_PASSWORD"
return
fi
local password
read -r -s -p "Password: " password
printf '\n' >&2
if [[ -z "$password" ]]; then
echo "Password is required." >&2
exit 1
fi
printf '%s' "$password"
}
apt_sources_file() {
if [[ "$RELEASE" == "noble" ]]; then
printf '%s' "/etc/apt/sources.list.d/ubuntu.sources"
else
printf '%s' "/etc/apt/sources.list"
fi
}
write_cloud_init_snippet() {
local sources_file
sources_file="$(apt_sources_file)"
mkdir -p "$SNIPPET_DIR"
cat > "$SNIPPET_PATH" <<EOF
#cloud-config
runcmd:
- sed -i 's/archive.ubuntu.com/mirror.kku.ac.th/g' ${sources_file}
- sed -i 's/security.ubuntu.com/mirror.kku.ac.th/g' ${sources_file}
- echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
- apt-get update -y
- apt-get upgrade -y
- apt-get install -y qemu-guest-agent
- apt-get install -y bash-completion
- systemctl enable ssh
- reboot
# Taken from https://forum.proxmox.com/threads/combining-custom-cloud-init-with-auto-generated.59008/page-3#post-428772
EOF
}
create_vm() {
local username="$1"
local password_hash="$2"
qemu-img resize "$IMAGE_TEMPLATE" "$IMAGE_SIZE"
qm create "$VMID" --name "$VM_NAME" \
--memory 8192 --balloon 0 \
--agent 1 \
--sockets 2 \
--cores 2 \
--net0 "$NET0" \
--scsihw virtio-scsi-pci
qm set "$VMID" --scsi0 "${STORAGE}:0,import-from=${IMAGE_TEMPLATE},ssd=1"
qm set "$VMID" --boot order=scsi0
qm set "$VMID" --ide2 "${STORAGE}:cloudinit"
qm set "$VMID" --cicustom "vendor=${SNIPPET_STORAGE}:snippets/${SNIPPET_NAME}"
qm set "$VMID" --tags "$TAGS"
qm set "$VMID" --ciupgrade 0
qm set "$VMID" --ciuser "$username"
qm set "$VMID" --cipassword "$password_hash"
qm set "$VMID" --sshkeys "$SSH_KEY_FILE"
qm set "$VMID" --ipconfig0 ip=dhcp
qm template "$VMID"
}
main() {
require_command openssl
require_command qm
require_command qemu-img
ensure_cloud_image
require_file "$SSH_KEY_FILE" "SSH public key"
local username
local password
local password_hash
username="$(prompt_for_username)"
password="$(prompt_for_password)"
password_hash="$(openssl passwd -6 "$password")"
write_cloud_init_snippet
create_vm "$username" "$password_hash"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment