#!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # 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 # Disable 'unbound variable' errors for this library (restored at end) _OLD_SET_STATE=$(set +o | grep -E 'set -(e|u|o)') set +u 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}" 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 } # ============================================================================== # SECTION 3: HELPER FUNCTIONS # ============================================================================== # ------------------------------------------------------------------------------ # _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-) # $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 # Add SSH keys only if explicitly provided (not auto-imported from host) if [ -n "${CLOUDINIT_SSH_KEYS:-}" ] && [ -f "$CLOUDINIT_SSH_KEYS" ]; then qm set "$vmid" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null 2>&1 || true _ci_msg_info "SSH keys imported from: $CLOUDINIT_SSH_KEYS" 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" < 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 # Restore previous shell options if they were saved if [ -n "${_OLD_SET_STATE:-}" ]; then eval "$_OLD_SET_STATE" fi # ============================================================================== # SECTION 7: EXAMPLES & DOCUMENTATION # ============================================================================== : <<'EXAMPLES' # 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