Created
May 6, 2026 03:32
-
-
Save hunzo/86e669d75717d04f97246261882fd680 to your computer and use it in GitHub Desktop.
proxmox image cloud-init
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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