2026-02-04 11:16:06 +01:00
# Copyright (c) 2021-2026 community-scripts ORG
2026-06-25 13:13:29 +02:00
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
2026-02-04 11:16:06 +01:00
set -euo pipefail
SPINNER_PID = ""
SPINNER_ACTIVE = 0
SPINNER_MSG = ""
declare -A MSG_INFO_SHOWN
# ------------------------------------------------------------------------------
# Loads core utility groups once (colors, formatting, icons, defaults).
# ------------------------------------------------------------------------------
[[ -n " ${ _CORE_FUNC_LOADED :- } " ]] && return
_CORE_FUNC_LOADED = 1
2026-06-25 13:13:29 +02:00
COMMUNITY_SCRIPTS_URL = " ${ COMMUNITY_SCRIPTS_URL :- https ://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main } "
load_api_functions() {
if ! declare -f post_to_api_vm >/dev/null 2>& 1; then
source /dev/stdin <<< $( curl -fsSL " $COMMUNITY_SCRIPTS_URL /misc/api.func" )
fi
}
2026-02-04 11:16:06 +01:00
load_functions() {
[[ -n " ${ __FUNCTIONS_LOADED :- } " ]] && return
__FUNCTIONS_LOADED = 1
2026-06-25 13:13:29 +02:00
load_api_functions
2026-02-04 11:16:06 +01:00
color
formatting
icons
default_vars
set_std_mode
shell_check
get_valid_nextid
cleanup_vmid
cleanup
check_root
pve_check
arch_check
}
2026-06-25 13:13:29 +02:00
load_cloud_init_functions() {
if ! declare -f setup_cloud_init >/dev/null 2>& 1; then
source <( curl -fsSL " $COMMUNITY_SCRIPTS_URL /misc/cloud-init.func" ) 2>/dev/null || true
fi
}
2026-02-04 11:16:06 +01:00
# Function to download & save header files
get_header() {
local app_name = $( echo " ${ APP ,, } " | tr ' ' '-' )
local app_type = ${ APP_TYPE :- vm }
2026-06-25 13:13:29 +02:00
local header_url = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ ${ app_type } /headers/ ${ app_name } "
2026-02-04 11:16:06 +01:00
local local_header_path = "/usr/local/community-scripts/headers/ ${ app_type } / ${ app_name } "
mkdir -p " $( dirname " $local_header_path " ) "
if [ ! -s " $local_header_path " ] ; then
if ! curl -fsSL " $header_url " -o " $local_header_path " ; then
2026-06-25 13:13:29 +02:00
return 1
2026-02-04 11:16:06 +01:00
fi
fi
cat " $local_header_path " 2>/dev/null || true
}
header_info() {
local app_name = $( echo " ${ APP ,, } " | tr ' ' '-' )
local header_content
header_content = $( get_header " $app_name " ) || header_content = ""
clear
local term_width
term_width = $( tput cols 2>/dev/null || echo 120)
if [ -n " $header_content " ] ; then
echo " $header_content "
fi
}
# ------------------------------------------------------------------------------
# Sets ANSI color codes used for styled terminal output.
# ------------------------------------------------------------------------------
color() {
YW = $( echo "\033[33m" )
YWB = $( echo "\033[93m" )
BL = $( echo "\033[36m" )
RD = $( echo "\033[01;31m" )
BGN = $( echo "\033[4;92m" )
GN = $( echo "\033[1;92m" )
DGN = $( echo "\033[32m" )
CL = $( echo "\033[m" )
}
# ------------------------------------------------------------------------------
# Defines formatting helpers like tab, bold, and line reset sequences.
# ------------------------------------------------------------------------------
formatting() {
BFR = "\\r\\033[K"
BOLD = $( echo "\033[1m" )
HOLD = " "
TAB = " "
TAB3 = " "
}
# ------------------------------------------------------------------------------
# Sets symbolic icons used throughout user feedback and prompts.
# ------------------------------------------------------------------------------
icons() {
CM = " ${ TAB } ✔️ ${ TAB } "
CROSS = " ${ TAB } ✖️ ${ TAB } "
DNSOK = "✔️ "
DNSFAIL = " ${ TAB } ✖️ ${ TAB } "
INFO = " ${ TAB } 💡 ${ TAB }${ CL } "
2026-06-25 13:13:29 +02:00
CLOUD = " ${ TAB } ☁️ ${ TAB }${ CL } "
2026-02-04 11:16:06 +01:00
OS = " ${ TAB } 🖥️ ${ TAB }${ CL } "
OSVERSION = " ${ TAB } 🌟 ${ TAB }${ CL } "
CONTAINERTYPE = " ${ TAB } 📦 ${ TAB }${ CL } "
DISKSIZE = " ${ TAB } 💾 ${ TAB }${ CL } "
CPUCORE = " ${ TAB } 🧠 ${ TAB }${ CL } "
RAMSIZE = " ${ TAB } 🛠️ ${ TAB }${ CL } "
SEARCH = " ${ TAB } 🔍 ${ TAB }${ CL } "
VERBOSE_CROPPED = "🔍 ${ TAB } "
VERIFYPW = " ${ TAB } 🔐 ${ TAB }${ CL } "
CONTAINERID = " ${ TAB } 🆔 ${ TAB }${ CL } "
HOSTNAME = " ${ TAB } 🏠 ${ TAB }${ CL } "
BRIDGE = " ${ TAB } 🌉 ${ TAB }${ CL } "
NETWORK = " ${ TAB } 📡 ${ TAB }${ CL } "
GATEWAY = " ${ TAB } 🌐 ${ TAB }${ CL } "
DISABLEIPV6 = " ${ TAB } 🚫 ${ TAB }${ CL } "
ICON_DISABLEIPV6 = " ${ TAB } 🚫 ${ TAB }${ CL } "
DEFAULT = " ${ TAB } ⚙️ ${ TAB }${ CL } "
MACADDRESS = " ${ TAB } 🔗 ${ TAB }${ CL } "
VLANTAG = " ${ TAB } 🏷️ ${ TAB }${ CL } "
ROOTSSH = " ${ TAB } 🔑 ${ TAB }${ CL } "
CREATING = " ${ TAB } 🚀 ${ TAB }${ CL } "
ADVANCED = " ${ TAB } 🧩 ${ TAB }${ CL } "
FUSE = " ${ TAB } 🗂️ ${ TAB }${ CL } "
GPU = " ${ TAB } 🎮 ${ TAB }${ CL } "
HOURGLASS = " ${ TAB } ⏳ ${ TAB } "
}
# ------------------------------------------------------------------------------
# Sets default verbose mode for script and os execution.
# ------------------------------------------------------------------------------
set_std_mode() {
if [ " ${ VERBOSE :- no } " = "yes" ] ; then
STD = ""
else
STD = "silent"
fi
}
# ------------------------------------------------------------------------------
# default_vars()
#
# - Sets default retry and wait variables used for system actions
# - RETRY_NUM: Maximum number of retry attempts (default: 10)
# - RETRY_EVERY: Seconds to wait between retries (default: 3)
# ------------------------------------------------------------------------------
default_vars() {
RETRY_NUM = 10
RETRY_EVERY = 3
i = $RETRY_NUM
}
# ------------------------------------------------------------------------------
# get_active_logfile()
#
# - Returns the appropriate log file based on execution context
# - BUILD_LOG: Host operations (VM creation)
# - Fallback to /tmp/build-<timestamp>.log if not set
# ------------------------------------------------------------------------------
get_active_logfile() {
if [[ -n " ${ BUILD_LOG :- } " ]] ; then
echo " $BUILD_LOG "
else
# Fallback for legacy scripts
echo "/tmp/build- $( date +%Y%m%d_%H%M%S) .log"
fi
}
# ------------------------------------------------------------------------------
# silent()
#
# - Executes command with output redirected to active log file
2026-02-23 17:01:18 +01:00
# - On error: displays last 20 lines of log and exits with original exit code
2026-02-04 11:16:06 +01:00
# - Temporarily disables error trap to capture exit code correctly
# - Sources explain_exit_code() for detailed error messages
# ------------------------------------------------------------------------------
silent() {
local cmd = " $* "
local caller_line = " ${ BASH_LINENO [0] :- unknown } "
local logfile = " $( get_active_logfile) "
set +Eeuo pipefail
trap - ERR
" $@ " >>" $logfile " 2>& 1
local rc = $?
set -Eeuo pipefail
trap 'error_handler' ERR
if [[ $rc -ne 0 ]] ; then
2026-06-25 13:13:29 +02:00
# Source explain_exit_code if needed
if ! declare -f explain_exit_code >/dev/null 2>& 1; then
source <( curl -fsSL " $COMMUNITY_SCRIPTS_URL /misc/error_handler.func" ) 2>/dev/null || true
fi
2026-04-05 21:02:56 +02:00
2026-06-25 13:13:29 +02:00
local explanation = ""
if declare -f explain_exit_code >/dev/null 2>& 1; then
explanation = " $( explain_exit_code " $rc " ) "
fi
printf "\e[?25h"
if [[ -n " $explanation " ]] ; then
msg_error "in line ${ caller_line } : exit code ${ rc } ( ${ explanation } )"
else
msg_error "in line ${ caller_line } : exit code ${ rc } "
fi
msg_custom "→" " ${ YWB } " " ${ cmd } "
if [[ -s " $logfile " ]] ; then
echo -e "\n ${ TAB } --- Last 20 lines of log ---"
tail -n 20 " $logfile "
echo -e " ${ TAB } ----------------------------\n"
fi
2026-04-05 21:02:56 +02:00
2026-06-25 13:13:29 +02:00
exit " $rc "
fi
2026-02-04 11:16:06 +01:00
}
# ------------------------------------------------------------------------------
# Performs a curl request with retry logic and inline feedback.
# ------------------------------------------------------------------------------
run_curl() {
if [ " $VERB " = "no" ] ; then
curl " $@ " >/dev/null 2>>/tmp/curl_error.log
else
curl " $@ " 2>>/tmp/curl_error.log
fi
}
curl_handler() {
local args =()
local url = ""
local max_retries = 0 delay = 2 attempt = 1
local exit_code has_output_file = false
for arg in " $@ " ; do
if [[ " $arg " != -* && -z " $url " ]] ; then
url = " $arg "
fi
[[ " $arg " == "-o" || " $arg " == --output ]] && has_output_file = true
args +=( " $arg " )
done
if [[ -z " $url " ]] ; then
msg_error "no valid url or option entered for curl_handler"
2026-06-25 13:13:29 +02:00
exit 1
2026-02-04 11:16:06 +01:00
fi
$STD msg_info "Fetching: $url "
while :; do
if $has_output_file ; then
$STD run_curl " ${ args [@] } "
exit_code = $?
else
$STD result = $( run_curl " ${ args [@] } " )
exit_code = $?
fi
if [[ $exit_code -eq 0 ]] ; then
stop_spinner
msg_ok "Fetched: $url "
$has_output_file || printf '%s' " $result "
return 0
fi
if (( attempt >= max_retries)) ; then
stop_spinner
if [ -s /tmp/curl_error.log ] ; then
local curl_stderr
curl_stderr = $( </tmp/curl_error.log)
rm -f /tmp/curl_error.log
fi
__curl_err_handler " $exit_code " " $url " " $curl_stderr "
2026-06-25 13:13:29 +02:00
exit 1 # hard exit if exit_code is not 0
2026-02-04 11:16:06 +01:00
fi
$STD printf "\r\033[K ${ INFO }${ YW } Retry $attempt / $max_retries in ${ delay } s... ${ CL } " >& 2
sleep " $delay "
(( attempt++))
done
}
# ------------------------------------------------------------------------------
# Handles specific curl error codes and displays descriptive messages.
# ------------------------------------------------------------------------------
__curl_err_handler() {
local exit_code = " $1 "
local target = " $2 "
local curl_msg = " $3 "
case $exit_code in
1) msg_error "Unsupported protocol: $target " ;;
2) msg_error "Curl init failed: $target " ;;
3) msg_error "Malformed URL: $target " ;;
5) msg_error "Proxy resolution failed: $target " ;;
6) msg_error "Host resolution failed: $target " ;;
7) msg_error "Connection failed: $target " ;;
9) msg_error "Access denied: $target " ;;
18) msg_error "Partial file transfer: $target " ;;
22) msg_error "HTTP error (e.g. 400/404): $target " ;;
23) msg_error "Write error on local system: $target " ;;
26) msg_error "Read error from local file: $target " ;;
28) msg_error "Timeout: $target " ;;
35) msg_error "SSL connect error: $target " ;;
47) msg_error "Too many redirects: $target " ;;
51) msg_error "SSL cert verify failed: $target " ;;
52) msg_error "Empty server response: $target " ;;
55) msg_error "Send error: $target " ;;
56) msg_error "Receive error: $target " ;;
60) msg_error "SSL CA not trusted: $target " ;;
67) msg_error "Login denied by server: $target " ;;
78) msg_error "Remote file not found (404): $target " ;;
*) msg_error "Curl failed with code $exit_code : $target " ;;
esac
[[ -n " $curl_msg " ]] && printf "%s\n" " $curl_msg " >& 2
2026-06-25 13:13:29 +02:00
exit 1
2026-02-04 11:16:06 +01:00
}
# ------------------------------------------------------------------------------
# shell_check()
#
# - Verifies that the script is running under Bash shell
# - Exits with error message if different shell is detected
# ------------------------------------------------------------------------------
shell_check() {
if [[ " $( ps -p $$ -o comm = ) " != "bash" ]] ; then
clear
msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell."
echo -e "\nExiting..."
sleep 2
2026-06-25 13:13:29 +02:00
exit
2026-02-04 11:16:06 +01:00
fi
}
# ------------------------------------------------------------------------------
# clear_line()
#
# - Clears current terminal line using tput or ANSI escape codes
# - Moves cursor to beginning of line (carriage return)
# - Fallback to ANSI codes if tput not available
# ------------------------------------------------------------------------------
clear_line() {
tput cr 2>/dev/null || echo -en "\r"
tput el 2>/dev/null || echo -en "\033[K"
}
# ------------------------------------------------------------------------------
# is_verbose_mode()
#
# - Determines if script should run in verbose mode
# - Checks VERBOSE and var_verbose variables
2026-06-25 13:13:29 +02:00
# - Also returns true if not running in TTY (pipe/redirect scenario)
2026-02-04 11:16:06 +01:00
# ------------------------------------------------------------------------------
is_verbose_mode() {
local verbose = " ${ VERBOSE :- ${ var_verbose :- no }} "
2026-06-25 13:13:29 +02:00
[[ " $verbose " != "no" || ! -t 2 ]]
2026-02-04 11:16:06 +01:00
}
### dev spinner ###
SPINNER_ACTIVE = 0
SPINNER_PID = ""
SPINNER_MSG = ""
declare -A MSG_INFO_SHOWN =()
# Trap cleanup on various signals
trap 'cleanup_spinner' EXIT INT TERM HUP
# Cleans up spinner process on exit
cleanup_spinner() {
stop_spinner
# Additional cleanup if needed
}
start_spinner() {
local msg = " ${ 1 :- Processing ... } "
local frames =( ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
local spin_i = 0
local interval = 0.1
# Set message and clear current line
SPINNER_MSG = " $msg "
printf "\r\e[2K" >& 2
# Stop any existing spinner
stop_spinner
# Set active flag
SPINNER_ACTIVE = 1
# Start spinner in background
{
while [[ " $SPINNER_ACTIVE " -eq 1 ]] ; do
printf "\r\e[2K%s %b" " ${ TAB }${ frames [spin_i] }${ TAB } " " ${ YW }${ SPINNER_MSG }${ CL } " >& 2
spin_i = $(( ( spin_i + 1 ) % ${# frames [@] } ))
sleep " $interval "
done
} &
SPINNER_PID = $!
# Disown to prevent getting "Terminated" messages
disown " $SPINNER_PID " 2>/dev/null || true
}
stop_spinner() {
# Check if spinner is active and PID exists
if [[ " $SPINNER_ACTIVE " -eq 1 ]] && [[ -n " ${ SPINNER_PID } " ]] ; then
SPINNER_ACTIVE = 0
if kill -0 " $SPINNER_PID " 2>/dev/null; then
kill " $SPINNER_PID " 2>/dev/null
# Give it a moment to terminate
sleep 0.1
# Force kill if still running
if kill -0 " $SPINNER_PID " 2>/dev/null; then
kill -9 " $SPINNER_PID " 2>/dev/null
fi
# Wait for process but ignore errors
wait " $SPINNER_PID " 2>/dev/null || true
fi
# Clear spinner line
printf "\r\e[2K" >& 2
SPINNER_PID = ""
fi
}
spinner_guard() {
# Safely stop spinner if it's running
if [[ " $SPINNER_ACTIVE " -eq 1 ]] && [[ -n " ${ SPINNER_PID } " ]] ; then
stop_spinner
fi
}
msg_info() {
local msg = " ${ 1 :- Information message } "
# Only show each message once unless reset
if [[ -n " ${ MSG_INFO_SHOWN [ " $msg " ]+x } " ]] ; then
return
fi
MSG_INFO_SHOWN[ " $msg " ]= 1
spinner_guard
start_spinner " $msg "
}
msg_ok() {
local msg = " ${ 1 :- Operation completed successfully } "
stop_spinner
printf "\r\e[2K%s %b\n" " ${ CM } " " ${ GN }${ msg }${ CL } " >& 2
# Remove from shown messages to allow it to be shown again
local sanitized_msg
sanitized_msg = $( printf '%s' " $msg " | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g' )
unset 'MSG_INFO_SHOWN[' " $sanitized_msg " ']' 2>/dev/null || true
}
msg_error() {
local msg = " ${ 1 :- An error occurred } "
stop_spinner
printf "\r\e[2K%s %b\n" " ${ CROSS } " " ${ RD }${ msg }${ CL } " >& 2
}
msg_warn() {
stop_spinner
local msg = " $1 "
echo -e " ${ BFR :- }${ INFO :- ℹ ️} ${ YWB }${ msg }${ CL } " >& 2
}
# Helper function to display a message with custom symbol and color
msg_custom() {
local symbol = " ${ 1 :- * } "
local color = " ${ 2 :- $CL } "
local msg = " ${ 3 :- Custom message } "
[[ -z " $msg " ]] && return
stop_spinner
printf "\r\e[2K%s %b\n" " $symbol " " ${ color }${ msg }${ CL } " >& 2
}
# ------------------------------------------------------------------------------
# msg_debug()
#
# - Displays debug message with timestamp when var_full_verbose=1
# - Automatically enables var_verbose if not already set
# - Uses bright yellow color for debug output
# ------------------------------------------------------------------------------
msg_debug() {
if [[ " ${ var_full_verbose :- 0 } " == "1" ]] ; then
[[ " ${ var_verbose :- 0 } " != "1" ]] && var_verbose = 1
echo -e " ${ YWB } [ $( date '+%F %T' ) ] [DEBUG] ${ CL } $* "
fi
}
2026-06-25 13:13:29 +02:00
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
}
2026-02-04 11:16:06 +01:00
# Displays error message and immediately terminates script
fatal() {
msg_error " $1 "
kill -INT $$
}
get_valid_nextid() {
local try_id
try_id = $( pvesh get /cluster/nextid)
while true; do
if [ -f "/etc/pve/qemu-server/ ${ try_id } .conf" ] || [ -f "/etc/pve/lxc/ ${ try_id } .conf" ] ; then
try_id = $(( try_id + 1 ))
continue
fi
if lvs --noheadings -o lv_name | grep -qE "(^|[-_]) ${ try_id } ( $|[-_])" ; then
try_id = $(( try_id + 1 ))
continue
fi
break
done
echo " $try_id "
}
cleanup_vmid() {
if [[ -z " ${ VMID :- } " ]] ; then
return
fi
if qm status " $VMID " & >/dev/null; then
qm stop " $VMID " & >/dev/null
qm destroy " $VMID " & >/dev/null
fi
}
cleanup() {
2026-02-16 17:14:00 +01:00
local exit_code = $?
2026-06-25 13:13:29 +02:00
stop_spinner
2026-02-04 11:16:06 +01:00
if [[ " $( dirs -p | wc -l) " -gt 1 ]] ; then
popd >/dev/null || true
fi
2026-06-25 13:13:29 +02:00
if [[ -n " ${ TEMP_DIR :- } " && -d " $TEMP_DIR " ]] ; then
rm -rf " $TEMP_DIR "
fi
2026-02-16 17:14:00 +01:00
# 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
if [[ $exit_code -ne 0 ]] ; then
post_update_to_api "failed" " $exit_code "
else
2026-07-20 20:32:36 +02:00
# Exited cleanly but description()/success was never called: the user
# backed out of a dialog. Report as aborted - NOT "failed 1" (which
# produced meaningless 'General error' records with no error text).
post_update_to_api "aborted" "0"
2026-02-16 17:14:00 +01:00
fi
fi
fi
2026-02-04 11:16:06 +01:00
}
check_root() {
if [[ " $( id -u) " -ne 0 || $( ps -o comm = -p $PPID ) == "sudo" ]] ; then
clear
msg_error "Please run this script as root."
echo -e "\nExiting..."
sleep 2
2026-06-25 13:13:29 +02:00
exit
2026-02-04 11:16:06 +01:00
fi
}
pve_check() {
2026-06-25 13:13:29 +02:00
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 > 2)) ; then
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported: Proxmox VE version 9.0 – 9.2"
exit 105
fi
return 0
2026-02-04 11:16:06 +01:00
fi
2026-06-25 13:13:29 +02:00
msg_error "This version of Proxmox VE is not supported."
msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.2"
exit 105
2026-02-04 11:16:06 +01:00
}
arch_check() {
2026-07-06 23:41:55 +10:00
local arch
arch = " $( dpkg --print-architecture) "
if [[ " $arch " != "amd64" && " $arch " != "arm64" ]] ; then
msg_error "This script requires amd64 or arm64."
2026-02-04 11:16:06 +01:00
echo -e "Exiting..."
sleep 2
2026-07-06 23:41:55 +10:00
exit 106
fi
if [[ " $arch " == "arm64" && " ${ var_arm64 :- } " != "yes" ]] ; then
echo -e "\n ${ INFO }${ YWB } This script does not yet support ARM64! \n"
echo -e "Exiting..."
sleep 2
exit 106
2026-06-25 13:13:29 +02:00
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
2026-02-04 11:16:06 +01:00
fi
}
exit_script() {
clear
echo -e "\n ${ CROSS }${ RD } User exited script ${ CL } \n"
2026-06-25 13:13:29 +02:00
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_machine_type_label() {
case " ${ 1 :- i440fx } " in
q35)
echo "Q35 (Modern)"
;;
*)
echo "i440fx"
;;
esac
}
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 } $( vm_machine_type_label " $MACHINE_TYPE " ) ${ CL } "
else
exit_script
fi
}
vm_prompt_cloud_init() {
local default_user = " ${ 1 :- root } "
USE_CLOUD_INIT = "no"
load_cloud_init_functions
if ! declare -f configure_cloud_init_interactive >/dev/null 2>& 1; then
echo -e " ${ CLOUD }${ BOLD }${ DGN } Cloud-Init: ${ BGN } unavailable ${ CL } "
return 1
fi
configure_cloud_init_interactive " $default_user " || true
USE_CLOUD_INIT = " ${ CLOUDINIT_ENABLE :- no } "
echo -e " ${ CLOUD }${ BOLD }${ DGN } Cloud-Init: ${ BGN }${ USE_CLOUD_INIT }${ CL } "
if [ " $USE_CLOUD_INIT " = "yes" ] && declare -f configure_cloudinit_ssh_keys >/dev/null 2>& 1; then
configure_cloudinit_ssh_keys || true
fi
return 0
}
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
2026-02-04 11:16:06 +01:00
}
check_hostname_conflict() {
local hostname = " $1 "
if qm list | awk '{print $2}' | grep -qx " $hostname " ; then
msg_error "Hostname $hostname already in use by another VM."
2026-06-25 13:13:29 +02:00
exit 1
2026-02-04 11:16:06 +01:00
fi
}
set_description() {
2026-06-25 13:13:29 +02:00
local description_title = " ${ APP :- ${ NSAPP } VM } "
2026-04-08 21:40:52 +02:00
2026-02-04 11:16:06 +01:00
DESCRIPTION = $(
cat <<EOF
<div align='center'>
2026-06-25 13:13:29 +02:00
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
2026-02-04 11:16:06 +01:00
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>
2026-06-25 13:13:29 +02:00
<h2 style='font-size: 24px; margin: 20px 0;'>${description_title}</h2>
2026-02-04 11:16:06 +01:00
<p style='margin: 16px 0;'>
2026-06-25 13:13:29 +02:00
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
2026-02-04 11:16:06 +01:00
</a>
</p>
<span style='margin: 0 10px;'>
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
</span>
</div>
EOF
)
qm set " $VMID " -description " $DESCRIPTION " >/dev/null
}