2025-12-01 13:49:42 +01:00
#!/usr/bin/env bash
2026-01-06 13:28:12 +01:00
# Copyright (c) 2021-2026 community-scripts ORG
2025-12-01 13:49:42 +01:00
# Author: community-scripts ORG
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
# Revision: 1
# ==============================================================================
# CLOUD-INIT.FUNC - VM CLOUD-INIT CONFIGURATION LIBRARY
# ==============================================================================
#
# Universal helper library for Cloud-Init configuration in Proxmox VMs.
# Provides functions for:
#
# - Native Proxmox Cloud-Init setup (user, password, network, SSH keys)
# - Interactive configuration dialogs (whiptail)
# - IP address retrieval via qemu-guest-agent
# - Cloud-Init status monitoring and waiting
#
# Usage:
# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/cloud-init.func)
# setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes"
#
# Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions
# ==============================================================================
# ==============================================================================
# SECTION 1: CONFIGURATION DEFAULTS
# ==============================================================================
# These can be overridden before sourcing this library
2026-02-05 08:11:13 +01:00
# Disable 'unbound variable' errors for this library (restored at end)
_OLD_SET_STATE = $( set +o | grep -E 'set -(e|u|o)' )
set +u
2025-12-01 13:49:42 +01:00
CLOUDINIT_DEFAULT_USER = " ${ CLOUDINIT_DEFAULT_USER :- root } "
CLOUDINIT_DNS_SERVERS = " ${ CLOUDINIT_DNS_SERVERS :- 1 .1.1.1 8.8.8.8 } "
CLOUDINIT_SEARCH_DOMAIN = " ${ CLOUDINIT_SEARCH_DOMAIN :- local } "
2026-02-05 08:11:13 +01:00
CLOUDINIT_SSH_KEYS = " ${ CLOUDINIT_SSH_KEYS :- } " # Empty by default - user must explicitly provide keys
# ==============================================================================
# SECTION 2: SSH KEY DISCOVERY AND SELECTION
# ==============================================================================
# ------------------------------------------------------------------------------
# _ci_ssh_extract_keys_from_file - Extracts valid SSH public keys from a file
# ------------------------------------------------------------------------------
function _ci_ssh_extract_keys_from_file( ) {
local file = " $1 "
[ [ -f " $file " && -r " $file " ] ] || return 0
grep -E '^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-)' " $file " 2>/dev/null || true
}
# ------------------------------------------------------------------------------
# _ci_ssh_discover_files - Scans standard paths for SSH keys
# ------------------------------------------------------------------------------
function _ci_ssh_discover_files( ) {
local -a cand = ( )
shopt -s nullglob
cand += ( /root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
cand += ( /root/.ssh/*.pub)
cand += ( /etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
shopt -u nullglob
printf '%s\0' " ${ cand [@] } "
}
# ------------------------------------------------------------------------------
# _ci_ssh_build_choices - Builds whiptail checklist from SSH key files
#
# Sets: CI_SSH_CHOICES (array), CI_SSH_COUNT (int), CI_SSH_MAPFILE (path)
# ------------------------------------------------------------------------------
function _ci_ssh_build_choices( ) {
local -a files = ( " $@ " )
CI_SSH_CHOICES = ( )
CI_SSH_COUNT = 0
CI_SSH_MAPFILE = " $( mktemp) "
local id key typ fp cmt base
for f in " ${ files [@] } " ; do
[ [ -f " $f " && -r " $f " ] ] || continue
base = " $( basename -- " $f " ) "
# Skip known_hosts and private keys
case " $base " in
known_hosts | known_hosts.* | config) continue ; ;
id_*) [ [ " $f " != *.pub ] ] && continue ; ;
esac
while IFS = read -r key; do
[ [ -n " $key " ] ] || continue
typ = ""
fp = ""
cmt = ""
read -r _typ _b64 _cmt <<< " $key "
typ = " ${ _typ :- key } "
cmt = " ${ _cmt :- } "
# Get fingerprint via ssh-keygen if available
if command -v ssh-keygen >/dev/null 2>& 1; then
fp = " $( printf '%s\n' " $key " | ssh-keygen -lf - 2>/dev/null | awk '{print $2}' ) "
fi
# Shorten long comments
[ [ ${# cmt } -gt 40 ] ] && cmt = " ${ cmt : 0 : 37 } ... "
CI_SSH_COUNT = $(( CI_SSH_COUNT + 1 ))
id = " K ${ CI_SSH_COUNT } "
echo " ${ id } | ${ key } " >>" $CI_SSH_MAPFILE "
CI_SSH_CHOICES += ( " $id " " [ $typ ] ${ fp : + $fp } ${ cmt : + $cmt } — ${ base } " "OFF" )
done < <( _ci_ssh_extract_keys_from_file " $f " )
done
}
# ------------------------------------------------------------------------------
# configure_cloudinit_ssh_keys - Interactive SSH key selection for Cloud-Init
#
# Usage: configure_cloudinit_ssh_keys
# Sets: CLOUDINIT_SSH_KEYS (path to temporary file with selected keys)
# ------------------------------------------------------------------------------
function configure_cloudinit_ssh_keys( ) {
local backtitle = "Proxmox VE Helper Scripts"
local ssh_key_mode
# Create temp file for selected keys
CLOUDINIT_SSH_KEYS_TEMP = " $( mktemp) "
: >" $CLOUDINIT_SSH_KEYS_TEMP "
# Discover keys and build choices
IFS = $'\0' read -r -d '' -a _def_files < <( _ci_ssh_discover_files && printf '\0' )
_ci_ssh_build_choices " ${ _def_files [@] } "
local default_key_count = " $CI_SSH_COUNT "
if [ [ " $default_key_count " -gt 0 ] ] ; then
ssh_key_mode = $( whiptail --backtitle " $backtitle " --title "SSH KEY SOURCE" --menu \
"Provision SSH keys for Cloud-Init VM:" 14 72 4 \
"found" " Select from detected keys ( ${ default_key_count } ) " \
"manual" "Paste a single public key" \
"folder" "Scan another folder (path or glob)" \
"none" "No SSH keys (password auth only)" 3>& 1 1>& 2 2>& 3) || return 1
else
ssh_key_mode = $( whiptail --backtitle " $backtitle " --title "SSH KEY SOURCE" --menu \
"No host keys detected. Choose:" 12 72 3 \
"manual" "Paste a single public key" \
"folder" "Scan another folder (path or glob)" \
"none" "No SSH keys (password auth only)" 3>& 1 1>& 2 2>& 3) || return 1
fi
case " $ssh_key_mode " in
found)
# Show checklist with individual keys
local selection
selection = $( whiptail --backtitle " $backtitle " --title "SELECT SSH KEYS" \
--checklist "Select one or more keys to import:" 20 140 10 " ${ CI_SSH_CHOICES [@] } " 3>& 1 1>& 2 2>& 3) || return 1
for tag in $selection ; do
tag = " ${ tag % \" } "
tag = " ${ tag # \" } "
local line
line = $( grep -E " ^ ${ tag } \| " " $CI_SSH_MAPFILE " | head -n1 | cut -d'|' -f2-)
[ [ -n " $line " ] ] && printf '%s\n' " $line " >>" $CLOUDINIT_SSH_KEYS_TEMP "
done
local imported
imported = $( wc -l <" $CLOUDINIT_SSH_KEYS_TEMP " )
echo -e " ${ ROOTSSH :- 🔑 } ${ BOLD } ${ DGN } SSH Keys: ${ BGN } ${ imported } key(s) selected ${ CL } "
; ;
manual)
local pubkey
pubkey = $( whiptail --backtitle " $backtitle " --title "PASTE SSH PUBLIC KEY" \
--inputbox "Paste your SSH public key (ssh-rsa, ssh-ed25519, etc.):" 10 76 3>& 1 1>& 2 2>& 3) || return 1
if [ [ -n " $pubkey " ] ] ; then
echo " $pubkey " >" $CLOUDINIT_SSH_KEYS_TEMP "
echo -e " ${ ROOTSSH :- 🔑 } ${ BOLD } ${ DGN } SSH Keys: ${ BGN } 1 key added manually ${ CL } "
else
echo -e " ${ ROOTSSH :- 🔑 } ${ BOLD } ${ DGN } SSH Keys: ${ BGN } none (empty input) ${ CL } "
CLOUDINIT_SSH_KEYS = ""
rm -f " $CLOUDINIT_SSH_KEYS_TEMP " " $CI_SSH_MAPFILE " 2>/dev/null
return 0
fi
; ;
folder)
local glob_path
glob_path = $( whiptail --backtitle " $backtitle " --title "SCAN FOLDER/GLOB" \
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub):" 10 72 3>& 1 1>& 2 2>& 3) || return 1
if [ [ -n " $glob_path " ] ] ; then
shopt -s nullglob
local -a _scan_files = ( $glob_path )
shopt -u nullglob
if [ [ " ${# _scan_files [@] } " -gt 0 ] ] ; then
_ci_ssh_build_choices " ${ _scan_files [@] } "
if [ [ " $CI_SSH_COUNT " -gt 0 ] ] ; then
local folder_selection
folder_selection = $( whiptail --backtitle " $backtitle " --title "SELECT FOLDER KEYS" \
--checklist "Select key(s) to import:" 20 140 10 " ${ CI_SSH_CHOICES [@] } " 3>& 1 1>& 2 2>& 3) || return 1
for tag in $folder_selection ; do
tag = " ${ tag % \" } "
tag = " ${ tag # \" } "
local line
line = $( grep -E " ^ ${ tag } \| " " $CI_SSH_MAPFILE " | head -n1 | cut -d'|' -f2-)
[ [ -n " $line " ] ] && printf '%s\n' " $line " >>" $CLOUDINIT_SSH_KEYS_TEMP "
done
local imported
imported = $( wc -l <" $CLOUDINIT_SSH_KEYS_TEMP " )
echo -e " ${ ROOTSSH :- 🔑 } ${ BOLD } ${ DGN } SSH Keys: ${ BGN } ${ imported } key(s) from folder ${ CL } "
else
whiptail --backtitle " $backtitle " --msgbox " No keys found in: $glob_path " 8 60
fi
else
whiptail --backtitle " $backtitle " --msgbox "Path/glob returned no files." 8 60
fi
fi
; ;
none | *)
echo -e " ${ ROOTSSH :- 🔑 } ${ BOLD } ${ DGN } SSH Keys: ${ BGN } none (password auth only) ${ CL } "
CLOUDINIT_SSH_KEYS = ""
rm -f " $CLOUDINIT_SSH_KEYS_TEMP " " $CI_SSH_MAPFILE " 2>/dev/null
return 0
; ;
esac
# Cleanup mapfile
rm -f " $CI_SSH_MAPFILE " 2>/dev/null
# Set the variable for setup_cloud_init to use
if [ [ -s " $CLOUDINIT_SSH_KEYS_TEMP " ] ] ; then
CLOUDINIT_SSH_KEYS = " $CLOUDINIT_SSH_KEYS_TEMP "
else
CLOUDINIT_SSH_KEYS = ""
rm -f " $CLOUDINIT_SSH_KEYS_TEMP "
fi
return 0
}
2025-12-01 13:49:42 +01:00
# ==============================================================================
2026-02-05 08:11:13 +01:00
# SECTION 3: HELPER FUNCTIONS
2025-12-01 13:49:42 +01:00
# ==============================================================================
# ------------------------------------------------------------------------------
# _ci_msg - Internal message helper with fallback
# ------------------------------------------------------------------------------
function _ci_msg_info( ) { msg_info " $1 " 2>/dev/null || echo " [INFO] $1 " ; }
function _ci_msg_ok( ) { msg_ok " $1 " 2>/dev/null || echo " [OK] $1 " ; }
function _ci_msg_warn( ) { msg_warn " $1 " 2>/dev/null || echo " [WARN] $1 " ; }
function _ci_msg_error( ) { msg_error " $1 " 2>/dev/null || echo " [ERROR] $1 " ; }
# ------------------------------------------------------------------------------
# validate_ip_cidr - Validate IP address in CIDR format
# Usage: validate_ip_cidr "192.168.1.100/24" && echo "Valid"
# Returns: 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
function validate_ip_cidr( ) {
local ip_cidr = " $1 "
# Match: 0-255.0-255.0-255.0-255/0-32
if [ [ " $ip_cidr " = ~ ^( [ 0-9] { 1,3} \. ) { 3} [ 0-9] { 1,3} /( [ 0-9] | [ 1-2] [ 0-9] | 3[ 0-2] ) $ ] ] ; then
# Validate each octet is 0-255
local ip = " ${ ip_cidr %/* } "
IFS = '.' read -ra octets <<< " $ip "
for octet in " ${ octets [@] } " ; do
( ( octet > 255) ) && return 1
done
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# validate_ip - Validate plain IP address (no CIDR)
# Usage: validate_ip "192.168.1.1" && echo "Valid"
# ------------------------------------------------------------------------------
function validate_ip( ) {
local ip = " $1 "
if [ [ " $ip " = ~ ^( [ 0-9] { 1,3} \. ) { 3} [ 0-9] { 1,3} $ ] ] ; then
IFS = '.' read -ra octets <<< " $ip "
for octet in " ${ octets [@] } " ; do
( ( octet > 255) ) && return 1
done
return 0
fi
return 1
}
# ==============================================================================
# SECTION 3: MAIN CLOUD-INIT FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# setup_cloud_init - Configures Proxmox Native Cloud-Init
# ------------------------------------------------------------------------------
# Parameters:
# $1 - VMID (required)
# $2 - Storage name (required)
# $3 - Hostname (optional, default: vm-<vmid>)
# $4 - Enable Cloud-Init (yes/no, default: no)
# $5 - User (optional, default: root)
# $6 - Network mode (dhcp/static, default: dhcp)
# $7 - Static IP (optional, format: 192.168.1.100/24)
# $8 - Gateway (optional)
# $9 - Nameservers (optional, default: 1.1.1.1 8.8.8.8)
#
# Returns: 0 on success, 1 on failure
# Exports: CLOUDINIT_USER, CLOUDINIT_PASSWORD, CLOUDINIT_CRED_FILE
# ==============================================================================
function setup_cloud_init( ) {
local vmid = " $1 "
local storage = " $2 "
local hostname = " ${ 3 :- vm - ${ vmid } } "
local enable = " ${ 4 :- no } "
local ciuser = " ${ 5 :- $CLOUDINIT_DEFAULT_USER } "
local network_mode = " ${ 6 :- dhcp } "
local static_ip = " ${ 7 :- } "
local gateway = " ${ 8 :- } "
local nameservers = " ${ 9 :- $CLOUDINIT_DNS_SERVERS } "
# Skip if not enabled
if [ " $enable " != "yes" ] ; then
return 0
fi
# Validate static IP if provided
if [ " $network_mode " = "static" ] ; then
if [ -n " $static_ip " ] && ! validate_ip_cidr " $static_ip " ; then
_ci_msg_error " Invalid static IP format: $static_ip (expected: x.x.x.x/xx) "
return 1
fi
if [ -n " $gateway " ] && ! validate_ip " $gateway " ; then
_ci_msg_error " Invalid gateway IP format: $gateway "
return 1
fi
fi
_ci_msg_info "Configuring Cloud-Init"
# Create Cloud-Init drive (try ide2 first, then scsi1 as fallback)
if ! qm set " $vmid " --ide2 " ${ storage } :cloudinit " >/dev/null 2>& 1; then
qm set " $vmid " --scsi1 " ${ storage } :cloudinit " >/dev/null 2>& 1
fi
# Set user
qm set " $vmid " --ciuser " $ciuser " >/dev/null
# Generate and set secure random password
local cipassword = $( openssl rand -base64 16)
qm set " $vmid " --cipassword " $cipassword " >/dev/null
2026-02-05 08:11:13 +01:00
# Add SSH keys only if explicitly provided (not auto-imported from host)
if [ -n " ${ CLOUDINIT_SSH_KEYS :- } " ] && [ -f " $CLOUDINIT_SSH_KEYS " ] ; then
2025-12-01 13:49:42 +01:00
qm set " $vmid " --sshkeys " $CLOUDINIT_SSH_KEYS " >/dev/null 2>& 1 || true
2026-02-05 08:11:13 +01:00
_ci_msg_info " SSH keys imported from: $CLOUDINIT_SSH_KEYS "
2025-12-01 13:49:42 +01:00
fi
# Configure network
if [ " $network_mode " = "static" ] && [ -n " $static_ip " ] && [ -n " $gateway " ] ; then
qm set " $vmid " --ipconfig0 " ip= ${ static_ip } ,gw= ${ gateway } " >/dev/null
else
qm set " $vmid " --ipconfig0 "ip=dhcp" >/dev/null
fi
# Set DNS servers
qm set " $vmid " --nameserver " $nameservers " >/dev/null
# Set search domain
qm set " $vmid " --searchdomain " $CLOUDINIT_SEARCH_DOMAIN " >/dev/null
# Enable package upgrades on first boot (if supported by Proxmox version)
qm set " $vmid " --ciupgrade 1 >/dev/null 2>& 1 || true
# Save credentials to file (with restrictive permissions)
local cred_file = " /tmp/ ${ hostname } - ${ vmid } -cloud-init-credentials.txt "
umask 077
cat >" $cred_file " <<EOF
╔══════════════════════════════════════════════════════════════════╗
║ ⚠️ SECURITY WARNING: DELETE THIS FILE AFTER NOTING CREDENTIALS ║
╚══════════════════════════════════════════════════════════════════╝
Cloud-Init Credentials
────────────────────────────────────────
VM ID: ${ vmid }
Hostname: ${ hostname }
Created: $( date)
Username: ${ ciuser }
Password: ${ cipassword }
Network: ${ network_mode } $( [ " $network_mode " = "static" ] && echo " (IP: ${ static_ip } , GW: ${ gateway } ) " || echo " (DHCP)" )
DNS: ${ nameservers }
────────────────────────────────────────
SSH Access ( if keys configured) :
ssh ${ ciuser } @<vm-ip>
Proxmox UI Configuration:
VM ${ vmid } > Cloud-Init > Edit
- User, Password, SSH Keys
- Network ( IP Config)
- DNS, Search Domain
────────────────────────────────────────
🗑️ To delete this file:
rm -f ${ cred_file }
────────────────────────────────────────
EOF
chmod 600 " $cred_file "
_ci_msg_ok " Cloud-Init configured (User: ${ ciuser } ) "
# Export for use in calling script (DO NOT display password here - will be shown in summary)
export CLOUDINIT_USER = " $ciuser "
export CLOUDINIT_PASSWORD = " $cipassword "
export CLOUDINIT_CRED_FILE = " $cred_file "
return 0
}
# ==============================================================================
# SECTION 4: INTERACTIVE CONFIGURATION
# ==============================================================================
# ------------------------------------------------------------------------------
# configure_cloud_init_interactive - Whiptail dialog for Cloud-Init setup
# ------------------------------------------------------------------------------
# Prompts user for Cloud-Init configuration choices
# Returns configuration via exported variables:
# - CLOUDINIT_ENABLE (yes/no)
# - CLOUDINIT_USER
# - CLOUDINIT_NETWORK_MODE (dhcp/static)
# - CLOUDINIT_IP (if static)
# - CLOUDINIT_GW (if static)
# - CLOUDINIT_DNS
# ------------------------------------------------------------------------------
function configure_cloud_init_interactive( ) {
local default_user = " ${ 1 :- root } "
# Check if whiptail is available
if ! command -v whiptail >/dev/null 2>& 1; then
echo "Warning: whiptail not available, skipping interactive configuration"
export CLOUDINIT_ENABLE = "no"
return 1
fi
# Ask if user wants to enable Cloud-Init
if ! ( whiptail --backtitle "Proxmox VE Helper Scripts" --title "CLOUD-INIT" \
--yesno "Enable Cloud-Init for VM configuration?\n\nCloud-Init allows automatic configuration of:\n• User accounts and passwords\n• SSH keys\n• Network settings (DHCP/Static)\n• DNS configuration\n\nYou can also configure these settings later in Proxmox UI." 16 68) ; then
export CLOUDINIT_ENABLE = "no"
return 0
fi
export CLOUDINIT_ENABLE = "yes"
# Username
if CLOUDINIT_USER = $( whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"Cloud-Init Username" 8 58 " $default_user " --title "USERNAME" 3>& 1 1>& 2 2>& 3) ; then
export CLOUDINIT_USER = " ${ CLOUDINIT_USER :- $default_user } "
else
export CLOUDINIT_USER = " $default_user "
fi
# Network configuration
if ( whiptail --backtitle "Proxmox VE Helper Scripts" --title "NETWORK MODE" \
--yesno "Use DHCP for network configuration?\n\nSelect 'No' for static IP configuration." 10 58) ; then
export CLOUDINIT_NETWORK_MODE = "dhcp"
else
export CLOUDINIT_NETWORK_MODE = "static"
# Static IP with validation
while true; do
if CLOUDINIT_IP = $( whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"Static IP Address (CIDR format)\nExample: 192.168.1.100/24" 9 58 "" --title "IP ADDRESS" 3>& 1 1>& 2 2>& 3) ; then
if validate_ip_cidr " $CLOUDINIT_IP " ; then
export CLOUDINIT_IP
break
else
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID IP" \
--msgbox " Invalid IP format: $CLOUDINIT_IP \n\nPlease use CIDR format: x.x.x.x/xx\nExample: 192.168.1.100/24 " 10 50
fi
else
_ci_msg_warn "Static IP required, falling back to DHCP"
export CLOUDINIT_NETWORK_MODE = "dhcp"
break
fi
done
# Gateway with validation
if [ " $CLOUDINIT_NETWORK_MODE " = "static" ] ; then
while true; do
if CLOUDINIT_GW = $( whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"Gateway IP Address\nExample: 192.168.1.1" 8 58 "" --title "GATEWAY" 3>& 1 1>& 2 2>& 3) ; then
if validate_ip " $CLOUDINIT_GW " ; then
export CLOUDINIT_GW
break
else
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID GATEWAY" \
--msgbox " Invalid gateway format: $CLOUDINIT_GW \n\nPlease use format: x.x.x.x\nExample: 192.168.1.1 " 10 50
fi
else
_ci_msg_warn "Gateway required, falling back to DHCP"
export CLOUDINIT_NETWORK_MODE = "dhcp"
break
fi
done
fi
fi
# DNS Servers
if CLOUDINIT_DNS = $( whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox \
"DNS Servers (space-separated)" 8 58 "1.1.1.1 8.8.8.8" --title "DNS SERVERS" 3>& 1 1>& 2 2>& 3) ; then
export CLOUDINIT_DNS = " ${ CLOUDINIT_DNS :- 1 .1.1.1 8.8.8.8 } "
else
export CLOUDINIT_DNS = "1.1.1.1 8.8.8.8"
fi
return 0
}
# ==============================================================================
# SECTION 5: UTILITY FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# display_cloud_init_info - Show Cloud-Init summary after setup
# ------------------------------------------------------------------------------
function display_cloud_init_info( ) {
local vmid = " $1 "
local hostname = " ${ 2 :- } "
if [ -n " $CLOUDINIT_CRED_FILE " ] && [ -f " $CLOUDINIT_CRED_FILE " ] ; then
if [ -n " ${ INFO :- } " ] ; then
echo -e " \n ${ INFO } ${ BOLD :- } ${ GN :- } Cloud-Init Configuration: ${ CL :- } "
echo -e " ${ TAB :- } ${ DGN :- } User: ${ BGN :- } ${ CLOUDINIT_USER :- root } ${ CL :- } "
echo -e " ${ TAB :- } ${ DGN :- } Password: ${ BGN :- } ${ CLOUDINIT_PASSWORD } ${ CL :- } "
echo -e " ${ TAB :- } ${ DGN :- } Credentials: ${ BL :- } ${ CLOUDINIT_CRED_FILE } ${ CL :- } "
echo -e " ${ TAB :- } ${ RD :- } ⚠️ Delete credentials file after noting password! ${ CL :- } "
echo -e " ${ TAB :- } ${ YW :- } 💡 Configure in Proxmox UI: VM ${ vmid } > Cloud-Init ${ CL :- } "
else
echo ""
echo "[INFO] Cloud-Init Configuration:"
echo " User: ${ CLOUDINIT_USER :- root } "
echo " Password: ${ CLOUDINIT_PASSWORD } "
echo " Credentials: ${ CLOUDINIT_CRED_FILE } "
echo " ⚠️ Delete credentials file after noting password!"
echo " Configure in Proxmox UI: VM ${ vmid } > Cloud-Init "
fi
fi
}
# ------------------------------------------------------------------------------
# cleanup_cloud_init_credentials - Remove credentials file
# ------------------------------------------------------------------------------
# Usage: cleanup_cloud_init_credentials
# Call this after user has noted/saved the credentials
# ------------------------------------------------------------------------------
function cleanup_cloud_init_credentials( ) {
if [ -n " $CLOUDINIT_CRED_FILE " ] && [ -f " $CLOUDINIT_CRED_FILE " ] ; then
rm -f " $CLOUDINIT_CRED_FILE "
_ci_msg_ok " Credentials file removed: $CLOUDINIT_CRED_FILE "
unset CLOUDINIT_CRED_FILE
return 0
fi
return 1
}
# ------------------------------------------------------------------------------
# has_cloud_init - Check if VM has Cloud-Init configured
# ------------------------------------------------------------------------------
function has_cloud_init( ) {
local vmid = " $1 "
qm config " $vmid " 2>/dev/null | grep -qE "(ide2|scsi1):.*cloudinit"
}
# ------------------------------------------------------------------------------
# regenerate_cloud_init - Regenerate Cloud-Init configuration
# ------------------------------------------------------------------------------
function regenerate_cloud_init( ) {
local vmid = " $1 "
if has_cloud_init " $vmid " ; then
_ci_msg_info "Regenerating Cloud-Init configuration"
qm cloudinit update " $vmid " >/dev/null 2>& 1 || true
_ci_msg_ok "Cloud-Init configuration regenerated"
return 0
else
_ci_msg_warn " VM $vmid does not have Cloud-Init configured "
return 1
fi
}
# ------------------------------------------------------------------------------
# get_vm_ip - Get VM IP address via qemu-guest-agent
# ------------------------------------------------------------------------------
function get_vm_ip( ) {
local vmid = " $1 "
local timeout = " ${ 2 :- 30 } "
local elapsed = 0
while [ $elapsed -lt $timeout ] ; do
local vm_ip = $( qm guest cmd " $vmid " network-get-interfaces 2>/dev/null |
jq -r '.[] | select(.name != "lo") | ."ip-addresses"[]? | select(."ip-address-type" == "ipv4") | ."ip-address"' 2>/dev/null | head -1)
if [ -n " $vm_ip " ] ; then
echo " $vm_ip "
return 0
fi
sleep 2
elapsed = $(( elapsed + 2 ))
done
return 1
}
# ------------------------------------------------------------------------------
# wait_for_cloud_init - Wait for Cloud-Init to complete (requires SSH access)
# ------------------------------------------------------------------------------
function wait_for_cloud_init( ) {
local vmid = " $1 "
local timeout = " ${ 2 :- 300 } "
local vm_ip = " ${ 3 :- } "
# Get IP if not provided
if [ -z " $vm_ip " ] ; then
vm_ip = $( get_vm_ip " $vmid " 60)
fi
if [ -z " $vm_ip " ] ; then
_ci_msg_warn "Unable to determine VM IP address"
return 1
fi
_ci_msg_info " Waiting for Cloud-Init to complete on ${ vm_ip } "
local elapsed = 0
while [ $elapsed -lt $timeout ] ; do
if timeout 10 ssh -o ConnectTimeout = 5 -o StrictHostKeyChecking = no -o UserKnownHostsFile = /dev/null \
" ${ CLOUDINIT_USER :- root } @ ${ vm_ip } " "cloud-init status --wait" 2>/dev/null; then
_ci_msg_ok "Cloud-Init completed successfully"
return 0
fi
sleep 10
elapsed = $(( elapsed + 10 ))
done
_ci_msg_warn " Cloud-Init did not complete within ${ timeout } s "
return 1
}
# ==============================================================================
# SECTION 6: EXPORTS
# ==============================================================================
# Export all functions for use in other scripts
export -f setup_cloud_init 2>/dev/null || true
export -f configure_cloud_init_interactive 2>/dev/null || true
export -f display_cloud_init_info 2>/dev/null || true
export -f cleanup_cloud_init_credentials 2>/dev/null || true
export -f has_cloud_init 2>/dev/null || true
export -f regenerate_cloud_init 2>/dev/null || true
export -f get_vm_ip 2>/dev/null || true
export -f wait_for_cloud_init 2>/dev/null || true
export -f validate_ip_cidr 2>/dev/null || true
export -f validate_ip 2>/dev/null || true
2026-02-05 08:11:13 +01:00
# Restore previous shell options if they were saved
if [ -n " ${ _OLD_SET_STATE :- } " ] ; then
eval " $_OLD_SET_STATE "
fi
2025-12-01 13:49:42 +01:00
# ==============================================================================
# SECTION 7: EXAMPLES & DOCUMENTATION
# ==============================================================================
: <<'EXAMPLE S'
# Example 1: Simple DHCP setup (most common)
setup_cloud_init " $VMID " " $STORAGE " " $HN " "yes"
# Example 2: Static IP setup
setup_cloud_init " $VMID " " $STORAGE " "myserver" "yes" "root" "static" "192.168.1.100/24" "192.168.1.1"
# Example 3: Interactive configuration in advanced_settings()
configure_cloud_init_interactive "admin"
if [ " $CLOUDINIT_ENABLE " = "yes" ] ; then
setup_cloud_init " $VMID " " $STORAGE " " $HN " "yes" " $CLOUDINIT_USER " \
" $CLOUDINIT_NETWORK_MODE " " $CLOUDINIT_IP " " $CLOUDINIT_GW " " $CLOUDINIT_DNS "
fi
# Example 4: Display info after VM creation
display_cloud_init_info " $VMID " " $HN "
# Example 5: Check if VM has Cloud-Init
if has_cloud_init " $VMID " ; then
echo "Cloud-Init is configured"
fi
# Example 6: Wait for Cloud-Init to complete after VM start
if [ " $START_VM " = "yes" ] ; then
qm start " $VMID "
sleep 30
wait_for_cloud_init " $VMID " 300
fi
# Example 7: Cleanup credentials file after user has noted password
display_cloud_init_info " $VMID " " $HN "
read -p "Have you saved the credentials? (y/N): " -r
[ [ $REPLY = ~ ^[ Yy] $ ] ] && cleanup_cloud_init_credentials
# Example 8: Validate IP before using
if validate_ip_cidr "192.168.1.100/24" ; then
echo "Valid IP/CIDR"
fi
EXAMPLES