Introduce optional Cloud-Init integration and better machine type handling for VMs. Changes include: loading cloud-init helpers lazily (load_cloud_init_functions), interactive cloud-init prompt/configuration (vm_prompt_cloud_init) and SSH key handling, a cloud icon for UI, and vm_machine_type_label for readable machine type display. Default machine type switched to q35 and displays the label in prompts and summaries. VM creation logic now conditionally attaches the cloudinit drive (ide2) and runs setup_cloud_init when enabled; otherwise it creates the VM without the cloudinit device. Post-install messaging now either shows cloud-init details or a guidance message about manual guest filesystem expansion. Minor UI/output adjustments and defaults updated accordingly.
1129 lines
33 KiB
Bash
1129 lines
33 KiB
Bash
# Copyright (c) 2021-2026 community-scripts ORG
|
||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE
|
||
|
||
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
|
||
|
||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://git.community-scripts.org/community-scripts/ProxmoxVED/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
|
||
}
|
||
|
||
load_functions() {
|
||
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
||
__FUNCTIONS_LOADED=1
|
||
load_api_functions
|
||
color
|
||
formatting
|
||
icons
|
||
default_vars
|
||
set_std_mode
|
||
shell_check
|
||
get_valid_nextid
|
||
cleanup_vmid
|
||
cleanup
|
||
check_root
|
||
pve_check
|
||
arch_check
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
# Function to download & save header files
|
||
get_header() {
|
||
local app_name=$(echo "${APP,,}" | tr ' ' '-')
|
||
local app_type=${APP_TYPE:-vm}
|
||
local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/${app_type}/headers/${app_name}"
|
||
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
|
||
return 1
|
||
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}"
|
||
CLOUD="${TAB}☁️${TAB}${CL}"
|
||
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
|
||
# - On error: displays last 20 lines of log and exits with original exit code
|
||
# - 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
|
||
# 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
|
||
|
||
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
|
||
|
||
exit "$rc"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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"
|
||
exit 1
|
||
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"
|
||
exit 1 # hard exit if exit_code is not 0
|
||
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
|
||
exit 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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
|
||
exit
|
||
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
|
||
# - Also returns true if not running in TTY (pipe/redirect scenario)
|
||
# ------------------------------------------------------------------------------
|
||
is_verbose_mode() {
|
||
local verbose="${VERBOSE:-${var_verbose:-no}}"
|
||
[[ "$verbose" != "no" || ! -t 2 ]]
|
||
}
|
||
|
||
### 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
|
||
}
|
||
|
||
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"
|
||
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() {
|
||
local exit_code=$?
|
||
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
|
||
if [[ $exit_code -ne 0 ]]; then
|
||
post_update_to_api "failed" "$exit_code"
|
||
else
|
||
# Exited cleanly but description()/success was never called — shouldn't happen
|
||
post_update_to_api "failed" "1"
|
||
fi
|
||
fi
|
||
fi
|
||
}
|
||
|
||
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
|
||
exit
|
||
fi
|
||
}
|
||
|
||
pve_check() {
|
||
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() {
|
||
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
|
||
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
|
||
echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
|
||
echo -e "Exiting..."
|
||
sleep 2
|
||
exit
|
||
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_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
|
||
}
|
||
|
||
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."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
set_description() {
|
||
local description_title="${APP:-${NSAPP} VM}"
|
||
|
||
DESCRIPTION=$(
|
||
cat <<EOF
|
||
<div align='center'>
|
||
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
|
||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||
</a>
|
||
|
||
<h2 style='font-size: 24px; margin: 20px 0;'>${description_title}</h2>
|
||
|
||
<p style='margin: 16px 0;'>
|
||
<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' />
|
||
</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/ProxmoxVED' 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/ProxmoxVED/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/ProxmoxVED/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||
</span>
|
||
</div>
|
||
EOF
|
||
)
|
||
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
|
||
|
||
}
|