mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-02-05 21:03:24 +01:00
- Add SSH key discovery from standard paths (/root/.ssh, /etc/ssh) - Add whiptail-based interactive key selection dialog - Extract key fingerprints and comments for better identification - Support multiple key selection with checkboxes - Auto-skip private keys and known_hosts files - Restore shell state after library load
709 lines
27 KiB
Bash
709 lines
27 KiB
Bash
#!/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-<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
|
|
|
|
# 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" <<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
|
|
|
|
# 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
|