diff --git a/misc/cloud-init.func b/misc/cloud-init.func index c50ae6ff8..3f6f197bf 100644 --- a/misc/cloud-init.func +++ b/misc/cloud-init.func @@ -1,30 +1,93 @@ #!/usr/bin/env bash +# Copyright (c) 2021-2025 community-scripts ORG +# Author: community-scripts ORG +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE +# Revision: 1 # ============================================================================== -# Cloud-Init Library - Universal Helper for all Proxmox VM Scripts +# CLOUD-INIT.FUNC - VM CLOUD-INIT CONFIGURATION LIBRARY # ============================================================================== -# Author: community-scripts ORG -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# +# 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: -# 1. Source this library in your VM script: -# source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/vm/cloud-init-lib.sh) -# -# 2. Call setup_cloud_init with parameters: -# setup_cloud_init "$VMID" "$STORAGE" "$HN" "$USE_CLOUD_INIT" +# source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/cloud-init.func) +# setup_cloud_init "$VMID" "$STORAGE" "$HN" "yes" # # Compatible with: Debian, Ubuntu, and all Cloud-Init enabled distributions # ============================================================================== -# Configuration defaults (can be overridden before sourcing) +# ============================================================================== +# SECTION 1: CONFIGURATION DEFAULTS +# ============================================================================== +# These can be overridden before sourcing this library + 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:-/root/.ssh/authorized_keys}" # ============================================================================== -# Main Setup Function - Configures Proxmox Native Cloud-Init +# SECTION 2: 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) @@ -55,7 +118,19 @@ function setup_cloud_init() { return 0 fi - msg_info "Configuring Cloud-Init" 2>/dev/null || echo "[INFO] Configuring Cloud-Init" + # 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 @@ -90,12 +165,16 @@ function setup_cloud_init() { # 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 + # Save credentials to file (with restrictive permissions) local cred_file="/tmp/${hostname}-${vmid}-cloud-init-credentials.txt" + umask 077 cat >"$cred_file" < + ssh ${ciuser}@ Proxmox UI Configuration: -VM ${vmid} > Cloud-Init > Edit -- User, Password, SSH Keys -- Network (IP Config) -- DNS, Search Domain -======================================== -EOF + VM ${vmid} > Cloud-Init > Edit + - User, Password, SSH Keys + - Network (IP Config) + - DNS, Search Domain - msg_ok "Cloud-Init configured (User: ${ciuser})" 2>/dev/null || echo "[OK] Cloud-Init configured (User: ${ciuser})" +──────────────────────────────────────── +🗑️ 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" @@ -129,8 +213,12 @@ EOF } # ============================================================================== -# Interactive Cloud-Init Configuration (Whiptail/Dialog) +# 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) @@ -139,7 +227,7 @@ EOF # - CLOUDINIT_IP (if static) # - CLOUDINIT_GW (if static) # - CLOUDINIT_DNS -# ============================================================================== +# ------------------------------------------------------------------------------ function configure_cloud_init_interactive() { local default_user="${1:-root}" @@ -174,24 +262,42 @@ function configure_cloud_init_interactive() { else export CLOUDINIT_NETWORK_MODE="static" - # Static IP - 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 - export CLOUDINIT_IP - else - echo "Error: Static IP required for static network mode" - export CLOUDINIT_NETWORK_MODE="dhcp" - fi - - # Gateway - if [ "$CLOUDINIT_NETWORK_MODE" = "static" ]; then - 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 - export CLOUDINIT_GW + # 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 - echo "Error: Gateway required for static network mode" + _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 @@ -207,8 +313,12 @@ function configure_cloud_init_interactive() { } # ============================================================================== -# Display Cloud-Init Summary Information +# 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:-}" @@ -219,48 +329,64 @@ function display_cloud_init_info() { 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:- }${YW:-}💡 You can configure Cloud-Init settings in Proxmox UI:${CL:-}" - echo -e "${TAB:- }${YW:-} VM ${vmid} > Cloud-Init > Edit (User, Password, SSH Keys, Network)${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 " You can configure Cloud-Init settings in Proxmox UI:" - echo " VM ${vmid} > Cloud-Init > Edit" + echo " ⚠️ Delete credentials file after noting password!" + echo " Configure in Proxmox UI: VM ${vmid} > Cloud-Init" fi fi } -# ============================================================================== -# Check if VM has Cloud-Init configured -# ============================================================================== +# ------------------------------------------------------------------------------ +# 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 configuration -# ============================================================================== +# ------------------------------------------------------------------------------ +# regenerate_cloud_init - Regenerate Cloud-Init configuration +# ------------------------------------------------------------------------------ function regenerate_cloud_init() { local vmid="$1" if has_cloud_init "$vmid"; then - msg_info "Regenerating Cloud-Init configuration" 2>/dev/null || echo "[INFO] Regenerating Cloud-Init" + _ci_msg_info "Regenerating Cloud-Init configuration" qm cloudinit update "$vmid" >/dev/null 2>&1 || true - msg_ok "Cloud-Init configuration regenerated" 2>/dev/null || echo "[OK] Cloud-Init regenerated" + _ci_msg_ok "Cloud-Init configuration regenerated" return 0 else - echo "Warning: VM $vmid does not have Cloud-Init configured" + _ci_msg_warn "VM $vmid does not have Cloud-Init configured" return 1 fi } -# ============================================================================== -# Get VM IP address via qemu-guest-agent -# ============================================================================== +# ------------------------------------------------------------------------------ +# get_vm_ip - Get VM IP address via qemu-guest-agent +# ------------------------------------------------------------------------------ function get_vm_ip() { local vmid="$1" local timeout="${2:-30}" @@ -282,9 +408,9 @@ function get_vm_ip() { return 1 } -# ============================================================================== -# Wait for Cloud-Init to complete (requires SSH access) -# ============================================================================== +# ------------------------------------------------------------------------------ +# 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}" @@ -296,40 +422,45 @@ function wait_for_cloud_init() { fi if [ -z "$vm_ip" ]; then - echo "Warning: Unable to determine VM IP address" + _ci_msg_warn "Unable to determine VM IP address" return 1 fi - msg_info "Waiting for Cloud-Init to complete on ${vm_ip}" 2>/dev/null || echo "[INFO] Waiting for Cloud-Init on ${vm_ip}" + _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 - msg_ok "Cloud-Init completed successfully" 2>/dev/null || echo "[OK] Cloud-Init completed" + _ci_msg_ok "Cloud-Init completed successfully" return 0 fi sleep 10 elapsed=$((elapsed + 10)) done - echo "Warning: Cloud-Init did not complete within ${timeout}s" + _ci_msg_warn "Cloud-Init did not complete within ${timeout}s" return 1 } # ============================================================================== -# Export all functions for use in other scripts +# 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 # ============================================================================== -# Quick Start Examples +# SECTION 7: EXAMPLES & DOCUMENTATION # ============================================================================== : <<'EXAMPLES' @@ -361,4 +492,14 @@ if [ "$START_VM" = "yes" ]; then 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