From 2f5a5771b0abc7ac2588cc1ec0f4c1111ee45734 Mon Sep 17 00:00:00 2001 From: MickLesk Date: Thu, 7 May 2026 09:45:45 +0200 Subject: [PATCH] Add interactive VM prompts and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce interactive whiptail-based helpers and robust error handling for VM creation. - Add error_handler() to report failures (calls post_update_to_api if available), print contextual error info, and call cleanup_vmid. - Ensure TEMP_DIR is removed in cleanup(). - Replace brittle pve_check with version parsing that supports Proxmox VE 8.0–8.9 and 9.0–9.1 (exits with code 105 on unsupported versions). - Add ssh_check() to warn users running the script over SSH. - Add sanitize_vm_hostname() and a suite of vm_* helper functions to prompt and validate interactive settings via whiptail: vm_confirm_new_vm, vm_choose_settings_mode, vm_prompt_vmid, vm_prompt_machine_type, vm_apply_machine_type, vm_prompt_disk_size, vm_prompt_disk_cache, vm_prompt_hostname, vm_prompt_cpu_model, vm_prompt_cpu_cores, vm_prompt_ram, vm_prompt_bridge, vm_prompt_mac, vm_prompt_vlan, vm_prompt_mtu, vm_prompt_start_vm. - Add storage helpers: vm_select_storage, vm_apply_storage_layout, vm_define_disk_references to detect storage pools, set formats/extensions and prepare disk refs. - Use APP/NSAPP for description title by introducing local description_title in set_description(). These changes centralize validation and interactive flow, improve UX, and harden error reporting and cleanup. --- misc/vm-core.func | 467 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 460 insertions(+), 7 deletions(-) diff --git a/misc/vm-core.func b/misc/vm-core.func index c57a1197..fc26a933 100644 --- a/misc/vm-core.func +++ b/misc/vm-core.func @@ -495,6 +495,20 @@ msg_debug() { fi } +error_handler() { + local exit_code="$?" + local line_number="${1:-unknown}" + local command="${2:-unknown}" + + if declare -f post_update_to_api >/dev/null 2>&1; then + post_update_to_api "failed" "$exit_code" + fi + + local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" + echo -e "\n$error_message\n" + cleanup_vmid +} + # Displays error message and immediately terminates script fatal() { msg_error "$1" @@ -533,6 +547,9 @@ cleanup() { if [[ "$(dirs -p | wc -l)" -gt 1 ]]; then popd >/dev/null || true fi + if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + fi # Report final telemetry status if post_to_api_vm was called but no update was sent if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then if declare -f post_update_to_api >/dev/null 2>&1; then @@ -557,13 +574,32 @@ check_root() { } pve_check() { - if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then - msg_error "This version of Proxmox Virtual Environment is not supported" - echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1." - echo -e "Exiting..." - sleep 2 - exit + local pve_ver + pve_ver="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" + + if [[ "$pve_ver" =~ ^8\.([0-9]+) ]]; then + local minor="${BASH_REMATCH[1]}" + if ((minor < 0 || minor > 9)); then + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported: Proxmox VE version 8.0 – 8.9" + exit 105 + fi + return 0 fi + + if [[ "$pve_ver" =~ ^9\.([0-9]+) ]]; then + local minor="${BASH_REMATCH[1]}" + if ((minor < 0 || minor > 1)); then + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported: Proxmox VE version 9.0 – 9.1" + exit 105 + fi + return 0 + fi + + msg_error "This version of Proxmox VE is not supported." + msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.1" + exit 105 } arch_check() { @@ -576,12 +612,427 @@ arch_check() { fi } +ssh_check() { + if command -v pveversion >/dev/null 2>&1 && [ -n "${SSH_CLIENT:-}" ]; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then + : + else + clear + exit + fi + fi +} + exit_script() { clear echo -e "\n${CROSS}${RD}User exited script${CL}\n" exit } +sanitize_vm_hostname() { + local hostname="${1,,}" + hostname=$(echo "$hostname" | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//') + echo "${hostname:0:63}" +} + +vm_confirm_new_vm() { + local title="$1" + local message="$2" + local height="${3:-10}" + local width="${4:-58}" + + whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" --yesno "$message" "$height" "$width" +} + +vm_choose_settings_mode() { + local message="${1:-Use Default Settings?}" + local height="${2:-10}" + local width="${3:-58}" + + whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "$message" --no-button Advanced "$height" "$width" +} + +vm_confirm_advanced_settings() { + local message="$1" + local height="${2:-10}" + local width="${3:-58}" + + whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "$message" --no-button Do-Over "$height" "$width" +} + +vm_prompt_vmid() { + local default_vmid="${1:-$(get_valid_nextid)}" + + while true; do + if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 "$default_vmid" --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$VMID" ]; then + VMID=$(get_valid_nextid) + fi + if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then + echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" + sleep 2 + continue + fi + echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" + break + else + exit_script + fi + done +} + +vm_apply_machine_type() { + local machine_type="${1:-i440fx}" + + if [ "$machine_type" = "q35" ]; then + MACHINE_TYPE="q35" + FORMAT="" + MACHINE=" -machine q35" + else + MACHINE_TYPE="i440fx" + FORMAT=",efitype=4m" + MACHINE="" + fi +} + +vm_prompt_machine_type() { + local default_machine="${1:-i440fx}" + local i440fx_default="ON" + local q35_default="OFF" + local machine_choice + + if [ "$default_machine" = "q35" ]; then + i440fx_default="OFF" + q35_default="ON" + fi + + if machine_choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \ + "i440fx" "Machine i440fx" "$i440fx_default" \ + "q35" "Machine q35" "$q35_default" \ + 3>&1 1>&2 2>&3); then + vm_apply_machine_type "$machine_choice" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}${MACHINE_TYPE}${CL}" + else + exit_script + fi +} + +vm_prompt_disk_size() { + local default_size="${1:-8G}" + local prompt_message="${2:-Set Disk Size in GiB (e.g., 10, 20)}" + + if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "$prompt_message" 8 58 "$default_size" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ') + if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then + DISK_SIZE="${DISK_SIZE}G" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}" + elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}" + else + echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}" + exit_script + fi + else + exit_script + fi +} + +vm_prompt_disk_cache() { + local default_cache="${1:-none}" + local none_default="ON" + local write_default="OFF" + local cache_choice + + if [ "$default_cache" = "writethrough" ]; then + none_default="OFF" + write_default="ON" + fi + + if cache_choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \ + "0" "None (Default)" "$none_default" \ + "1" "Write Through" "$write_default" \ + 3>&1 1>&2 2>&3); then + if [ "$cache_choice" = "1" ]; then + DISK_CACHE="cache=writethrough," + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}" + else + DISK_CACHE="" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}" + fi + else + exit_script + fi +} + +vm_prompt_hostname() { + local default_hostname="${1:-vm}" + local adjusted_hostname + local input_hostname + + if input_hostname=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$default_hostname" --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$input_hostname" ]; then + HN="$default_hostname" + else + adjusted_hostname=$(sanitize_vm_hostname "$input_hostname") + HN="${adjusted_hostname:-$default_hostname}" + if [ "$HN" != "${input_hostname,,}" ]; then + whiptail --backtitle "Proxmox VE Helper Scripts" --title "HOSTNAME ADJUSTED" --msgbox "Invalid characters detected. Hostname has been adjusted to:\n\n $HN" 10 58 + fi + fi + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + else + exit_script + fi +} + +vm_prompt_cpu_model() { + local default_model="${1:-kvm64}" + local kvm_default="ON" + local host_default="OFF" + local cpu_choice + + if [ "$default_model" = "host" ]; then + kvm_default="OFF" + host_default="ON" + fi + + if cpu_choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \ + "0" "KVM64 (Default)" "$kvm_default" \ + "1" "Host" "$host_default" \ + 3>&1 1>&2 2>&3); then + if [ "$cpu_choice" = "1" ]; then + CPU_TYPE=" -cpu host" + echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}" + else + CPU_TYPE="" + echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}" + fi + else + exit_script + fi +} + +vm_prompt_cpu_cores() { + local default_cores="${1:-2}" + + while true; do + if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 "$default_cores" --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$CORE_COUNT" ]; then + CORE_COUNT="$default_cores" + fi + if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + break + fi + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "CPU Cores must be a positive integer (e.g., 2)." 8 58 + else + exit_script + fi + done +} + +vm_prompt_ram() { + local default_ram="${1:-2048}" + + while true; do + if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 "$default_ram" --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$RAM_SIZE" ]; then + RAM_SIZE="$default_ram" + fi + if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}" + break + fi + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "RAM Size must be a positive integer in MiB (e.g., 2048)." 8 58 + else + exit_script + fi + done +} + +vm_prompt_bridge() { + local default_bridge="${1:-vmbr0}" + + if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 "$default_bridge" --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$BRG" ]; then + BRG="$default_bridge" + fi + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + else + exit_script + fi +} + +vm_prompt_mac() { + local default_mac="${1:-$GEN_MAC}" + local input_mac + + while true; do + if input_mac=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 "$default_mac" --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$input_mac" ]; then + MAC="$default_mac" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}" + break + fi + if [[ "$input_mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then + MAC="$input_mac" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}" + break + fi + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "Invalid MAC address format. Use XX:XX:XX:XX:XX:XX (e.g., AA:BB:CC:DD:EE:FF)." 8 58 + else + exit_script + fi + done +} + +vm_prompt_vlan() { + local default_vlan="${1:-}" + local input_vlan + + while true; do + if input_vlan=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan (leave blank for default)" 8 58 "$default_vlan" --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$input_vlan" ]; then + VLAN="" + echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}" + break + fi + if [[ "$input_vlan" =~ ^[0-9]+$ ]] && [ "$input_vlan" -ge 1 ] && [ "$input_vlan" -le 4094 ]; then + VLAN=",tag=$input_vlan" + echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$input_vlan${CL}" + break + fi + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "VLAN must be a number between 1 and 4094, or leave blank for default." 8 58 + else + exit_script + fi + done +} + +vm_prompt_mtu() { + local default_mtu="${1:-}" + local input_mtu + + while true; do + if input_mtu=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 "$default_mtu" --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then + if [ -z "$input_mtu" ]; then + MTU="" + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}" + break + fi + if [[ "$input_mtu" =~ ^[0-9]+$ ]] && [ "$input_mtu" -ge 576 ] && [ "$input_mtu" -le 65520 ]; then + MTU=",mtu=$input_mtu" + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$input_mtu${CL}" + break + fi + whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "MTU Size must be a number between 576 and 65520, or leave blank for default." 8 58 + else + exit_script + fi + done +} + +vm_prompt_start_vm() { + local default_start="${1:-yes}" + local default_flag=() + + if [ "$default_start" = "no" ]; then + default_flag=(--defaultno) + fi + + if whiptail --backtitle "Proxmox VE Helper Scripts" "${default_flag[@]}" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58; then + START_VM="yes" + else + START_VM="no" + fi + + echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}" +} + +vm_apply_storage_layout() { + local storage_type="$1" + + case $storage_type in + nfs | dir | cifs) + DISK_EXT=".qcow2" + DISK_REF="$VMID/" + DISK_IMPORT_FORMAT="qcow2" + THIN="" + ;; + btrfs) + DISK_EXT=".raw" + DISK_REF="$VMID/" + DISK_IMPORT_FORMAT="raw" + FORMAT=",efitype=4m" + THIN="" + ;; + *) + DISK_EXT="" + DISK_REF="" + DISK_IMPORT_FORMAT="raw" + ;; + esac +} + +vm_select_storage() { + local hostname="${1:-${HN:-vm}}" + local storage_menu=() + local msg_max_length=0 + local line tag type free item + local offset=2 + local valid_storage + + msg_info "Validating Storage" + + while read -r line; do + tag=$(echo "$line" | awk '{print $1}') + type=$(echo "$line" | awk '{printf "%-10s", $2}') + free=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}') + item=" Type: $type Free: $free " + if [[ $((${#item} + offset)) -gt $msg_max_length ]]; then + msg_max_length=$((${#item} + offset)) + fi + storage_menu+=("$tag" "$item" "OFF") + done < <(pvesm status -content images | awk 'NR>1') + + valid_storage=$(pvesm status -content images | awk 'NR>1') + if [ -z "$valid_storage" ]; then + msg_error "Unable to detect a valid storage location." + exit + elif [ $((${#storage_menu[@]} / 3)) -eq 1 ]; then + STORAGE=${storage_menu[0]} + else + if [ -n "${SPINNER_PID:-}" ] && ps -p "$SPINNER_PID" >/dev/null 2>&1; then + kill "$SPINNER_PID" >/dev/null 2>&1 || true + SPINNER_ACTIVE=0 + printf "\r\e[2K" >&2 + fi + while [ -z "${STORAGE:+x}" ]; do + STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ + "Which storage pool would you like to use for ${hostname}?\nTo make a selection, use the Spacebar.\n" \ + 16 $(($msg_max_length + 23)) 6 \ + "${storage_menu[@]}" 3>&1 1>&2 2>&3) + done + fi + + msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." + msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}." + + STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}') + vm_apply_storage_layout "$STORAGE_TYPE" +} + +vm_define_disk_references() { + local disk_count="${1:-2}" + local i disk_name + + for ((i = 0; i < disk_count; i++)); do + disk_name="vm-${VMID}-disk-${i}${DISK_EXT:-}" + printf -v "DISK${i}" '%s' "$disk_name" + printf -v "DISK${i}_REF" '%s' "${STORAGE}:${DISK_REF:-}${disk_name}" + done +} + check_hostname_conflict() { local hostname="$1" if qm list | awk '{print $2}' | grep -qx "$hostname"; then @@ -591,6 +1042,8 @@ check_hostname_conflict() { } set_description() { + local description_title="${APP:-${NSAPP} VM}" + DESCRIPTION=$( cat < @@ -598,7 +1051,7 @@ set_description() { Logo -

${NSAPP} VM

+

${description_title}