Skip to content

Instantly share code, notes, and snippets.

@QNimbus
Last active June 27, 2025 12:16
Show Gist options
  • Save QNimbus/a972908f09c2b6fed2b33307d00076f1 to your computer and use it in GitHub Desktop.
Save QNimbus/a972908f09c2b6fed2b33307d00076f1 to your computer and use it in GitHub Desktop.
VM Creation script #proxmox
#!/usr/bin/env bash
# LibVersion: 1.1.4
#
# Proxmox VM management functions for Talos/Kubernetes.
# Relies on:
# - Logging functions (log_info, log_error, etc.) from the main script.
# - VERBOSE_FLAG from the main script.
# - run_* utility functions (run_quiet, etc.) from utils.lib.sh.
# - Default configuration variables (DEFAULT_VM_NAME_PREFIX, CORES, RAM_MB, etc.) from main script.
# - Option variables (CORES_OPT, RAM_MB_OPT, etc.) from main script argument parsing.
usage() {
# Uses SCRIPT_NAME, DEFAULT_VM_NAME_PREFIX, CORES, RAM_MB from the main script's scope
echo ""
echo "Usage: $SCRIPT_NAME <action> [options]"
echo ""
echo "Actions:"
echo " create <VMID> [VM_NAME_SUFFIX] [create_options]"
echo " Creates a new Talos VM."
echo " VMID: A unique numeric ID for the VM (e.g., 9001)."
echo " VM_NAME_SUFFIX (optional): Suffix for VM name (e.g., 'master-1'). Defaults to 'node'."
echo " Full name will be ${DEFAULT_VM_NAME_PREFIX}-<VM_NAME_SUFFIX>"
echo " destroy <VMID[,VMID,...]>"
echo " Stops and destroys existing VM(s). Supports multiple VMIDs separated by commas."
echo " start <VMID[,VMID,...]>"
echo " Starts stopped VM(s). Supports multiple VMIDs separated by commas."
echo " stop <VMID[,VMID,...]>"
echo " Stops running VM(s) (gracefully, then force if needed). Supports multiple VMIDs."
echo " shutdown <VMID[,VMID,...]>"
echo " Attempts to gracefully shut down VM(s) via QEMU guest agent,"
echo " then falls back to stop if agent command fails or times out."
echo " restart <VMID[,VMID,...]>"
echo " Stops and then starts VM(s) (host-level restart). Supports multiple VMIDs."
echo " reboot <VMID[,VMID,...]>"
echo " Attempts to reboot VM(s) via QEMU guest agent (guest OS reboot)."
echo " mount <VMID> --iso=<ISO_NAME> [--storage-iso=<STORAGE>]"
echo " Mounts an ISO image to the VM's IDE2 interface."
echo " unmount <VMID[,VMID,...]>"
echo " Unmounts any ISO image from the VM(s) IDE2 interface."
echo " list-iso"
echo " Lists all available ISO storages and their contents."
echo " update"
echo " Checks for script updates (main script and libraries) and prompts to install."
echo " version | --version"
echo " Shows the script and library versions."
echo ""
echo "Create Options:"
echo " --cores=<N> Number of CPU cores (default: $CORES)"
echo " --sockets=<N> Number of CPU sockets (default: $SOCKETS)"
echo " --ram=<MB> RAM in MB (default: $RAM_MB, min: 512)"
echo " --iso=<ISO_NAME> Specific ISO file to mount (e.g., talos-v1.6.0-amd64.iso)"
echo " --storage-iso=<STORAGE> Storage pool for ISO (defaults to 'local' if --iso is used without it)"
echo " --storage-os=<STORAGE> Storage pool for OS disk (default: $STORAGE_POOL_OS)"
echo " --storage-data=<STORAGE> Storage pool for data disk (default: $STORAGE_POOL_DATA)"
echo " --vlan=<VLAN_ID> VLAN tag for network interface (1-4094)"
echo " --mac-address=<MAC> Specific MAC address for network interface (format: XX:XX:XX:XX:XX:XX)"
echo " --force Delete existing VM with same VMID before creating"
echo " --start Automatically start the VM after creation"
echo ""
echo "Global Options:"
echo " --verbose Show detailed output during operations"
echo ""
echo "Examples:"
echo " $SCRIPT_NAME create 9001 master-1"
echo " $SCRIPT_NAME create 9002 worker-1 --iso=talos-v1.7.0-amd64.iso --cores=2 --ram=8192 --start"
echo " $SCRIPT_NAME create 9003 worker-2 --iso=custom.iso --storage-iso=nfs-iso --vlan=100"
echo " $SCRIPT_NAME create 9004 worker-3 --force --storage-os=local-zfs --storage-data=local-zfs --start"
echo " $SCRIPT_NAME create 9005 worker-4 --mac-address=02:00:00:00:00:01 --vlan=100"
echo " $SCRIPT_NAME destroy 9001"
echo " $SCRIPT_NAME destroy 9001,9002,9003"
echo " $SCRIPT_NAME start 9001,9002"
echo " $SCRIPT_NAME stop 9001,9002,9003"
echo " $SCRIPT_NAME restart 9001,9002"
echo " $SCRIPT_NAME mount 9001 --iso=talos-v1.7.0-amd64.iso"
echo " $SCRIPT_NAME mount 9002 --iso=custom.iso --storage-iso=nfs-iso"
echo " $SCRIPT_NAME unmount 9001"
echo " $SCRIPT_NAME unmount 9001,9002,9003"
echo " $SCRIPT_NAME list-iso"
echo " $SCRIPT_NAME update --verbose"
# This function is typically called with `usage; exit 1` from main script.
}
check_vmid_exists() {
local vmid=$1
if qm status "$vmid" >/dev/null 2>&1; then
return 0 # Exists
else
return 1 # Does not exist
fi
}
# Get VM information from cluster
get_vm_info() {
local vmid=$1
local vm_info
log_verbose "Querying cluster for VM $vmid information..."
# Get cluster resources and filter for the specific VMID
if ! vm_info=$(pvesh get /cluster/resources --type vm --output json 2>/dev/null | jq -r ".[] | select(.vmid == $vmid)"); then
log_verbose "Failed to query cluster resources for VM $vmid"
return 1
fi
if [[ -z "$vm_info" || "$vm_info" == "null" ]]; then
log_verbose "VM $vmid not found in cluster"
return 1
fi
echo "$vm_info"
return 0
}
# Get the node where a VM is located
get_vm_node() {
local vmid=$1
local vm_info node_name
if ! vm_info=$(get_vm_info "$vmid"); then
return 1
fi
node_name=$(echo "$vm_info" | jq -r '.node')
if [[ -z "$node_name" || "$node_name" == "null" ]]; then
log_verbose "Could not determine node for VM $vmid"
return 1
fi
echo "$node_name"
return 0
}
# Get current node name
get_current_node() {
local current_node
if ! current_node=$(hostname); then
log_verbose "Failed to get current hostname"
return 1
fi
echo "$current_node"
return 0
}
# Check if VM exists locally or on remote node
check_vmid_exists_cluster() {
local vmid=$1
local vm_info
# First try local check for performance
if qm status "$vmid" >/dev/null 2>&1; then
return 0 # Exists locally
fi
# Check cluster-wide
if vm_info=$(get_vm_info "$vmid"); then
return 0 # Exists somewhere in cluster
else
return 1 # Does not exist
fi
}
# Execute command on remote node if VM is not local
execute_vm_command() {
local vmid=$1
local script_content="$2"
shift 2
local script_args=("$@")
local vm_node current_node
# Get current node
if ! current_node=$(get_current_node); then
log_error "Failed to determine current node"
return 1
fi
# Check if VM exists locally first
if qm status "$vmid" >/dev/null 2>&1; then
log_verbose "VM $vmid found locally, executing command locally"
bash -c "$script_content" -- "${script_args[@]}"
return $?
fi
# Get VM node from cluster
if ! vm_node=$(get_vm_node "$vmid"); then
log_error "VM $vmid not found in cluster"
return 1
fi
if [[ "$vm_node" == "$current_node" ]]; then
# Should not happen since we checked locally first, but just in case
log_verbose "VM $vmid should be local but wasn't found locally"
bash -c "$script_content" -- "${script_args[@]}"
return $?
else
log_info "VM $vmid is on node '$vm_node', executing command remotely via SSH..."
log_verbose "Executing script on $vm_node with args: ${script_args[*]}"
# Create a properly escaped command for SSH
local ssh_command="bash -c $(printf '%q' "$script_content") --"
for arg in "${script_args[@]}"; do
ssh_command+=" $(printf '%q' "$arg")"
done
# Execute command on remote node via SSH
if ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "root@$vm_node" "$ssh_command"; then
return 0
else
log_error "Failed to execute command on remote node '$vm_node'"
return 1
fi
fi
}
is_vm_running() {
local vmid=$1
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
return 0 # Running
else
return 1 # Not running
fi
}
create_vm() {
local vmid=$1
local vm_name_suffix=${2:-node}
local vm_name="${DEFAULT_VM_NAME_PREFIX}-${vm_name_suffix}"
# Handle MAC address prompt BEFORE potentially destroying existing VM
local mac_address_to_use=""
if [[ -n "${MAC_ADDRESS_OPT:-}" ]]; then
mac_address_to_use="$MAC_ADDRESS_OPT"
log_verbose "Using custom MAC address: $mac_address_to_use"
else
# If VM exists and we're forcing, show its current MAC address for convenience
if check_vmid_exists_cluster "$vmid" && [[ "${FORCE_FLAG_OPT:-false}" == "true" ]]; then
local existing_mac
existing_mac=$(execute_vm_command "$vmid" "qm config \$1 | grep '^net0:' | grep -o -E '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}'" "$vmid" 2>/dev/null || echo "N/A")
if [[ "$existing_mac" != "N/A" ]]; then
log_info "Existing VM $vmid MAC address: $existing_mac"
fi
fi
log_info "No MAC address specified. A random MAC address will be generated."
read -r -p "Continue with random MAC address? (Y/n): " mac_choice
if [[ "$mac_choice" =~ ^[Nn]$ ]]; then
log_info "VM creation aborted. Specify a MAC address with --mac-address=XX:XX:XX:XX:XX:XX"
return 1
fi
log_info "Proceeding with random MAC address generation..."
fi
# Now handle existing VM after MAC address decision is made
if check_vmid_exists_cluster "$vmid"; then
if [[ "${FORCE_FLAG_OPT:-false}" == "true" ]]; then
log_warning "VM $vmid already exists. Force flag enabled - deleting existing VM first..."
local force_destroy_script=$(cat << 'EOF'
vmid=$1
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
log_verbose "Force destroying VM $vmid..."
qm stop "$vmid" --timeout 10 >/dev/null 2>&1 || true
for _ in {1..3}; do
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
break
fi
sleep 1
done
qm stop "$vmid" --force >/dev/null 2>&1 || true
sleep 1
qm unlock "$vmid" >/dev/null 2>&1 || true
qm destroy "$vmid" --purge >/dev/null 2>&1 || true
EOF
)
if execute_vm_command "$vmid" "$force_destroy_script" "$vmid"; then
log_success "Existing VM $vmid deleted. Proceeding with creation..."
else
log_error "Failed to delete existing VM $vmid. Aborting creation."
return 1
fi
else
log_error "VMID $vmid already exists. Use '--force' flag or choose different VMID."
return 1
fi
fi
log_info "Creating VM $vmid ($vm_name)..."
local actual_cores="${CORES_OPT:-$CORES}"
local actual_sockets="${SOCKETS_OPT:-$SOCKETS}"
local actual_ram_mb="${RAM_MB_OPT:-$RAM_MB}"
log_verbose "VM resources: Cores=$actual_cores, Sockets=$actual_sockets, RAM=${actual_ram_mb}MB"
local iso_path actual_iso_storage
if [[ -n "${ISO_NAME_OPT:-}" ]]; then
actual_iso_storage="${STORAGE_ISO_OPT:-local}"
iso_path="${actual_iso_storage}:iso/${ISO_NAME_OPT}"
log_verbose "Using custom ISO: $iso_path (from storage: $actual_iso_storage)"
else
iso_path="$TALOS_ISO_PATH"
log_verbose "Using default ISO: $iso_path"
fi
local actual_storage_os="${STORAGE_OS_OPT:-$STORAGE_POOL_OS}"
local actual_storage_efi="${STORAGE_EFI_OPT:-$actual_storage_os}" # Use specific EFI storage if provided, else same as OS
local actual_storage_data="${STORAGE_DATA_OPT:-$STORAGE_POOL_DATA}"
if [[ "$actual_storage_os" != "$STORAGE_POOL_OS" ]]; then log_verbose "Using custom OS storage: $actual_storage_os"; fi
if [[ "$actual_storage_efi" != "$actual_storage_os" ]]; then log_verbose "Using custom EFI storage: $actual_storage_efi"; fi # Only log if different from OS
if [[ "$actual_storage_data" != "$STORAGE_POOL_DATA" ]]; then log_verbose "Using custom data storage: $actual_storage_data"; fi
local net_config="virtio,bridge=$NETWORK_BRIDGE,firewall=0"
if [[ -n "$mac_address_to_use" ]]; then
net_config="${net_config},macaddr=${mac_address_to_use}"
fi
if [[ -n "${VLAN_TAG_OPT:-}" ]]; then
net_config="${net_config},tag=${VLAN_TAG_OPT}"
log_verbose "Using VLAN tag: $VLAN_TAG_OPT"
fi
log_verbose "Creating VM with basic settings using 'qm create'..."
run_critical qm create "$vmid" \
--name "$vm_name" --ostype "$OS_TYPE" --machine "$MACHINE_TYPE" --bios "$BIOS_TYPE" \
--cpu host --cores "$actual_cores" --sockets "$actual_sockets" --numa 1 \
--memory "$actual_ram_mb" --balloon 0 --onboot 1 --net0 "$net_config"
log_verbose "Adding EFI disk to storage '$actual_storage_efi'..."
run_with_warnings qm set "$vmid" --efidisk0 "${actual_storage_efi}:0,efitype=4m,pre-enrolled-keys=0"
log_verbose "Adding SCSI controller (virtio-scsi-pci)..."
run_quiet qm set "$vmid" --scsihw virtio-scsi-pci
log_verbose "Adding OS disk (${DISK_OS_SIZE_GB}GB) to storage '$actual_storage_os'..."
run_with_warnings qm set "$vmid" --scsi0 "${actual_storage_os}:${DISK_OS_SIZE_GB},ssd=1"
log_verbose "Adding data disk (${DISK_DATA_SIZE_GB}GB) to storage '$actual_storage_data'..."
run_with_warnings qm set "$vmid" --scsi1 "${actual_storage_data}:${DISK_DATA_SIZE_GB},ssd=1"
log_verbose "Mounting ISO: $iso_path"
run_critical qm set "$vmid" --ide2 "$iso_path,media=cdrom"
log_verbose "Setting boot order: ide2 (ISO), then scsi0 (OS Disk)..."
run_quiet qm set "$vmid" --boot order="ide2;scsi0"
log_verbose "Adding serial console and setting VGA to $VGA_TYPE..."
run_quiet qm set "$vmid" --serial0 socket --vga "$VGA_TYPE"
log_verbose "Enabling QEMU Guest Agent..."
run_quiet qm set "$vmid" --agent enabled=1
log_success "VM $vmid ($vm_name) created successfully!"
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo; log_verbose "VM Configuration:"; qm config "$vmid"; echo; fi
# Always retrieve and display MAC address
local actual_mac_address
actual_mac_address=$(qm config "$vmid" | grep "^net0:" | grep -o -E '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' 2>/dev/null || echo "N/A")
if [[ -n "$mac_address_to_use" ]]; then
log_info "VM $vmid MAC address: $actual_mac_address (user-specified)"
else
log_info "VM $vmid MAC address: $actual_mac_address (randomly generated)"
fi
if [[ "${START_FLAG_OPT:-false}" == "true" ]]; then
log_info "Starting VM $vmid (--start flag provided)..."
if run_with_output qm start "$vmid"; then
log_success "VM $vmid started successfully!"
wait_for_vm_online "$vmid"
else
log_error "Failed to start VM $vmid. Check Proxmox task logs."
return 1
fi
log_warning "After Talos install, remember to:"
log_warning " - Eject ISO : 'qm set $vmid --ide2 none'"
log_warning " - Adjust boot order : 'qm set $vmid --boot order=scsi0'"
else
log_info "VM $vmid created but not started (use --start flag to auto-start)."
log_info "To start manually: qm start $vmid"
log_warning "Remember to eject ISO ('qm set $vmid --ide2 none') and adjust boot order after Talos install."
fi
return 0
}
destroy_vm() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then
log_warning "VMID $vmid does not exist. Nothing to destroy."
return 0
fi
log_info "Attempting to destroy VM $vmid..."
read -r -p "ARE YOU SURE to PERMANENTLY destroy VM $vmid and its disks? (yes/NO): " conf
if [[ "$conf" != "yes" ]]; then log_info "Destruction aborted."; return 0; fi
# Execute the destruction on the appropriate node
local destroy_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
log_info "Stopping VM $vmid (if running)..."
qm stop "$vmid" --timeout 30 || log_verbose "VM $vmid not running or stop timed out."
for i in {1..5}; do
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_verbose "VM $vmid is stopped."; break;
fi
log_verbose "Waiting for VM $vmid to stop... ($i/5)"; sleep 2
done
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_warning "VM $vmid still running. Attempting force stop."
qm stop "$vmid" --force || log_verbose "Force stop issued."
sleep 3
fi
log_info "Destroying VM $vmid and its disks..."
if qm destroy "$vmid" --purge; then
log_success "VM $vmid destroyed."
else
log_warning "Failed to destroy VM $vmid. Attempting unlock and retry..."
qm unlock "$vmid" || log_verbose "Unlock issued for VM $vmid."
if qm destroy "$vmid" --purge; then
log_success "VM $vmid destroyed on retry."
else
log_error "Still failed to destroy VM $vmid. Manual intervention may be required."
exit 1
fi
fi
EOF
)
if execute_vm_command "$vmid" "$destroy_script" "$vmid"; then
log_success "VM $vmid destruction completed."
return 0
else
log_error "Failed to destroy VM $vmid"
return 1
fi
}
force_destroy_vm() {
local vmid=$1
if ! check_vmid_exists "$vmid"; then log_verbose "VM $vmid non-existent, nothing to force destroy."; return 0; fi
log_verbose "Force destroying VM $vmid..."
qm stop "$vmid" --timeout 10 >/dev/null 2>&1 || true
for _ in {1..3}; do if ! is_vm_running "$vmid"; then break; fi; sleep 1; done
if is_vm_running "$vmid"; then qm stop "$vmid" --force >/dev/null 2>&1 || true; sleep 2; fi
if qm destroy "$vmid" --purge >/dev/null 2>&1; then
log_verbose "VM $vmid force-destroyed."
return 0
else
qm unlock "$vmid" >/dev/null 2>&1 || true
if qm destroy "$vmid" --purge >/dev/null 2>&1; then
log_verbose "VM $vmid force-destroyed on retry after unlock."
return 0
else
return 1 # Caller (create_vm) will log error
fi
fi
}
stop_vm() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then log_warning "VM $vmid does not exist."; return 1; fi
local stop_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_info "VM $vmid is already stopped."
exit 0
fi
log_info "Attempting graceful stop for VM $vmid..."
if qm stop "$vmid" --timeout 60; then
log_success "VM $vmid stopped."
exit 0
fi
log_warning "Graceful stop failed or timed out."
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_success "VM $vmid now stopped (after timeout)."
exit 0
fi
log_info "Attempting force stop for VM $vmid..."
if qm stop "$vmid" --force; then
log_success "VM $vmid forcibly stopped."
else
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_success "VM $vmid now stopped (after force attempt)."
exit 0
fi
log_error "Failed to stop VM $vmid even with force."
exit 1
fi
EOF
)
execute_vm_command "$vmid" "$stop_script" "$vmid"
return $?
}
shutdown_vm() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then log_warning "VM $vmid does not exist."; return 1; fi
local shutdown_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_info "VM $vmid is already stopped."
exit 0
fi
log_info "Attempting guest OS shutdown for VM $vmid via QEMU agent..."
if qm guest cmd "$vmid" ping >/dev/null 2>&1; then
log_verbose "QEMU agent responsive. Sending shutdown..."
if qm guest cmd "$vmid" shutdown; then
log_info "Shutdown command sent. Waiting up to 60s for VM $vmid to power off..."
wait_time=0
dots=false
while [[ $wait_time -lt 60 ]]; do
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$dots" == "true" ]]; then echo; fi
log_success "VM $vmid shut down via guest agent."
exit 0
fi
if [[ "${VERBOSE_FLAG:-false}" != "true" ]]; then printf "."; dots=true; fi
log_verbose "VM $vmid still running. Waited ${wait_time}s..."
sleep 5
wait_time=$((wait_time + 5))
done
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$dots" == "true" ]]; then echo; fi
log_warning "VM $vmid did not shut down via guest agent within 60s."
else
log_warning "Agent ping OK, but qm guest cmd $vmid shutdown failed."
fi
else
log_warning "QEMU agent for VM $vmid not responding. Cannot send guest shutdown."
fi
log_info "Falling back to standard stop for VM $vmid."
# Fall back to standard stop
if qm stop "$vmid" --timeout 60; then
log_success "VM $vmid stopped."
else
log_warning "Graceful stop failed or timed out."
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_success "VM $vmid now stopped (after timeout)."
exit 0
fi
log_info "Attempting force stop for VM $vmid..."
if qm stop "$vmid" --force; then
log_success "VM $vmid forcibly stopped."
else
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_success "VM $vmid now stopped (after force attempt)."
exit 0
fi
log_error "Failed to stop VM $vmid even with force."
exit 1
fi
fi
EOF
)
execute_vm_command "$vmid" "$shutdown_script" "$vmid"
return $?
}
restart_vm() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then log_warning "VM $vmid does not exist."; return 1; fi
log_info "Attempting host-level restart for VM $vmid..."
local restart_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_info "VM $vmid running. Stopping it first..."
if ! qm stop "$vmid" --timeout 60; then
log_warning "Graceful stop failed or timed out."
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_info "Attempting force stop for VM $vmid..."
if ! qm stop "$vmid" --force; then
log_error "Failed to stop VM $vmid. Aborting restart."
exit 1
fi
fi
fi
log_verbose "VM $vmid stopped. Proceeding with start."
else
log_info "VM $vmid not running. Will start it."
fi
log_info "Starting VM $vmid..."
if qm start "$vmid"; then
log_success "VM $vmid started."
else
log_error "Failed to start VM $vmid."
exit 1
fi
EOF
)
if execute_vm_command "$vmid" "$restart_script" "$vmid"; then
wait_for_vm_online "$vmid"
return 0
else
return 1
fi
}
start_vm() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then log_warning "VM $vmid does not exist."; return 1; fi
local start_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_info "VM $vmid is already running."
exit 0
fi
log_info "Starting VM $vmid..."
if qm start "$vmid"; then
log_success "VM $vmid started."
else
log_error "Failed to start VM $vmid."
exit 1
fi
EOF
)
if execute_vm_command "$vmid" "$start_script" "$vmid"; then
wait_for_vm_online "$vmid"
return 0
else
return 1
fi
}
reboot_vm() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then log_warning "VM $vmid does not exist."; return 1; fi
local reboot_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
log_warning "VM $vmid not running. Use restart or start it first."
exit 1
fi
log_info "Attempting guest OS reboot for VM $vmid..."
if ! qm guest cmd "$vmid" ping >/dev/null 2>&1; then
log_warning "QEMU agent for VM $vmid not responding. Cannot send guest reboot."
log_info "Consider restart command for host-level restart."
exit 1
fi
log_verbose "Agent ping OK. Sending guest reboot command (via shutdown)..."
# Proxmox reboot command can be unreliable, shutdown + start is more robust.
if qm guest cmd "$vmid" shutdown; then
log_info "Guest shutdown command sent for reboot. Waiting up to 60s..."
wait_s=0
timeout_s=60
dots_s=false
while [[ $wait_s -lt $timeout_s ]]; do
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$dots_s" == "true" ]]; then echo; fi
log_success "VM $vmid shut down via agent."
break
fi
if [[ "${VERBOSE_FLAG:-false}" != "true" ]]; then printf "."; dots_s=true; fi
log_verbose "VM $vmid still running. Waited ${wait_s}s..."
sleep 3
wait_s=$((wait_s + 3))
done
if [[ $wait_s -ge $timeout_s ]]; then
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$dots_s" == "true" ]]; then echo; fi
log_warning "VM $vmid did not shut down within ${timeout_s}s. Forcing restart..."
qm stop "$vmid" --force || true
sleep 2
fi
log_info "Starting VM $vmid after reboot..."
if qm start "$vmid"; then
log_success "VM $vmid reboot completed."
else
log_error "Failed to start VM $vmid after reboot."
exit 1
fi
else
log_error "Failed to send shutdown command for reboot."
exit 1
fi
EOF
)
if execute_vm_command "$vmid" "$reboot_script" "$vmid"; then
wait_for_vm_online "$vmid"
return 0
else
return 1
fi
}
list_iso_storages() {
log_info "=== Available ISO Storages and Contents ==="; echo
local storages
if ! storages=$(pvesm status --content iso 2>/dev/null | tail -n +2 | awk '{print $1}'); then
log_warning "Could not list ISO storages (pvesm status failed)."; return 1;
fi
if [[ -z "$storages" ]]; then log_info "No storages with ISO content found."; return 0; fi
for storage in $storages; do
echo "Storage: $storage"; echo "---------------------------------------"
local storage_info; storage_info=$(pvesm status --storage "$storage" 2>/dev/null || true)
if [[ -n "$storage_info" ]]; then
echo "Status: $(echo "$storage_info" | tail -n +2 | awk '{print $3" ("$2")"}')"
fi
local iso_files; iso_files=$(pvesm list "$storage" --content iso 2>/dev/null | tail -n +2 || true)
if [[ -n "$iso_files" ]]; then
echo "Available ISO files:"
echo "$iso_files" | while IFS= read -r line; do
local volid name size format human_size=""
volid=$(echo "$line" | awk '{print $1}')
name=$(basename "$volid")
size=$(echo "$line" | awk '{print $4}')
format=$(echo "$line" | awk '{print $2}')
if command -v numfmt >/dev/null && [[ "$size" =~ ^[0-9]+$ ]]; then
human_size=" ($(numfmt --to=iec-i --suffix=B --format="%.1f" "$size"))"
fi
echo " - ${name} (VolID: ${volid}, Format: ${format}, Size: ${size}${human_size})"
done
else
echo " No ISO files found in this storage."
fi; echo
done
return 0
}
wait_for_vm_online() {
local vmid=$1 max_wait=300 interval=5 elapsed=0 init_msg=false dots=false
log_info "Waiting for VM $vmid QEMU agent..."
local wait_script=$(cat << 'EOF'
vmid=$1
max_wait=$2
interval=$3
elapsed=0
init_msg=false
dots=false
while [[ $elapsed -lt $max_wait ]]; do
if ! qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$init_msg" == "false" ]]; then echo -n "VM not running, waiting"; init_msg=true; fi
if [[ "${VERBOSE_FLAG:-false}" != "true" ]]; then printf "."; dots=true; fi
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 VM $vmid not running. Waiting..." >&2; fi
sleep $interval
elapsed=$((elapsed + interval))
continue
fi
if ! $init_msg && [[ "${VERBOSE_FLAG:-false}" != "true" ]]; then echo -n "VM running, waiting for agent"; init_msg=true; fi
if qm guest cmd "$vmid" ping >/dev/null 2>&1; then
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$dots" == "true" ]]; then echo; fi
echo "✅ VM $vmid agent responding to ping!"
exit 0
fi
if [[ "${VERBOSE_FLAG:-false}" != "true" ]]; then printf "."; dots=true; fi
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 VM $vmid agent not yet responding to ping. Waiting..." >&2; fi
sleep $interval
elapsed=$((elapsed + interval))
done
if [[ "${VERBOSE_FLAG:-false}" != "true" && "$dots" == "true" ]]; then echo; fi
echo "⚠️ Timeout waiting for VM $vmid agent after ${max_wait}s." >&2
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
echo "ℹ️ VM $vmid running, but agent unresponsive."
else
echo "ℹ️ VM $vmid not running."
fi
echo "ℹ️ Check manually: qm status $vmid / qm terminal $vmid"
exit 1
EOF
)
execute_vm_command "$vmid" "$wait_script" "$vmid" "$max_wait" "$interval"
return $?
}
display_vm_stats() {
local vmid=$1
log_info "VM Statistics ($vmid):"
echo "┌────────────────────────────────────────────────────────────┐"
local stats_script=$(cat << 'EOF'
vmid=$1
status=$(qm status "$vmid" | awk "{print \$2}" 2>/dev/null || echo "unknown")
mac=$(qm config "$vmid" | grep "^net0:" | grep -o -E "([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}" 2>/dev/null || echo "N/A")
node=$(hostname 2>/dev/null || echo "unknown")
printf "│ %-15s │ %-40s │\n" "Status:" "$status"
printf "│ %-15s │ %-40s │\n" "MAC Address:" "$mac"
printf "│ %-15s │ %-40s │\n" "Node:" "$node"
EOF
)
execute_vm_command "$vmid" "$stats_script" "$vmid"
echo "└────────────────────────────────────────────────────────────┘"
}
mount_iso() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then
log_error "VM $vmid does not exist."
return 1
fi
# Build the ISO path
local iso_path actual_iso_storage
actual_iso_storage="${STORAGE_ISO_OPT:-local}"
iso_path="${actual_iso_storage}:iso/${ISO_NAME_OPT}"
log_info "Mounting ISO '$iso_path' to VM $vmid..."
log_verbose "Using storage: $actual_iso_storage, ISO file: $ISO_NAME_OPT"
local mount_script=$(cat << 'EOF'
vmid=$1
iso_path=$2
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
# Check if an ISO is already mounted
current_ide2=$(qm config "$vmid" | grep "^ide2:" || true)
if [[ -n "$current_ide2" && "$current_ide2" != *"none"* ]]; then
log_warning "VM $vmid already has IDE2 configured: $current_ide2"
read -r -p "Replace current IDE2 configuration? (y/N): " replace_choice
if [[ ! "$replace_choice" =~ ^[Yy]$ ]]; then
log_info "Mount operation cancelled."
exit 0
fi
fi
# Mount the ISO
if qm set "$vmid" --ide2 "$iso_path,media=cdrom"; then
log_success "ISO \"$iso_path\" mounted to VM $vmid on IDE2."
# Show current configuration for verification
new_ide2=$(qm config "$vmid" | grep "^ide2:" || echo "none")
log_info "Current IDE2 configuration: $new_ide2"
else
log_error "Failed to mount ISO \"$iso_path\" to VM $vmid."
exit 1
fi
EOF
)
execute_vm_command "$vmid" "$mount_script" "$vmid" "$iso_path"
return $?
}
unmount_iso() {
local vmid=$1
if ! check_vmid_exists_cluster "$vmid"; then
log_error "VM $vmid does not exist."
return 1
fi
log_info "Unmounting ISO from VM $vmid..."
local unmount_script=$(cat << 'EOF'
vmid=$1
log_info() { echo "ℹ️ $*"; }
log_success() { echo "✅ $*"; }
log_warning() { echo "⚠️ $*" >&2; }
log_error() { echo "❌ $*" >&2; }
log_verbose() { if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then echo "🔍 $*" >&2; fi; }
# Check current IDE2 configuration
current_ide2=$(qm config "$vmid" | grep "^ide2:" || true)
if [[ -z "$current_ide2" || "$current_ide2" == *"none"* ]]; then
log_info "VM $vmid has no ISO mounted (IDE2 is already empty)."
exit 0
fi
log_verbose "Current IDE2 configuration: $current_ide2"
# Unmount the ISO by setting IDE2 to none
if qm set "$vmid" --ide2 none; then
log_success "ISO unmounted from VM $vmid (IDE2 set to none)."
# Verify the unmount
new_ide2=$(qm config "$vmid" | grep "^ide2:" || echo "none")
log_verbose "New IDE2 configuration: $new_ide2"
else
log_error "Failed to unmount ISO from VM $vmid."
exit 1
fi
EOF
)
execute_vm_command "$vmid" "$unmount_script" "$vmid"
return $?
}
#!/usr/bin/env bash
# LibVersion: 1.0.0
#
# Generic utility functions for Bash scripts.
# Relies on VERBOSE_FLAG being set in the calling script.
# Relies on logging functions (log_info, log_error etc.) being defined in the calling script.
# Helper execution functions
run_quiet() {
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then
"$@"
else
"$@" >/dev/null 2>&1
fi
}
run_with_output() {
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then
"$@"
else
"$@" 2>/dev/null # Suppresses stderr in non-verbose. Exit status is still checked.
fi
}
run_with_warnings() {
local temp_output
temp_output=$(mktemp)
local exit_code
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then
"$@" 2>&1 | tee "$temp_output"
exit_code=${PIPESTATUS[0]}
else
if "$@" >"$temp_output" 2>&1; then
exit_code=0
else
exit_code=$?
fi
if grep -i "WARNING" "$temp_output" >/dev/null 2>&1; then
echo ""
log_warning "Storage warnings detected:" # Assumes log_warning is defined in main script
grep -i "WARNING" "$temp_output" | while IFS= read -r warning_line; do
echo " $warning_line"
done
echo ""
fi
fi
rm -f "$temp_output"
return $exit_code
}
run_critical() {
if [[ "${VERBOSE_FLAG:-false}" == "true" ]]; then
"$@"
else
local temp_output
temp_output=$(mktemp)
if "$@" >"$temp_output" 2>&1; then
rm -f "$temp_output"
return 0
else
local exit_code=$?
log_error "Command failed. Error details:" # Assumes log_error is defined
cat "$temp_output" >&2
rm -f "$temp_output"
return $exit_code
fi
fi
}
# Version comparison function
# Usage: compare_versions "1.0.0" "1.0.1" -> returns 0 (v1 < v2)
# compare_versions "1.0.1" "1.0.0" -> returns 2 (v1 > v2)
# compare_versions "1.0.0" "1.0.0" -> returns 1 (v1 == v2)
compare_versions() {
local v1=$1
local v2=$2
if [[ "$v1" == "$v2" ]]; then return 1; fi
if [[ "$(printf '%s\n' "$v1" "$v2" | sort -V | head -n1)" == "$v1" ]]; then
return 0 # v1 < v2
else
return 2 # v1 > v2
fi
}
#!/usr/bin/env bash
# To download:
# curl -o vm.sh https://gist.githubusercontent.com/QNimbus/a972908f09c2b6fed2b33307d00076f1/raw/vm.sh && chmod +x vm.sh
################################################################################
# Proxmox VM Creation and Management Script for Talos/Kubernetes Nodes
################################################################################
#
# Description: Automated script for creating and managing Talos Kubernetes VMs
# on Proxmox VE with advanced configuration options and power controls.
# Uses external libraries for utility and Proxmox functions.
#
# Author: B. van Wetten <[email protected]>
# Version: 1.3.8
# Created: 2025-06-18
# Updated: 2025-06-27
#
# Features: - Automated VM creation with configurable resources (CPU, RAM, Disks)
# - Custom ISO mounting and storage selection
# - VLAN tagging support
# - QEMU Guest Agent integration for creation and power operations
# - Force recreation of existing VMs
# - VM power controls: stop, shutdown, restart, reboot
# - Verbose and quiet operation modes
# - VM status monitoring and statistics
# - Clean, formatted output with progress indicators
# - Robust error trapping and reporting
# - Self-update mechanism for main script and libraries
# - Modular design with functions in ./lib/
# - Cluster-aware VM management (automatically detects VM location)
# - Cross-node VM operations via SSH
# - Dependency checking including jq for JSON parsing
#
# Usage: ./vm.sh <action> [VMID[,VMID,...]] [options]
#
# Examples: ./vm.sh create 1001 server-1 --iso=talos-v1.7.0-amd64.iso --cores=2 --ram=4096
# ./vm.sh create 1002 worker-1 --vlan=100 --force
# ./vm.sh destroy 1001
# ./vm.sh destroy 1001,1002,1003
# ./vm.sh start 1001,1002
# ./vm.sh stop 1001,1002,1003
# ./vm.sh list-iso
# ./vm.sh update
#
# Repository: https://gist.github.com/QNimbus/a972908f09c2b6fed2b33307d00076f1
#
################################################################################
# --- Script Metadata for Updates ---
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
SCRIPT_CURRENT_VERSION_LINE=$(grep '^# Version:' "$0" || echo "# Version: 0.0.0-local")
SCRIPT_CURRENT_VERSION=$(echo "$SCRIPT_CURRENT_VERSION_LINE" | awk '{print $3}')
SCRIPT_RAW_URL="https://gist.githubusercontent.com/QNimbus/a972908f09c2b6fed2b33307d00076f1/raw/vm.sh"
# --- Library Configuration ---
LIB_DIR_NAME="lib"
LIB_DIR="${SCRIPT_DIR}/${LIB_DIR_NAME}"
declare -A LIBRARIES_CONFIG
LIBRARIES_CONFIG=(
["utils"]="utils.lib.sh UTILS_LIB_RAW_URL UTILS_LIB_CURRENT_VERSION ^# LibVersion:"
["proxmox"]="proxmox.lib.sh PROXMOX_LIB_RAW_URL PROXMOX_LIB_CURRENT_VERSION ^# LibVersion:"
)
UTILS_LIB_RAW_URL="https://gist.githubusercontent.com/QNimbus/a972908f09c2b6fed2b33307d00076f1/raw/utils.lib.sh"
PROXMOX_LIB_RAW_URL="https://gist.githubusercontent.com/QNimbus/a972908f09c2b6fed2b33307d00076f1/raw/proxmox.lib.sh"
# --- Global Flags & Early Utility Definitions ---
VERBOSE_FLAG="false"
log_info() { echo "ℹ️ $*"; }
log_success() { echo "$*"; }
log_warning() { echo "⚠️ $*" >&2; } # Warnings to stderr
log_error() { echo "$*" >&2; } # Errors to stderr
log_verbose() { if [[ "$VERBOSE_FLAG" == "true" ]]; then echo "🔍 $*" >&2; fi; } # Verbose to stderr (FIXED)
_err_trap() {
local exit_code=$?; local line_no=${1:-$LINENO}; local command_str="${BASH_COMMAND}"
local func_stack=("${FUNCNAME[@]}"); local source_stack=("${BASH_SOURCE[@]}")
if [[ "$command_str" == "exit"* || "$command_str" == *"_err_trap"* || "$exit_code" -eq 0 || "$command_str" == "return"* ]]; then return; fi
echo; log_error "ERROR in $SCRIPT_NAME: Script exited with status $exit_code."
log_error "Failed command: '$command_str' on line $line_no of file '${BASH_SOURCE[0]}'."
if [[ ${#func_stack[@]} -gt 1 ]]; then
log_error "Call Stack (most recent call first):"
for i in $(seq 1 $((${#func_stack[@]} - 1))); do
local func_idx=$((i)); local src_idx=$((i)); local line_idx=$((i-1))
local func="${func_stack[$func_idx]}"; local src_file="${source_stack[$src_idx]}"; local src_line="${BASH_LINENO[$line_idx]}"
log_error " -> function '$func' in file '$src_file' at line $src_line"
done
fi
}
trap '_err_trap "${LINENO}"' ERR
set -euo pipefail
# --- Library Management Functions ---
get_remote_file_version() {
local file_url="$1"; local version_grep_pattern="$2"; local timestamp cache_busted_url remote_script_content remote_version_line curl_exit_code
log_verbose "Fetching remote version metadata from URL: $file_url"
timestamp=$(date +%s); cache_busted_url="${file_url}?v=${timestamp}&nocache=$(date +%s%N 2>/dev/null || echo "$RANDOM")"
log_verbose "Using cache-busted URL: $cache_busted_url"
local curl_cmd_args=( # FIXED curl call
--fail -sL
-H "Cache-Control: no-cache, no-store, max-age=0, must-revalidate"
-H "Pragma: no-cache"
-H "Expires: 0"
"$cache_busted_url"
)
set +e; remote_script_content=$(curl "${curl_cmd_args[@]}"); curl_exit_code=$?; set -e
log_verbose "curl exit code for version fetch: $curl_exit_code"
if [[ $curl_exit_code -ne 0 ]]; then log_warning "curl failed for '$file_url' (Code: $curl_exit_code)."; return 1; fi
if [[ -z "$remote_script_content" ]]; then log_warning "Fetched content empty for '$file_url'."; return 1; fi
remote_version_line=$(echo "$remote_script_content" | grep "$version_grep_pattern" || true)
if [[ -z "$remote_version_line" ]]; then log_warning "No version (pattern: '$version_grep_pattern') in '$file_url'."; return 1; fi
echo "$remote_version_line" | awk '{print $3}' # Only this goes to stdout
}
get_local_file_version() {
local file_path="$1"; local version_grep_pattern="$2"; local version_line
if [[ ! -f "$file_path" ]]; then return 1; fi
version_line=$(grep "$version_grep_pattern" "$file_path" || echo "")
if [[ -z "$version_line" ]]; then log_warning "No version (pattern: '$version_grep_pattern') in local '$file_path'."; return 1; fi
echo "$version_line" | awk '{print $3}'
}
download_file() {
local file_url="$1"; local local_path="$2"; local temp_file timestamp cache_busted_url
log_info "Downloading '$file_url' to '$local_path'..."
if ! temp_file=$(mktemp); then log_error "Failed to create temp file."; return 1; fi
timestamp=$(date +%s); cache_busted_url="${file_url}?v=${timestamp}&nocache=$(date +%s%N 2>/dev/null || echo "$RANDOM")"
log_verbose "Downloading from (cache-busted): $cache_busted_url"
local curl_cmd_args=( # FIXED curl call
--fail -sL
-o "$temp_file"
-H "Cache-Control: no-cache, no-store, max-age=0, must-revalidate"
-H "Pragma: no-cache"
-H "Expires: 0"
"$cache_busted_url"
)
if ! curl "${curl_cmd_args[@]}"; then
log_error "Failed to download from '$file_url' (curl exit: $?). Temp: $temp_file";
rm -f "$temp_file"; # Clean up temp file on curl failure
return 1;
fi
if [[ "$local_path" == *".sh" ]] && ! grep -qE "^#!/(bin/bash|usr/bin/env bash)|^# LibVersion:|^# Version:" "$temp_file"; then
log_warning "Downloaded file '$temp_file' for '$local_path' may not be valid script.";
fi
if mv "$temp_file" "$local_path"; then
if [[ "$local_path" == *".sh" ]]; then chmod +x "$local_path"; fi
log_success "File '$local_path' downloaded."; return 0
else
log_error "Failed to move temp to '$local_path' (mv exit $?). Content at $temp_file"; return 1;
fi
}
ensure_library_loaded() {
local lib_key="$1"; IFS=' ' read -r lib_filename lib_url_var_name lib_version_var_name lib_version_grep_pattern <<< "${LIBRARIES_CONFIG[$lib_key]}"
local lib_path="${LIB_DIR}/${lib_filename}"; local lib_url="${!lib_url_var_name}"
log_verbose "Ensuring library '$lib_filename' is loaded..."
if [[ ! -d "$LIB_DIR" ]]; then log_info "Creating lib dir: $LIB_DIR"; if ! mkdir -p "$LIB_DIR"; then log_error "Failed to create '$LIB_DIR'."; exit 1; fi; fi
if [[ ! -f "$lib_path" ]]; then
log_info "Library '$lib_filename' not found. Downloading..."
if ! download_file "$lib_url" "$lib_path"; then log_error "Failed to download '$lib_filename'."; exit 1; fi
fi
if [[ -f "$lib_path" ]]; then
source "$lib_path"; log_verbose "Library '$lib_filename' sourced."
local current_lib_version=$(get_local_file_version "$lib_path" "$lib_version_grep_pattern" || echo "0.0.0-local")
declare -g "$lib_version_var_name=$current_lib_version"; log_verbose "$lib_filename version: ${!lib_version_var_name}"
else log_error "'$lib_filename' not found after download attempt."; exit 1; fi
}
temp_args=()
for arg in "$@"; do case "$arg" in --verbose) VERBOSE_FLAG="true";; *) temp_args+=("$arg");; esac; done
set -- "${temp_args[@]}"; ACTION="${1:-}"
log_verbose "Script directory: $SCRIPT_DIR"; log_verbose "Libraries directory: $LIB_DIR"
for lib_key in "${!LIBRARIES_CONFIG[@]}"; do ensure_library_loaded "$lib_key"; done
# --- VMID Parsing and Multi-VM Functions ---
parse_vmids() {
local vmid_string="$1"
local vmids=()
# Split by comma and validate each VMID
IFS=',' read -ra vmid_array <<< "$vmid_string"
for vmid in "${vmid_array[@]}"; do
# Trim whitespace
vmid=$(echo "$vmid" | xargs)
if ! [[ "$vmid" =~ ^[0-9]+$ ]]; then
log_error "Invalid VMID: '$vmid' (must be numeric)"
return 1
fi
vmids+=("$vmid")
done
# Return array as space-separated string
echo "${vmids[@]}"
}
execute_multi_vm_action() {
local action="$1"
local vmids=("${@:2}")
local success_count=0
local failure_count=0
local total_count=${#vmids[@]}
# Temporarily disable error exit to handle failures gracefully
set +e
if [[ $total_count -gt 1 ]]; then
log_info "Executing '$action' on $total_count VMs: ${vmids[*]}"
echo
fi
for vmid in "${vmids[@]}"; do
if [[ $total_count -gt 1 ]]; then
echo "--- Processing VM $vmid ---"
fi
case "$action" in
create)
# For create action, use the first (and only) VMID since we validated it's single
create_vm "$vmid" "$VM_NAME_SUFFIX"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
destroy)
destroy_vm "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
start)
start_vm "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
stop)
stop_vm "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
shutdown)
shutdown_vm "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
restart)
restart_vm "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
reboot)
reboot_vm "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
mount)
mount_iso "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
unmount)
unmount_iso "$vmid"
local result=$?
if [[ $result -eq 0 ]]; then
((success_count++))
else
((failure_count++))
fi
;;
*)
log_error "Unknown action: $action"
set -e # Re-enable error exit before returning
return 1
;;
esac
if [[ $total_count -gt 1 ]]; then
echo
fi
done
if [[ $total_count -gt 1 ]]; then
echo "--- Summary ---"
log_info "Action '$action' completed on $total_count VMs"
if [[ $success_count -gt 0 ]]; then
log_success "Successful: $success_count"
fi
if [[ $failure_count -gt 0 ]]; then
log_error "Failed: $failure_count"
fi
fi
# Re-enable error exit
set -e
# Return success only if all operations succeeded
if [[ $failure_count -eq 0 ]]; then
return 0
else
return 1
fi
}
# --- Preflight Checks ---
check_dependencies() {
log_verbose "Performing preflight dependency checks..."
local missing_deps=()
# Check for required commands
local required_commands=("qm" "pvesh" "pvesm" "jq" "ssh" "curl")
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "Missing required dependencies: ${missing_deps[*]}"
log_error "Please install the missing dependencies and try again."
return 1
fi
log_verbose "All required dependencies are available."
return 0
}
# Run preflight checks for all actions except update and version
case "$ACTION" in
update|version|--version|""|"-h"|"--help") ;;
*)
if ! check_dependencies; then
log_error "Preflight checks failed. Exiting."
exit 1
fi
;;
esac
DEFAULT_VM_NAME_PREFIX="talos-k8s"; CORES=4; SOCKETS=1; RAM_MB=32768
DISK_OS_SIZE_GB=128; DISK_DATA_SIZE_GB=256; TALOS_ISO_PATH="local:iso/metal-amd64.iso"
STORAGE_POOL_EFI="local-lvm"; STORAGE_POOL_OS="local-lvm"; STORAGE_POOL_DATA="local-lvm"
NETWORK_BRIDGE="vmbr0"; OS_TYPE="l26"; MACHINE_TYPE="q35"; BIOS_TYPE="ovmf"; VGA_TYPE="serial0"
check_and_prompt_script_update() { # FIXED
log_info "Checking for updates to main script ($SCRIPT_NAME)..."
local remote_version
remote_version=$(get_remote_file_version "$SCRIPT_RAW_URL" "^# Version:" || true)
if [[ -z "$remote_version" ]]; then log_warning "Update check for $SCRIPT_NAME failed: no remote version."; return 1; fi
log_verbose "Current $SCRIPT_NAME version: $SCRIPT_CURRENT_VERSION, Remote version: $remote_version"
local comparison_result
if compare_versions "$SCRIPT_CURRENT_VERSION" "$remote_version"; then
comparison_result=0
else
comparison_result=$?
fi
log_verbose "Version comparison result: $comparison_result (0: remote newer, 1: equal, 2: local newer)"
if [[ "$comparison_result" -eq 0 ]]; then
log_success "A new version ($remote_version) of $SCRIPT_NAME is available! (Current: $SCRIPT_CURRENT_VERSION)"
read -r -p "Download and install? (y/N): " choice
if [[ "$choice" =~ ^[Yy]$ ]]; then
if perform_main_script_update "$remote_version"; then
log_success "$SCRIPT_NAME updated successfully!"
log_info "📋 Reminder: Run '$0 update' again to check if libraries need updating."
exit 0
else log_error "Update for $SCRIPT_NAME failed."; return 1; fi
else log_info "Update for $SCRIPT_NAME skipped."; return 2; fi
elif [[ "$comparison_result" -eq 1 ]]; then
log_info "$SCRIPT_NAME is already the latest version ($SCRIPT_CURRENT_VERSION)."
return 0
elif [[ "$comparison_result" -eq 2 ]]; then
log_warning "Current $SCRIPT_NAME version ($SCRIPT_CURRENT_VERSION) > remote ($remote_version)."
return 0
else
log_error "Unknown comparison_result ($comparison_result) for $SCRIPT_NAME."
return 1
fi
log_error "Fallback return from check_and_prompt_script_update - logic error."; return 1
}
perform_main_script_update() {
local new_version=$1; log_info "Attempting to update $SCRIPT_NAME to version $new_version..."
local script_path="${SCRIPT_DIR}/${SCRIPT_NAME}"; local new_script_temp_path
if ! new_script_temp_path=$(mktemp); then log_error "Failed to create temp file for update."; return 1; fi
# The first download_file call was part of a more complex logic, simplified now.
# We directly download to new_script_temp_path for self-update.
log_info "Downloading new version of $SCRIPT_NAME to temporary location..." # This line was correct
if ! download_file "$SCRIPT_RAW_URL" "$new_script_temp_path"; then
log_error "Failed to download new version of $SCRIPT_NAME. Update aborted."; rm -f "$new_script_temp_path"; return 1; fi
local downloaded_version=$(get_local_file_version "$new_script_temp_path" "^# Version:" || true)
if [[ -z "$downloaded_version" ]]; then log_error "Could not extract version from downloaded script $new_script_temp_path."; rm -f "$new_script_temp_path"; return 1; fi
if [[ "$downloaded_version" != "$new_version" ]]; then
log_warning "Downloaded $SCRIPT_NAME version ($downloaded_version) mismatch expected ($new_version)."
local comp_res; if compare_versions "$new_version" "$downloaded_version"; then comp_res=0; else comp_res=$?; fi
if [[ "$comp_res" -eq 2 ]]; then log_error "Downloaded $SCRIPT_NAME older than expected."; rm -f "$new_script_temp_path"; return 1; fi
fi
log_verbose "New $SCRIPT_NAME ($downloaded_version) at $new_script_temp_path. Replacing $script_path..."
if mv "$new_script_temp_path" "$script_path"; then
chmod +x "$script_path"; return 0
else log_error "Failed to replace $SCRIPT_NAME. New version at $new_script_temp_path"; return 1; fi
}
check_and_prompt_library_update() { # FIXED
local lib_key="$1"; IFS=' ' read -r lib_filename lib_url_var_name lib_version_var_name lib_version_grep_pattern <<< "${LIBRARIES_CONFIG[$lib_key]}"
local lib_path="${LIB_DIR}/${lib_filename}"; local lib_url="${!lib_url_var_name}"; local current_lib_version_val="${!lib_version_var_name}"
log_info "Checking for updates to library '$lib_filename'..."
local remote_lib_version=$(get_remote_file_version "$lib_url" "$lib_version_grep_pattern" || true)
if [[ -z "$remote_lib_version" ]]; then log_warning "Update check for '$lib_filename' failed."; return 1; fi
log_verbose "Current '$lib_filename' version: $current_lib_version_val, Remote version: $remote_lib_version"
local comparison_result; if compare_versions "$current_lib_version_val" "$remote_lib_version"; then comparison_result=0; else comparison_result=$?; fi
log_verbose "Library '$lib_filename' version comparison result: $comparison_result"
if [[ "$comparison_result" -eq 0 ]]; then
log_success "New version ($remote_lib_version) of '$lib_filename' available! (Current: $current_lib_version_val)"
read -r -p "Download and install? (y/N): " choice
if [[ "$choice" =~ ^[Yy]$ ]]; then
if perform_library_update "$lib_key" "$remote_lib_version"; then
log_success "'$lib_filename' updated."; declare -g "$lib_version_var_name=$remote_lib_version"
log_info "You may need to re-run script or re-source library if behavior changed."
else log_error "Update for '$lib_filename' failed."; fi
else log_info "Update for '$lib_filename' skipped."; fi
elif [[ "$comparison_result" -eq 1 ]]; then log_info "'$lib_filename' is latest ($current_lib_version_val)."
elif [[ "$comparison_result" -eq 2 ]]; then log_warning "Current '$lib_filename' ($current_lib_version_val) > remote ($remote_lib_version)."
else log_error "Unknown comparison_result ($comparison_result) for '$lib_filename'."; fi
return 0 # check_and_prompt_library_update itself should return 0 unless it cannot check
}
perform_library_update() {
local lib_key="$1"; local new_version="$2"
IFS=' ' read -r lib_filename lib_url_var_name _ lib_version_grep_pattern <<< "${LIBRARIES_CONFIG[$lib_key]}"
local lib_path="${LIB_DIR}/${lib_filename}"; local lib_url="${!lib_url_var_name}"
log_info "Attempting to update library '$lib_filename' to version $new_version..."
if ! download_file "$lib_url" "$lib_path"; then log_error "Failed to download '$lib_filename'."; return 1; fi
local downloaded_version=$(get_local_file_version "$lib_path" "$lib_version_grep_pattern" || true)
if [[ -z "$downloaded_version" ]]; then log_error "No version in new '$lib_filename'."; return 1; fi
if [[ "$downloaded_version" != "$new_version" ]]; then
log_warning "Downloaded '$lib_filename' version ($downloaded_version) != expected ($new_version)."
local comp_res; if compare_versions "$new_version" "$downloaded_version"; then comp_res=0; else comp_res=$?; fi
if [[ "$comp_res" -eq 2 ]]; then log_error "Downloaded '$lib_filename' older. Failed."; return 1; fi
fi
return 0
}
VMID_STRING=""; VMIDS=(); VM_NAME_SUFFIX=""
CORES_OPT=""; SOCKETS_OPT=""; RAM_MB_OPT=""; ISO_NAME_OPT=""
STORAGE_ISO_OPT=""; STORAGE_OS_OPT=""; STORAGE_EFI_OPT=""; STORAGE_DATA_OPT=""
VLAN_TAG_OPT=""; MAC_ADDRESS_OPT=""; FORCE_FLAG_OPT="false"; START_FLAG_OPT="false"
case "$ACTION" in
list-iso) list_iso_storages; exit $?;;
update)
check_and_prompt_script_update; update_status_main_script=$?
if [[ "$update_status_main_script" -eq 1 ]]; then log_error "Main script update failed."; exit 1;
elif [[ "$update_status_main_script" -eq 2 ]]; then log_info "Main script update skipped."; fi
log_info "Proceeding to check for library updates..."
for lib_key_for_update in "${!LIBRARIES_CONFIG[@]}"; do check_and_prompt_library_update "$lib_key_for_update"; done
log_info "Update check process finished."; exit 0 ;;
version|--version)
echo "$SCRIPT_NAME version $SCRIPT_CURRENT_VERSION"; echo "--- Library Versions ---"
for lib_key_for_ver_display in "${!LIBRARIES_CONFIG[@]}"; do
IFS=' ' read -r lib_filename _ lib_version_var_name _ <<< "${LIBRARIES_CONFIG[$lib_key_for_ver_display]}"
echo "${lib_filename}: ${!lib_version_var_name}"; done; exit 0 ;;
""|"-h"|"--help") usage; exit 1 ;; # usage is in proxmox.lib.sh, it does not exit itself.
esac
case "$ACTION" in
create|destroy|start|stop|shutdown|restart|reboot|mount|unmount)
if [[ -z "${2:-}" ]]; then log_error "Action '$ACTION' needs VMID(s)."; usage; exit 1; fi
VMID_STRING="$2"
if ! VMIDS=($(parse_vmids "$VMID_STRING")); then
log_error "Invalid VMID format: '$VMID_STRING'"
log_error "Use single VMID (e.g., '1001') or comma-separated list (e.g., '1001,1002,1003')"
usage
exit 1
fi
;;
*) log_error "Invalid action '$ACTION'."; usage; exit 1 ;;
esac
# Validate that create action only accepts single VMID
if [[ "$ACTION" == "create" ]]; then
if [[ ${#VMIDS[@]} -gt 1 ]]; then
log_error "Action '$ACTION' only supports a single VMID, got: $VMID_STRING"
usage
exit 1
fi
fi
param_offset=2
if [[ "$ACTION" == "create" ]]; then
if [[ -n "${3:-}" && "${3::2}" != "--" ]]; then VM_NAME_SUFFIX="$3"; param_offset=3; else VM_NAME_SUFFIX="node"; fi
fi
shift "$param_offset"
if [[ "$ACTION" != "create" && "$ACTION" != "mount" && -n "$@" ]]; then log_warning "Action '$ACTION' ignores extra options: '$@'"; fi
if [[ "$ACTION" == "create" ]]; then
for arg in "$@"; do
case "$arg" in
--cores=*) CORES_OPT="${arg#--cores=}"; if ! [[ "$CORES_OPT" =~ ^[0-9]+$ && "$CORES_OPT" -gt 0 ]]; then log_error "Invalid cores: '$CORES_OPT'"; usage; exit 1; fi;;
--sockets=*) SOCKETS_OPT="${arg#--sockets=}"; if ! [[ "$SOCKETS_OPT" =~ ^[0-9]+$ && "$SOCKETS_OPT" -gt 0 ]]; then log_error "Invalid sockets: '$SOCKETS_OPT'"; usage; exit 1; fi;;
--ram=*) RAM_MB_OPT="${arg#--ram=}"; if ! [[ "$RAM_MB_OPT" =~ ^[0-9]+$ && "$RAM_MB_OPT" -ge 512 ]]; then log_error "Invalid RAM: '$RAM_MB_OPT'"; usage; exit 1; fi;;
--iso=*) ISO_NAME_OPT="${arg#--iso=}";;
--storage-iso=*) STORAGE_ISO_OPT="${arg#--storage-iso=}";;
--storage-os=*) STORAGE_OS_OPT="${arg#--storage-os=}";;
--storage-data=*) STORAGE_DATA_OPT="${arg#--storage-data=}";;
--vlan=*) VLAN_TAG_OPT="${arg#--vlan=}"; if ! [[ "$VLAN_TAG_OPT" =~ ^[0-9]+$ && "$VLAN_TAG_OPT" -ge 1 && "$VLAN_TAG_OPT" -le 4094 ]]; then log_error "Invalid VLAN: '$VLAN_TAG_OPT'"; usage; exit 1; fi;;
--mac-address=*) MAC_ADDRESS_OPT="${arg#--mac-address=}"; if ! [[ "$MAC_ADDRESS_OPT" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then log_error "Invalid MAC address format: '$MAC_ADDRESS_OPT'. Expected format: XX:XX:XX:XX:XX:XX"; usage; exit 1; fi;;
--force) FORCE_FLAG_OPT="true";;
--start) START_FLAG_OPT="true";;
*) if [[ "$arg" != "--verbose" ]]; then log_warning "Unknown param '$arg' for 'create'."; fi;;
esac
done
fi
if [[ "$ACTION" == "mount" ]]; then
for arg in "$@"; do
case "$arg" in
--iso=*) ISO_NAME_OPT="${arg#--iso=}";;
--storage-iso=*) STORAGE_ISO_OPT="${arg#--storage-iso=}";;
*) if [[ "$arg" != "--verbose" ]]; then log_warning "Unknown param '$arg' for 'mount'."; fi;;
esac
done
# Validate that --iso is provided for mount action
if [[ -z "${ISO_NAME_OPT:-}" ]]; then
log_error "Mount action requires --iso option to specify ISO file to mount."
usage
exit 1
fi
fi
case "$ACTION" in
create) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
destroy) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
start) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
stop) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
shutdown) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
restart) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
reboot) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
mount) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
unmount) execute_multi_vm_action "$ACTION" "${VMIDS[@]}"; exit $?;;
*) log_error "Internal error: Unhandled action '$ACTION'."; usage; exit 1;;
esac
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment