ProxmoxVED/misc/build.func
CanbiZ 17cb74a8f0
Some checks failed
Bump build.func Revision / bump-revision (push) Has been cancelled
Update build.func
2025-09-19 08:28:10 +02:00

3119 lines
109 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Copyright (c) 2021-2025 community-scripts ORG
# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Revision: 1
# ------------------------------------------------------------------------------
# variables()
#
# - Normalize application name (NSAPP = lowercase, no spaces)
# - Build installer filename (var_install)
# - Define regex for integer validation
# - Fetch hostname of Proxmox node
# - Set default values for diagnostics/method
# - Generate random UUID for tracking
# ------------------------------------------------------------------------------
variables() {
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
METHOD="default" # sets the METHOD variable to "default", used for the API call.
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
#CT_TYPE=${var_unprivileged:-$CT_TYPE}
}
# -----------------------------------------------------------------------------
# Community-Scripts bootstrap loader
# - Always sources build.func from remote
# - Updates local core files only if build.func changed
# - Local cache: /usr/local/community-scripts/core
# -----------------------------------------------------------------------------
FUNC_DIR="/usr/local/community-scripts/core"
mkdir -p "$FUNC_DIR"
BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func"
BUILD_REV="$FUNC_DIR/build.rev"
DEVMODE="${DEVMODE:-no}"
# --- Step 1: fetch build.func content once, compute hash ---
build_content="$(curl -fsSL "$BUILD_URL")" || {
echo "❌ Failed to fetch build.func"
exit 1
}
newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}')
oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "")
# --- Step 2: if build.func changed, offer update for core files ---
if [ "$newhash" != "$oldhash" ]; then
echo "⚠️ build.func changed!"
while true; do
read -rp "Refresh local core files? [y/N/diff]: " ans
case "$ans" in
[Yy]*)
echo "$newhash" >"$BUILD_REV"
update_func_file() {
local file="$1"
local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file"
local local_path="$FUNC_DIR/$file"
echo "⬇️ Downloading $file ..."
curl -fsSL "$url" -o "$local_path" || {
echo "❌ Failed to fetch $file"
exit 1
}
echo "✔️ Updated $file"
}
update_func_file core.func
update_func_file error_handler.func
update_func_file tools.func
break
;;
[Dd]*)
for file in core.func error_handler.func tools.func; do
local_path="$FUNC_DIR/$file"
url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file"
remote_tmp="$(mktemp)"
curl -fsSL "$url" -o "$remote_tmp" || continue
if [ -f "$local_path" ]; then
echo "🔍 Diff for $file:"
diff -u "$local_path" "$remote_tmp" || echo "(no differences)"
else
echo "📦 New file $file will be installed"
fi
rm -f "$remote_tmp"
done
;;
*)
echo "❌ Skipped updating local core files"
break
;;
esac
done
else
if [ "$DEVMODE" != "yes" ]; then
echo "✔️ build.func unchanged → using existing local core files"
fi
fi
if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then
return 0 2>/dev/null || exit 0
fi
_COMMUNITY_SCRIPTS_LOADER=1
# --- Step 3: always source local versions of the core files ---
source "$FUNC_DIR/core.func"
source "$FUNC_DIR/error_handler.func"
source "$FUNC_DIR/tools.func"
# --- Step 4: finally, source build.func directly from memory ---
# (no tmp file needed)
source <(printf "%s" "$build_content")
# ------------------------------------------------------------------------------
# Load core + error handler functions from community-scripts repo
#
# - Prefer curl if available, fallback to wget
# - Load: core.func, error_handler.func, api.func
# - Initialize error traps after loading
# ------------------------------------------------------------------------------
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func)
if command -v curl >/dev/null 2>&1; then
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
load_functions
init_error_traps
#echo "(build.func) Loaded core.func via curl"
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func)
source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func)
load_functions
init_error_traps
#echo "(build.func) Loaded core.func via wget"
fi
# ------------------------------------------------------------------------------
# maxkeys_check()
#
# - Reads kernel keyring limits (maxkeys, maxbytes)
# - Checks current usage for LXC user (UID 100000)
# - Warns if usage is close to limits and suggests sysctl tuning
# - Exits if thresholds are exceeded
# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
# ------------------------------------------------------------------------------
maxkeys_check() {
# Read kernel parameters
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
# Exit if kernel parameters are unavailable
if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
exit 1
fi
# Fetch key usage for user ID 100000 (typical for containers)
used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
# Calculate thresholds and suggested new limits
threshold_keys=$((per_user_maxkeys - 100))
threshold_bytes=$((per_user_maxbytes - 1000))
new_limit_keys=$((per_user_maxkeys * 2))
new_limit_bytes=$((per_user_maxbytes * 2))
# Check if key or byte usage is near limits
failure=0
if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
failure=1
fi
if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
failure=1
fi
# Provide next steps if issues are detected
if [[ "$failure" -eq 1 ]]; then
echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
exit 1
fi
echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
}
# ------------------------------------------------------------------------------
# get_current_ip()
#
# - Returns current container IP depending on OS type
# - Debian/Ubuntu: uses `hostname -I`
# - Alpine: parses eth0 via `ip -4 addr`
# ------------------------------------------------------------------------------
get_current_ip() {
if [ -f /etc/os-release ]; then
# Check for Debian/Ubuntu (uses hostname -I)
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
CURRENT_IP=$(hostname -I | awk '{print $1}')
# Check for Alpine (uses ip command)
elif grep -q 'ID=alpine' /etc/os-release; then
CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
else
CURRENT_IP="Unknown"
fi
fi
echo "$CURRENT_IP"
}
# ------------------------------------------------------------------------------
# update_motd_ip()
#
# - Updates /etc/motd with current container IP
# - Removes old IP entries to avoid duplicates
# ------------------------------------------------------------------------------
update_motd_ip() {
MOTD_FILE="/etc/motd"
if [ -f "$MOTD_FILE" ]; then
# Remove existing IP Address lines to prevent duplication
sed -i '/IP Address:/d' "$MOTD_FILE"
IP=$(get_current_ip)
# Add the new IP address
echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
fi
}
# ------------------------------------------------------------------------------
# ssh_check()
#
# - Detects if script is running over SSH
# - Warns user and recommends using Proxmox shell
# - User can choose to continue or abort
# ------------------------------------------------------------------------------
ssh_check() {
if [ -n "${SSH_CLIENT:+x}" ]; then
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72
else
clear
echo "Exiting due to SSH usage. Please consider using the Proxmox shell."
exit
fi
fi
}
# ------------------------------------------------------------------------------
# install_ssh_keys_into_ct()
#
# - Installs SSH keys into container root account if SSH is enabled
# - Uses pct push or direct input to authorized_keys
# - Falls back to warning if no keys provided
# ------------------------------------------------------------------------------
install_ssh_keys_into_ct() {
[[ "$SSH" != "yes" ]] && return 0
if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
msg_info "Installing selected SSH keys into CT ${CTID}"
pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || {
msg_error "prepare /root/.ssh failed"
return 1
}
pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
msg_error "write authorized_keys failed"
return 1
}
pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
msg_ok "Installed SSH keys into CT ${CTID}"
return 0
fi
# Fallback: nichts ausgewählt
msg_warn "No SSH keys to install (skipping)."
return 0
}
# ------------------------------------------------------------------------------
# base_settings()
#
# - Defines all base/default variables for container creation
# - Reads from environment variables (var_*)
# - Provides fallback defaults for OS type/version
# ------------------------------------------------------------------------------
base_settings() {
# Default Settings
CT_TYPE=${var_unprivileged:-"1"}
DISK_SIZE=${var_disk:-"4"}
CORE_COUNT=${var_cpu:-"1"}
RAM_SIZE=${var_ram:-"1024"}
VERBOSE=${var_verbose:-"${1:-no}"}
PW=${var_pw:-""}
CT_ID=${var_ctid:-$NEXTID}
HN=${var_hostname:-$NSAPP}
BRG=${var_brg:-"vmbr0"}
NET=${var_net:-"dhcp"}
IPV6_METHOD=${var_ipv6_method:-"none"}
IPV6_STATIC=${var_ipv6_static:-""}
GATE=${var_gateway:-""}
APT_CACHER=${var_apt_cacher:-""}
APT_CACHER_IP=${var_apt_cacher_ip:-""}
MTU=${var_mtu:-""}
SD=${var_storage:-""}
NS=${var_ns:-""}
MAC=${var_mac:-""}
VLAN=${var_vlan:-""}
SSH=${var_ssh:-"no"}
SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
UDHCPC_FIX=${var_udhcpc_fix:-""}
TAGS="community-script,${var_tags:-}"
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
ENABLE_TUN=${var_tun:-"${1:-no}"}
# Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
if [ -z "$var_os" ]; then
var_os="debian"
fi
if [ -z "$var_version" ]; then
var_version="12"
fi
}
# ------------------------------------------------------------------------------
# echo_default()
#
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
# - Uses icons and formatting for readability
# - Convert CT_TYPE to description
# ------------------------------------------------------------------------------
echo_default() {
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
if [ "$VERBOSE" == "yes" ]; then
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
fi
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
echo -e " "
}
# ------------------------------------------------------------------------------
# exit_script()
#
# - Called when user cancels an action
# - Clears screen and exits gracefully
# ------------------------------------------------------------------------------
exit_script() {
clear
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
exit
}
# ------------------------------------------------------------------------------
# find_host_ssh_keys()
#
# - Scans system for available SSH keys
# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
# - Returns list of files containing valid SSH public keys
# - Sets FOUND_HOST_KEY_COUNT to number of keys found
# ------------------------------------------------------------------------------
find_host_ssh_keys() {
local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
local -a files=() cand=()
local g="${var_ssh_import_glob:-}"
local total=0 f base c
shopt -s nullglob
if [[ -n "$g" ]]; then
for pat in $g; do cand+=($pat); done
else
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
cand+=(/root/.ssh/*.pub)
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
fi
shopt -u nullglob
for f in "${cand[@]}"; do
[[ -f "$f" && -r "$f" ]] || continue
base="$(basename -- "$f")"
case "$base" in
known_hosts | known_hosts.* | config) continue ;;
id_*) [[ "$f" != *.pub ]] && continue ;;
esac
# CRLF safe check for host keys
c=$(tr -d '\r' <"$f" | awk '
/^[[:space:]]*#/ {next}
/^[[:space:]]*$/ {next}
{print}
' | grep -E -c '"$re"' || true)
if ((c > 0)); then
files+=("$f")
total=$((total + c))
fi
done
# Fallback to /root/.ssh/authorized_keys
if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
if grep -E -q "$re" /root/.ssh/authorized_keys; then
files+=(/root/.ssh/authorized_keys)
total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
fi
fi
FOUND_HOST_KEY_COUNT="$total"
(
IFS=:
echo "${files[*]}"
)
}
# ------------------------------------------------------------------------------
# advanced_settings()
#
# - Interactive whiptail menu for advanced configuration
# - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM
# - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode
# - Ends with confirmation or re-entry if cancelled
# ------------------------------------------------------------------------------
advanced_settings() {
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58
# Setting Default Tag for Advanced Settings
TAGS="community-script;${var_tags:-}"
CT_DEFAULT_TYPE="${CT_TYPE}"
CT_TYPE=""
while [ -z "$CT_TYPE" ]; do
if [ "$CT_DEFAULT_TYPE" == "1" ]; then
if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
"1" "Unprivileged" ON \
"0" "Privileged" OFF \
3>&1 1>&2 2>&3); then
if [ -n "$CT_TYPE" ]; then
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os | ${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
fi
else
exit_script
fi
fi
if [ "$CT_DEFAULT_TYPE" == "0" ]; then
if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
"1" "Unprivileged" OFF \
"0" "Privileged" ON \
3>&1 1>&2 2>&3); then
if [ -n "$CT_TYPE" ]; then
CT_TYPE_DESC="Unprivileged"
if [ "$CT_TYPE" -eq 0 ]; then
CT_TYPE_DESC="Privileged"
fi
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
fi
else
exit_script
fi
fi
done
while true; do
if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
# Empty = Autologin
if [[ -z "$PW1" ]]; then
PW=""
PW1="Automatic Login"
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
break
fi
# Invalid: contains spaces
if [[ "$PW1" == *" "* ]]; then
whiptail --msgbox "Password cannot contain spaces." 8 58
continue
fi
# Invalid: too short
if ((${#PW1} < 5)); then
whiptail --msgbox "Password must be at least 5 characters." 8 58
continue
fi
# Confirm password
if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
if [[ "$PW1" == "$PW2" ]]; then
PW="-password $PW1"
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
break
else
whiptail --msgbox "Passwords do not match. Please try again." 8 58
fi
else
exit_script
fi
else
exit_script
fi
done
if CT_ID=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
if [ -z "$CT_ID" ]; then
CT_ID="$NEXTID"
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
else
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
fi
else
exit_script
fi
while true; do
if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
if [ -z "$CT_NAME" ]; then
HN="$NSAPP"
else
HN=$(echo "${CT_NAME,,}" | tr -d ' ')
fi
# Hostname validate (RFC 1123)
if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
break
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70
fi
else
exit_script
fi
done
while true; do
DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script
if [ -z "$DISK_SIZE" ]; then
DISK_SIZE="$var_disk"
fi
if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
break
else
whiptail --msgbox "Disk size must be a positive integer!" 8 58
fi
done
while true; do
CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
--inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script
if [ -z "$CORE_COUNT" ]; then
CORE_COUNT="$var_cpu"
fi
if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
break
else
whiptail --msgbox "CPU core count must be a positive integer!" 8 58
fi
done
while true; do
RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
--inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script
if [ -z "$RAM_SIZE" ]; then
RAM_SIZE="$var_ram"
fi
if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
break
else
whiptail --msgbox "RAM size must be a positive integer!" 8 58
fi
done
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
BRIDGES=""
OLD_IFS=$IFS
IFS=$'\n'
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
(grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
if [ -f "${iface_indexes_tmpfile}" ]; then
while read -r pair; do
start=$(echo "${pair}" | cut -d':' -f1)
end=$(echo "${pair}" | cut -d':' -f2)
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
fi
done <"${iface_indexes_tmpfile}"
rm -f "${iface_indexes_tmpfile}"
fi
done
IFS=$OLD_IFS
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
if [[ -z "$BRIDGES" ]]; then
BRG="vmbr0"
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
else
BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3)
if [[ -z "$BRG" ]]; then
exit_script
else
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
fi
fi
# IPv4 methods: dhcp, static, none
while true; do
IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "IPv4 Address Management" \
--menu "Select IPv4 Address Assignment Method:" 12 60 2 \
"dhcp" "Automatic (DHCP, recommended)" \
"static" "Static (manual entry)" \
3>&1 1>&2 2>&3)
exit_status=$?
if [ $exit_status -ne 0 ]; then
exit_script
fi
case "$IPV4_METHOD" in
dhcp)
NET="dhcp"
GATE=""
echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}"
break
;;
static)
# Static: call and validate CIDR address
while true; do
NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \
--title "IPv4 ADDRESS" 3>&1 1>&2 2>&3)
if [ -z "$NET" ]; then
whiptail --msgbox "IPv4 address must not be empty." 8 58
continue
elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}"
break
else
whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58
fi
done
# call and validate Gateway
while true; do
GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \
--title "Gateway IP" 3>&1 1>&2 2>&3)
if [ -z "$GATE1" ]; then
whiptail --msgbox "Gateway IP address cannot be empty." 8 58
elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
whiptail --msgbox "Invalid Gateway IP address format." 8 58
else
GATE=",gw=$GATE1"
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
break
fi
done
break
;;
esac
done
# IPv6 Address Management selection
while true; do
IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \
"Select IPv6 Address Management Type:" 15 58 4 \
"auto" "SLAAC/AUTO (recommended, default)" \
"dhcp" "DHCPv6" \
"static" "Static (manual entry)" \
"none" "Disabled" \
--default-item "auto" 3>&1 1>&2 2>&3)
[ $? -ne 0 ] && exit_script
case "$IPV6_METHOD" in
auto)
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}"
IPV6_ADDR=""
IPV6_GATE=""
break
;;
dhcp)
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}"
IPV6_ADDR="dhcp"
IPV6_GATE=""
break
;;
static)
# Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64)
while true; do
IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
"Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \
--title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script
if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}"
break
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
"$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58
fi
done
# Optional: ask for IPv6 gateway for static config
while true; do
IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \
"Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3)
if [ -z "$IPV6_GATE" ]; then
IPV6_GATE=""
break
elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then
break
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \
"Invalid IPv6 gateway format." 8 58
fi
done
break
;;
none)
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}"
IPV6_ADDR="none"
IPV6_GATE=""
break
;;
*)
exit_script
;;
esac
done
if [ "$var_os" == "alpine" ]; then
APT_CACHER=""
APT_CACHER_IP=""
else
if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
APT_CACHER="${APT_CACHER_IP:+yes}"
echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
else
exit_script
fi
fi
# if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then
# DISABLEIP6="yes"
# else
# DISABLEIP6="no"
# fi
# echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}"
if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
if [ -z "$MTU1" ]; then
MTU1="Default"
MTU=""
else
MTU=",mtu=$MTU1"
fi
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
else
exit_script
fi
if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
if [ -z "$SD" ]; then
SX=Host
SD=""
else
SX=$SD
SD="-searchdomain=$SD"
fi
echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
else
exit_script
fi
if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
if [ -z "$NX" ]; then
NX=Host
NS=""
else
NS="-nameserver=$NX"
fi
echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
else
exit_script
fi
if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then
UDHCPC_FIX="yes"
else
UDHCPC_FIX="no"
fi
export UDHCPC_FIX
if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
if [ -z "$MAC1" ]; then
MAC1="Default"
MAC=""
else
MAC=",hwaddr=$MAC1"
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
fi
else
exit_script
fi
if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
if [ -z "$VLAN1" ]; then
VLAN1="Default"
VLAN=""
else
VLAN=",tag=$VLAN1"
fi
echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
else
exit_script
fi
if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
if [ -n "${ADV_TAGS}" ]; then
ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
TAGS="${ADV_TAGS}"
else
TAGS=";"
fi
echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
else
exit_script
fi
# --- SSH key provisioning (one dialog) ---
SSH_KEYS_FILE="$(mktemp)"
: >"$SSH_KEYS_FILE"
IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
ssh_build_choices_from_files "${_def_files[@]}"
DEF_KEYS_COUNT="$COUNT"
if [[ "$DEF_KEYS_COUNT" -gt 0 ]]; then
SSH_KEY_MODE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
"Provision SSH keys for root:" 14 72 4 \
"found" "Select from detected keys (${DEF_KEYS_COUNT})" \
"manual" "Paste a single public key" \
"folder" "Scan another folder (path or glob)" \
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
else
SSH_KEY_MODE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
"No host keys detected; choose manual/none:" 12 72 2 \
"manual" "Paste a single public key" \
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
fi
case "$SSH_KEY_MODE" in
found)
SEL=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
--checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
for tag in $SEL; do
tag="${tag%\"}"
tag="${tag#\"}"
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
done
;;
manual)
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
[[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
;;
folder)
GLOB_PATH="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)"
if [[ -n "$GLOB_PATH" ]]; then
shopt -s nullglob
read -r -a _scan_files <<<"$GLOB_PATH"
shopt -u nullglob
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
ssh_build_choices_from_files "${_scan_files[@]}"
if [[ "$COUNT" -gt 0 ]]; then
SEL=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
--checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
for tag in $SEL; do
tag="${tag%\"}"
tag="${tag#\"}"
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
done
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $GLOB_PATH" 8 60
fi
else
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
fi
fi
;;
none) : ;;
esac
# Dedupe + clean EOF
if [[ -s "$SSH_KEYS_FILE" ]]; then
sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
printf '\n' >>"$SSH_KEYS_FILE"
fi
# SSH activate, if keys found or password set
if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
SSH="yes"
else
SSH="no"
fi
else
SSH="no"
fi
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
export SSH_KEYS_FILE
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then
ENABLE_FUSE="yes"
else
ENABLE_FUSE="no"
fi
echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
VERBOSE="yes"
else
VERBOSE="no"
fi
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
else
clear
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
advanced_settings
fi
}
# ------------------------------------------------------------------------------
# diagnostics_check()
#
# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
# - Asks user whether to send anonymous diagnostic data
# - Saves DIAGNOSTICS=yes/no in the config file
# ------------------------------------------------------------------------------
diagnostics_check() {
if ! [ -d "/usr/local/community-scripts" ]; then
mkdir -p /usr/local/community-scripts
fi
if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
cat <<EOF >/usr/local/community-scripts/diagnostics
DIAGNOSTICS=yes
#This file is used to store the diagnostics settings for the Community-Scripts API.
#https://github.com/community-scripts/ProxmoxVED/discussions/1836
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will disable the diagnostics feature.
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will enable the diagnostics feature.
#The following information will be sent:
#"ct_type"
#"disk_size"
#"core_count"
#"ram_size"
#"os_type"
#"os_version"
#"disableip6"
#"nsapp"
#"method"
#"pve_version"
#"status"
#If you have any concerns, please review the source code at /misc/build.func
EOF
DIAGNOSTICS="yes"
else
cat <<EOF >/usr/local/community-scripts/diagnostics
DIAGNOSTICS=no
#This file is used to store the diagnostics settings for the Community-Scripts API.
#https://github.com/community-scripts/ProxmoxVED/discussions/1836
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will disable the diagnostics feature.
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
#This will enable the diagnostics feature.
#The following information will be sent:
#"ct_type"
#"disk_size"
#"core_count"
#"ram_size"
#"os_type"
#"os_version"
#"disableip6"
#"nsapp"
#"method"
#"pve_version"
#"status"
#If you have any concerns, please review the source code at /misc/build.func
EOF
DIAGNOSTICS="no"
fi
else
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
fi
}
# ------------------------------------------------------------------------------
# default_var_settings
#
# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
# - Loads var_* values from default.vars (safe parser, no source/eval)
# - Precedence: ENV var_* > default.vars > built-in defaults
# - Maps var_verbose → VERBOSE
# - Calls base_settings "$VERBOSE" and echo_default
# ------------------------------------------------------------------------------
default_var_settings() {
# Allowed var_* keys (alphabetically sorted)
local VAR_WHITELIST=(
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
# Snapshot: environment variables (highest precedence)
declare -A _HARD_ENV=()
local _k
for _k in "${VAR_WHITELIST[@]}"; do
if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
done
# Find default.vars location
local _find_default_vars
_find_default_vars() {
local f
for f in \
/usr/local/community-scripts/default.vars \
"$HOME/.config/community-scripts/default.vars" \
"./default.vars"; do
[ -f "$f" ] && {
echo "$f"
return 0
}
done
return 1
}
# Allow override of storages via env (for non-interactive use cases)
[ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
[ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
# Create once, with storages already selected, no var_ctid/var_hostname lines
local _ensure_default_vars
_ensure_default_vars() {
_find_default_vars >/dev/null 2>&1 && return 0
local canonical="/usr/local/community-scripts/default.vars"
msg_info "No default.vars found. Creating ${canonical}"
mkdir -p /usr/local/community-scripts
# Pick storages before writing the file (always ask unless only one)
# Create a minimal temp file to write into
: >"$canonical"
# Base content (no var_ctid / var_hostname here)
cat >"$canonical" <<'EOF'
# Community-Scripts defaults (var_* only). Lines starting with # are comments.
# Precedence: ENV var_* > default.vars > built-ins.
# Keep keys alphabetically sorted.
# Container type
var_unprivileged=1
# Resources
var_cpu=1
var_disk=4
var_ram=1024
# Network
var_brg=vmbr0
var_net=dhcp
var_ipv6_method=none
# var_gateway=
# var_ipv6_static=
# var_vlan=
# var_mtu=
# var_mac=
# var_ns=
# SSH
var_ssh=no
# var_ssh_authorized_key=
# APT cacher (optional)
# var_apt_cacher=yes
# var_apt_cacher_ip=192.168.1.10
# Features/Tags/verbosity
var_fuse=no
var_tun=no
var_tags=community-script
var_verbose=no
# Security (root PW) empty => autologin
# var_pw=
EOF
# Now choose storages (always prompt unless just one exists)
choose_and_set_storage_for_file "$canonical" template
choose_and_set_storage_for_file "$canonical" container
chmod 0644 "$canonical"
msg_ok "Created ${canonical}"
}
# Whitelist check
local _is_whitelisted_key
_is_whitelisted_key() {
local k="$1"
local w
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
return 1
}
# Safe parser for KEY=VALUE lines
local _load_vars_file
_load_vars_file() {
local file="$1"
[ -f "$file" ] || return 0
msg_info "Loading defaults from ${file}"
local line key val
while IFS= read -r line || [ -n "$line" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
local var_key="${BASH_REMATCH[1]}"
local var_val="${BASH_REMATCH[2]}"
[[ "$var_key" != var_* ]] && continue
_is_whitelisted_key "$var_key" || {
msg_debug "Ignore non-whitelisted ${var_key}"
continue
}
# Strip quotes
if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
var_val="${BASH_REMATCH[1]}"
elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
var_val="${BASH_REMATCH[1]}"
fi
# Unsafe characters
case $var_val in
\"*\")
var_val=${var_val#\"}
var_val=${var_val%\"}
;;
\'*\')
var_val=${var_val#\'}
var_val=${var_val%\'}
;;
esac # Hard env wins
[[ -n "${_HARD_ENV[$var_key]:-}" ]] && continue
# Set only if not already exported
[[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
else
msg_warn "Malformed line in ${file}: ${line}"
fi
done <"$file"
msg_ok "Loaded ${file}"
}
# 1) Ensure file exists
_ensure_default_vars
# 2) Load file
local dv
dv="$(_find_default_vars)" || {
msg_error "default.vars not found after ensure step"
return 1
}
_load_vars_file "$dv"
# 3) Map var_verbose → VERBOSE
if [[ -n "${var_verbose:-}" ]]; then
case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
else
VERBOSE="no"
fi
# 4) Apply base settings and show summary
METHOD="mydefaults-global"
base_settings "$VERBOSE"
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using My Defaults (default.vars) on node $PVEHOST_NAME${CL}"
echo_default
}
# ------------------------------------------------------------------------------
# get_app_defaults_path()
#
# - Returns full path for app-specific defaults file
# - Example: /usr/local/community-scripts/defaults/<app>.vars
# ------------------------------------------------------------------------------
get_app_defaults_path() {
local n="${NSAPP:-${APP,,}}"
echo "/usr/local/community-scripts/defaults/${n}.vars"
}
# ------------------------------------------------------------------------------
# maybe_offer_save_app_defaults
#
# - Called after advanced_settings returned with fully chosen values.
# - If no <nsapp>.vars exists, offers to persist current advanced settings
# into /usr/local/community-scripts/defaults/<nsapp>.vars
# - Only writes whitelisted var_* keys.
# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
# ------------------------------------------------------------------------------
if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
declare -ag VAR_WHITELIST=(
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_ctid var_disk var_fuse
var_gateway var_hostname var_ipv6_method var_ipv6_static var_mac var_mtu
var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
fi
_is_whitelisted_key() {
local k="$1"
local w
for w in "${VAR_WHITELIST[@]}"; do [[ "$k" == "$w" ]] && return 0; done
return 1
}
_sanitize_value() {
# Disallow Command-Substitution / Shell-Meta
case "$1" in
*'$('* | *'`'* | *';'* | *'&'* | *'<('*)
echo ""
return 0
;;
esac
echo "$1"
}
# Map-Parser: read var_* from file into _VARS_IN associative array
declare -A _VARS_IN
_load_vars_file() {
local file="$1"
[ -f "$file" ] || return 0
msg_info "Loading defaults from ${file}"
local line key val
while IFS= read -r line || [ -n "$line" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[ -z "$line" ] && continue
case "$line" in
\#*) continue ;;
esac
key=$(printf "%s" "$line" | cut -d= -f1)
val=$(printf "%s" "$line" | cut -d= -f2-)
case "$key" in
var_*)
if _is_whitelisted_key "$key"; then
[ -z "${!key+x}" ] && export "$key=$val"
fi
;;
esac
done <"$file"
msg_ok "Loaded ${file}"
}
# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
_build_vars_diff() {
local oldf="$1" newf="$2"
local k
local -A OLD=() NEW=()
_load_vars_file_to_map "$oldf"
for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
_load_vars_file_to_map "$newf"
for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
local out
out+="# Diff for ${APP} (${NSAPP})\n"
out+="# Old: ${oldf}\n# New: ${newf}\n\n"
local found_change=0
# Changed & Removed
for k in "${!OLD[@]}"; do
if [[ -v NEW["$k"] ]]; then
if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
found_change=1
fi
else
out+="- ${k}\n - old: ${OLD[$k]}\n"
found_change=1
fi
done
# Added
for k in "${!NEW[@]}"; do
if [[ ! -v OLD["$k"] ]]; then
out+="+ ${k}\n + new: ${NEW[$k]}\n"
found_change=1
fi
done
if [[ $found_change -eq 0 ]]; then
out+="(No differences)\n"
fi
printf "%b" "$out"
}
# Build a temporary <app>.vars file from current advanced settings
_build_current_app_vars_tmp() {
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
# NET/GW
_net="${NET:-}"
_gate=""
case "${GATE:-}" in
,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
esac
# IPv6
_ipv6_method="${IPV6_METHOD:-auto}"
_ipv6_static=""
_ipv6_gateway=""
if [ "$_ipv6_method" = "static" ]; then
_ipv6_static="${IPV6_ADDR:-}"
_ipv6_gateway="${IPV6_GATE:-}"
fi
# MTU/VLAN/MAC
_mtu=""
_vlan=""
_mac=""
case "${MTU:-}" in
,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
esac
case "${VLAN:-}" in
,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
esac
case "${MAC:-}" in
,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
esac
# DNS / Searchdomain
_ns=""
_searchdomain=""
case "${NS:-}" in
-nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
esac
case "${SD:-}" in
-searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
esac
# SSH / APT / Features
_ssh="${SSH:-no}"
_ssh_auth="${SSH_AUTHORIZED_KEY:-}"
_apt_cacher="${APT_CACHER:-}"
_apt_cacher_ip="${APT_CACHER_IP:-}"
_fuse="${ENABLE_FUSE:-no}"
_tun="${ENABLE_TUN:-no}"
_tags="${TAGS:-}"
_verbose="${VERBOSE:-no}"
# Type / Resources / Identity
_unpriv="${CT_TYPE:-1}"
_cpu="${CORE_COUNT:-1}"
_ram="${RAM_SIZE:-1024}"
_disk="${DISK_SIZE:-4}"
_hostname="${HN:-$NSAPP}"
# Storage
_tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
_ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
{
echo "# App-specific defaults for ${APP} (${NSAPP})"
echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo
echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
echo "var_cpu=$(_sanitize_value "$_cpu")"
echo "var_ram=$(_sanitize_value "$_ram")"
echo "var_disk=$(_sanitize_value "$_disk")"
[ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
[ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
[ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
[ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
[ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
[ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
[ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
[ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
[ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")"
[ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
[ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
[ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
[ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
[ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
[ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
[ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
[ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
[ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
[ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
[ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
[ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
} >"$tmpf"
echo "$tmpf"
}
# ------------------------------------------------------------------------------
# maybe_offer_save_app_defaults()
#
# - Called after advanced_settings()
# - Offers to save current values as app defaults if not existing
# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
# ------------------------------------------------------------------------------
maybe_offer_save_app_defaults() {
local app_vars_path
app_vars_path="$(get_app_defaults_path)"
# always build from current settings
local new_tmp diff_tmp
new_tmp="$(_build_current_app_vars_tmp)"
diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
# 1) if no file → offer to create
if [[ ! -f "$app_vars_path" ]]; then
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then
mkdir -p "$(dirname "$app_vars_path")"
install -m 0644 "$new_tmp" "$app_vars_path"
msg_ok "Saved app defaults: ${app_vars_path}"
fi
rm -f "$new_tmp" "$diff_tmp"
return 0
fi
# 2) if file exists → build diff
_build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
# if no differences → do nothing
if grep -q "^(No differences)$" "$diff_tmp"; then
rm -f "$new_tmp" "$diff_tmp"
return 0
fi
# 3) if file exists → show menu with default selection "Update Defaults"
local app_vars_file
app_vars_file="$(basename "$app_vars_path")"
while true; do
local sel
sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "APP DEFAULTS ${APP}" \
--menu "Differences detected. What do you want to do?" 20 78 10 \
"Update Defaults" "Write new values to ${app_vars_file}" \
"Keep Current" "Keep existing defaults (no changes)" \
"View Diff" "Show a detailed diff" \
"Cancel" "Abort without changes" \
--default-item "Update Defaults" \
3>&1 1>&2 2>&3)" || { sel="Cancel"; }
case "$sel" in
"Update Defaults")
install -m 0644 "$new_tmp" "$app_vars_path"
msg_ok "Updated app defaults: ${app_vars_path}"
break
;;
"Keep Current")
msg_info "Keeping current app defaults: ${app_vars_path}"
break
;;
"View Diff")
whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "Diff ${APP}" \
--scrolltext --textbox "$diff_tmp" 25 100
;;
"Cancel" | *)
msg_info "Canceled. No changes to app defaults."
break
;;
esac
done
rm -f "$new_tmp" "$diff_tmp"
}
ensure_storage_selection_for_vars_file() {
vf="$1"
# Read stored values (if any)
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-)
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-)
# Template storage
if [ -n "$tpl" ]; then
TEMPLATE_STORAGE="$tpl"
else
choose_and_set_storage_for_file "$vf" template
fi
# Container storage
if [ -n "$ct" ]; then
CONTAINER_STORAGE="$ct"
else
choose_and_set_storage_for_file "$vf" container
fi
echo_storage_summary_from_file "$vf"
}
diagnostics_menu() {
if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "DIAGNOSTIC SETTINGS" \
--yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
--yes-button "No" --no-button "Back"; then
DIAGNOSTICS="no"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
fi
else
if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "DIAGNOSTIC SETTINGS" \
--yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
--yes-button "Yes" --no-button "Back"; then
DIAGNOSTICS="yes"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
fi
fi
}
# ------------------------------------------------------------------------------
# install_script()
#
# - Main entrypoint for installation mode
# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
# - Applies chosen settings and triggers container build
# ------------------------------------------------------------------------------
install_script() {
pve_check
shell_check
root_check
arch_check
ssh_check
maxkeys_check
diagnostics_check
if systemctl is-active -q ping-instances.service; then
systemctl -q stop ping-instances.service
fi
NEXTID=$(pvesh get /cluster/nextid)
timezone=$(cat /etc/timezone)
header_info
# --- Support CLI argument as direct preset (default, advanced, …) ---
CHOICE="${mode:-${1:-}}"
# If no CLI argument → show whiptail menu
if [ -z "$CHOICE" ]; then
local menu_items=(
"1" "Default Install"
"2" "Advanced Install"
"3" "My Defaults"
)
if [ -f "$(get_app_defaults_path)" ]; then
menu_items+=("4" "App Defaults for ${APP}")
menu_items+=("5" "Settings")
else
menu_items+=("4" "Settings")
fi
TMP_CHOICE=$(whiptail \
--backtitle "Proxmox VE Helper Scripts" \
--title "Community-Scripts Options" \
--ok-button "Select" --cancel-button "Exit Script" \
--notags \
--menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
20 60 9 \
"${menu_items[@]}" \
--default-item "1" \
3>&1 1>&2 2>&3) || exit_script
CHOICE="$TMP_CHOICE"
fi
# --- Main case ---
case "$CHOICE" in
1 | default | DEFAULT)
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
VERBOSE="no"
METHOD="default"
base_settings "$VERBOSE"
echo_default
ensure_storage_selection_for_vars_file "/usr/local/community-scripts/default.vars"
;;
2 | advanced | ADVANCED)
header_info
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
METHOD="advanced"
base_settings
advanced_settings
ensure_storage_selection_for_vars_file "/usr/local/community-scripts/default.vars"
maybe_offer_save_app_defaults
;;
3 | mydefaults | MYDEFAULTS)
default_var_settings || {
msg_error "Failed to apply default.vars"
exit 1
}
ensure_storage_selection_for_vars_file "/usr/local/community-scripts/default.vars"
;;
4 | appdefaults | APPDEFAULTS)
if [ -f "$(get_app_defaults_path)" ]; then
header_info
echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
METHOD="appdefaults"
base_settings
_load_vars_file "$(get_app_defaults_path)"
echo_default
ensure_storage_selection_for_vars_file "$(get_app_defaults_path)"
else
msg_error "No App Defaults available for ${APP}"
exit 1
fi
;;
5 | settings | SETTINGS)
settings_menu
;;
*)
echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
exit 1
;;
esac
}
# ------------------------------------------------------------------------------
# check_storage_or_prompt()
#
# - Validates container/template storage
# - If invalid or missing, prompts user to select new storage
# - Updates vars file accordingly
# ------------------------------------------------------------------------------
# check_storage_or_prompt() {
# local vars_file="$1"
# local changed=0
# if [ ! -f "$vars_file" ]; then
# msg_warn "No vars file found at $vars_file"
# return 0
# fi
# # Helper: validate storage ID
# _validate_storage() {
# local s="$1"
# [ -n "$s" ] || return 1
# pvesm status -content images | awk 'NR>1 {print $1}' | grep -qx "$s"
# }
# # Load current values (empty if not set)
# local ct_store tpl_store
# ct_store=$(awk -F= '/^var_container_storage=/ {print $2; exit}' "$vars_file")
# tpl_store=$(awk -F= '/^var_template_storage=/ {print $2; exit}' "$vars_file")
# # Container storage
# if ! _validate_storage "$ct_store"; then
# local new_ct
# new_ct=$(pvesm status -content images | awk 'NR>1 {print $1" "$2" "$6}')
# new_ct=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
# --title "Select Container Storage" \
# --menu "Choose container storage:" 20 60 10 $new_ct 3>&1 1>&2 2>&3) || return 0
# if [ -n "$new_ct" ]; then
# sed -i "/^var_container_storage=/d" "$vars_file"
# echo "var_container_storage=$new_ct" >>"$vars_file"
# changed=1
# msg_ok "Updated container storage in $vars_file → $new_ct"
# fi
# fi
# # Template storage
# if ! _validate_storage "$tpl_store"; then
# local new_tpl
# new_tpl=$(pvesm status -content vztmpl | awk 'NR>1 {print $1" "$2" "$6}')
# new_tpl=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
# --title "Select Template Storage" \
# --menu "Choose template storage:" 20 60 10 $new_tpl 3>&1 1>&2 2>&3) || return 0
# if [ -n "$new_tpl" ]; then
# sed -i "/^var_template_storage=/d" "$vars_file"
# echo "var_template_storage=$new_tpl" >>"$vars_file"
# changed=1
# msg_ok "Updated template storage in $vars_file → $new_tpl"
# fi
# fi
# # Always succeed (no aborts from here)
# return 0
# }
# # ------------------------------------------------------------------------------
# # storage_settings_menu()
# #
# # - Menu for managing storage defaults
# # - Options: update My Defaults or App Defaults storage
# # ------------------------------------------------------------------------------
# storage_settings_menu() {
# local vars_file="/usr/local/community-scripts/default.vars"
# check_storage_or_prompt "$vars_file"
# _echo_storage_summary "$vars_file"
# # Always ask user if they want to update, even if values are valid
# if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
# --title "STORAGE SETTINGS" \
# --yesno "Do you want to update the storage defaults?\n\nCurrent values will be kept unless you select new ones." 12 72; then
# # container storage selection
# local new_ct
# new_ct=$(pvesm status -content images | awk 'NR>1 {print $1" "$2" "$6}')
# new_ct=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
# --title "Select Container Storage" \
# --menu "Choose container storage:" 20 60 10 $new_ct 3>&1 1>&2 2>&3) || true
# if [ -n "$new_ct" ]; then
# sed -i '/^var_container_storage=/d' "$vars_file"
# echo "var_container_storage=$new_ct" >>"$vars_file"
# msg_ok "Updated container storage → $new_ct"
# fi
# # template storage selection
# local new_tpl
# new_tpl=$(pvesm status -content vztmpl | awk 'NR>1 {print $1" "$2" "$6}')
# new_tpl=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
# --title "Select Template Storage" \
# --menu "Choose template storage:" 20 60 10 $new_tpl 3>&1 1>&2 2>&3) || true
# if [ -n "$new_tpl" ]; then
# sed -i '/^var_template_storage=/d' "$vars_file"
# echo "var_template_storage=$new_tpl" >>"$vars_file"
# msg_ok "Updated template storage → $new_tpl"
# fi
# fi
# }
edit_default_storage() {
local vf="/usr/local/community-scripts/default.vars"
# make sure file exists
if [ ! -f "$vf" ]; then
msg_info "No default.vars found, creating $vf"
mkdir -p /usr/local/community-scripts
touch "$vf"
fi
# reuse the same Whiptail selection we already have
ensure_storage_selection_for_vars_file "$vf"
}
settings_menu() {
while true; do
local settings_items=(
"1" "Manage API-Diagnostic Setting"
"2" "Edit Default.vars"
"3" "Edit Default Storage"
)
if [ -f "$(get_app_defaults_path)" ]; then
settings_items+=("4" "Edit App.vars for ${APP}")
settings_items+=("5" "Exit")
else
settings_items+=("4" "Exit")
fi
local choice
choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "Community-Scripts SETTINGS Menu" \
--ok-button "OK" --cancel-button "Back" \
--menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \
"${settings_items[@]}" \
3>&1 1>&2 2>&3) || break
case "$choice" in
1) diagnostics_menu ;;
2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
3) edit_default_storage ;;
4)
if [ -f "$(get_app_defaults_path)" ]; then
${EDITOR:-nano} "$(get_app_defaults_path)"
else
exit_script
fi
;;
5) exit_script ;;
esac
done
}
# ===== Unified storage selection & writing to vars files =====
_write_storage_to_vars() {
# $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
local vf="$1" key="$2" val="$3"
# remove uncommented and commented versions to avoid duplicates
sed -i "/^[#[:space:]]*${key}=/d" "$vf"
echo "${key}=${val}" >>"$vf"
}
choose_and_set_storage_for_file() {
# $1 = vars_file, $2 = class ('container'|'template')
local vf="$1" class="$2" key="" current=""
case "$class" in
container) key="var_container_storage" ;;
template) key="var_template_storage" ;;
*)
msg_error "Unknown storage class: $class"
return 1
;;
esac
current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
# If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
local content="rootdir"
[[ "$class" == "template" ]] && content="vztmpl"
local count
count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
if [[ "$count" -eq 1 ]]; then
STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
STORAGE_INFO=""
else
# If the current value is preselectable, we could show it, but per your requirement we always offer selection
select_storage "$class" || return 1
fi
_write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
# Keep environment in sync for later steps (e.g. app-default save)
if [[ "$class" == "container" ]]; then
export var_container_storage="$STORAGE_RESULT"
export CONTAINER_STORAGE="$STORAGE_RESULT"
else
export var_template_storage="$STORAGE_RESULT"
export TEMPLATE_STORAGE="$STORAGE_RESULT"
fi
msg_ok "Updated ${key}${STORAGE_RESULT}"
}
echo_storage_summary_from_file() {
local vars_file="$1"
local ct_store tpl_store
ct_store=$(awk -F= '/^var_container_storage=/ {print $2; exit}' "$vars_file")
tpl_store=$(awk -F= '/^var_template_storage=/ {print $2; exit}' "$vars_file")
if [ -n "$tpl" ]; then
TEMPLATE_STORAGE="$tpl"
else
choose_and_set_storage_for_file "$vf" template
fi
if [ -n "$ct" ]; then
CONTAINER_STORAGE="$ct"
else
choose_and_set_storage_for_file "$vf" container
fi
}
# ------------------------------------------------------------------------------
# check_container_resources()
#
# - Compares host RAM/CPU with required values
# - Warns if under-provisioned and asks user to continue or abort
# ------------------------------------------------------------------------------
check_container_resources() {
current_ram=$(free -m | awk 'NR==2{print $2}')
current_cpu=$(nproc)
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? <yes/No> "
read -r prompt
if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
exit 1
fi
else
echo -e ""
fi
}
# ------------------------------------------------------------------------------
# check_container_storage()
#
# - Checks /boot partition usage
# - Warns if usage >80% and asks user confirmation before proceeding
# ------------------------------------------------------------------------------
check_container_storage() {
total_size=$(df /boot --output=size | tail -n 1)
local used_size=$(df /boot --output=used | tail -n 1)
usage=$((100 * used_size / total_size))
if ((usage > 80)); then
echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
echo -ne "Continue anyway? <y/N> "
read -r prompt
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
exit 1
fi
fi
}
# ------------------------------------------------------------------------------
# ssh_extract_keys_from_file()
#
# - Extracts valid SSH public keys from given file
# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
# ------------------------------------------------------------------------------
ssh_extract_keys_from_file() {
local f="$1"
[[ -r "$f" ]] || return 0
tr -d '\r' <"$f" | awk '
/^[[:space:]]*#/ {next}
/^[[:space:]]*$/ {next}
# nackt: typ base64 [comment]
/^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
# mit Optionen: finde ab erstem Key-Typ
{
match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
if (RSTART>0) { print substr($0, RSTART) }
}
'
}
# ------------------------------------------------------------------------------
# ssh_build_choices_from_files()
#
# - Builds interactive whiptail checklist of available SSH keys
# - Generates fingerprint, type and comment for each key
# ------------------------------------------------------------------------------
ssh_build_choices_from_files() {
local -a files=("$@")
CHOICES=()
COUNT=0
MAPFILE="$(mktemp)"
local id key typ fp cmt base ln=0
for f in "${files[@]}"; do
[[ -f "$f" && -r "$f" ]] || continue
base="$(basename -- "$f")"
case "$base" in
known_hosts | known_hosts.* | config) continue ;;
id_*) [[ "$f" != *.pub ]] && continue ;;
esac
# map every key in file
while IFS= read -r key; do
[[ -n "$key" ]] || continue
typ=""
fp=""
cmt=""
# Only the pure key part (without options) is already included in key.
read -r _typ _b64 _cmt <<<"$key"
typ="${_typ:-key}"
cmt="${_cmt:-}"
# Fingerprint via ssh-keygen (if available)
if command -v ssh-keygen >/dev/null 2>&1; then
fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
fi
# Label shorten
[[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
ln=$((ln + 1))
COUNT=$((COUNT + 1))
id="K${COUNT}"
echo "${id}|${key}" >>"$MAPFILE"
CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }${base}" "OFF")
done < <(ssh_extract_keys_from_file "$f")
done
}
# ------------------------------------------------------------------------------
# ssh_discover_default_files()
#
# - Scans standard paths for SSH keys
# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
# ------------------------------------------------------------------------------
ssh_discover_default_files() {
local -a cand=()
shopt -s nullglob
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
cand+=(/root/.ssh/*.pub)
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
shopt -u nullglob
printf '%s\0' "${cand[@]}"
}
# ------------------------------------------------------------------------------
# start()
#
# - Entry point of script
# - On Proxmox host: calls install_script
# - In silent mode: runs update_script
# - Otherwise: shows update/setting menu
# ------------------------------------------------------------------------------
start() {
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func)
if command -v pveversion >/dev/null 2>&1; then
install_script || return 0
return 0
elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
VERBOSE="no"
set_std_mode
update_script
else
CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
"Support/Update functions for ${APP} LXC. Choose an option:" \
12 60 3 \
"1" "YES (Silent Mode)" \
"2" "YES (Verbose Mode)" \
"3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
case "$CHOICE" in
1)
VERBOSE="no"
set_std_mode
;;
2)
VERBOSE="yes"
set_std_mode
;;
3)
clear
exit_script
exit
;;
esac
update_script
fi
}
# ------------------------------------------------------------------------------
# build_container()
#
# - Creates and configures the LXC container
# - Builds network string and applies features (FUSE, TUN, VAAPI passthrough)
# - Starts container and waits for network connectivity
# - Installs base packages, SSH keys, and runs <app>-install.sh
# ------------------------------------------------------------------------------
build_container() {
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}${MAC:-},ip=${NET:-dhcp}${GATE:-}${VLAN:-}${MTU:-}"
case "$IPV6_METHOD" in
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
static)
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
;;
none) ;;
esac
if [ "$CT_TYPE" == "1" ]; then
FEATURES="keyctl=1,nesting=1"
else
FEATURES="nesting=1"
fi
if [ "$ENABLE_FUSE" == "yes" ]; then
FEATURES="$FEATURES,fuse=1"
fi
#if [[ $DIAGNOSTICS == "yes" ]]; then
# post_to_api
#fi
TEMP_DIR=$(mktemp -d)
pushd "$TEMP_DIR" >/dev/null
if [ "$var_os" == "alpine" ]; then
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)"
else
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)"
fi
export DIAGNOSTICS="$DIAGNOSTICS"
export RANDOM_UUID="$RANDOM_UUID"
export CACHER="$APT_CACHER"
export CACHER_IP="$APT_CACHER_IP"
export tz="$timezone"
#export DISABLEIPV6="$DISABLEIP6"
export APPLICATION="$APP"
export app="$NSAPP"
export PASSWORD="$PW"
export VERBOSE="$VERBOSE"
export SSH_ROOT="${SSH}"
export SSH_AUTHORIZED_KEY
export CTID="$CT_ID"
export CTTYPE="$CT_TYPE"
export ENABLE_FUSE="$ENABLE_FUSE"
export ENABLE_TUN="$ENABLE_TUN"
export PCT_OSTYPE="$var_os"
export PCT_OSVERSION="${var_version%%.*}"
export PCT_DISK_SIZE="$DISK_SIZE"
export PCT_OPTIONS="
-features $FEATURES
-hostname $HN
-tags $TAGS
$SD
$NS
$NET_STRING
-onboot 1
-cores $CORE_COUNT
-memory $RAM_SIZE
-unprivileged $CT_TYPE
$PW
"
export TEMPLATE_STORAGE="${var_template_storage:-}"
export CONTAINER_STORAGE="${var_container_storage:-}"
create_lxc_container || exit $?
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
# USB passthrough for privileged LXC (CT_TYPE=0)
if [ "$CT_TYPE" == "0" ]; then
cat <<EOF >>"$LXC_CONFIG"
# USB passthrough
lxc.cgroup2.devices.allow: a
lxc.cap.drop:
lxc.cgroup2.devices.allow: c 188:* rwm
lxc.cgroup2.devices.allow: c 189:* rwm
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
EOF
fi
# VAAPI passthrough for privileged containers or known apps
VAAPI_APPS=(
"immich" "Channels" "Emby" "ErsatzTV" "Frigate" "Jellyfin"
"Plex" "Scrypted" "Tdarr" "Unmanic" "Ollama" "FileFlows"
"Open WebUI" "Debian" "Tunarr"
)
is_vaapi_app=false
for vaapi_app in "${VAAPI_APPS[@]}"; do
if [[ "$APP" == "$vaapi_app" ]]; then
is_vaapi_app=true
break
fi
done
if [[ "$CT_TYPE" == "0" || "$is_vaapi_app" == "true" ]]; then
VAAPI_DEVICES=()
SINGLE_VAAPI_DEVICE=""
seen_ids=()
for bypath in /dev/dri/by-path/*-render /dev/dri/renderD*; do
[[ -e "$bypath" ]] || continue
dev_render=$(readlink -f "$bypath") || continue
id=$(basename "$dev_render")
[[ " ${seen_ids[*]} " == *" $id "* ]] && continue
seen_ids+=("$id")
card_index="${id#renderD}"
card="/dev/dri/card${card_index}"
combo_devices=("$dev_render")
if [[ "${#combo_devices[@]}" -eq 1 && -e /dev/dri/card0 ]]; then
combo_devices+=("/dev/dri/card0")
fi
#echo "combo_devices=${combo_devices[*]}"
pci_addr=$(basename "$bypath" | cut -d- -f1 --complement | sed 's/-render//' || true)
pci_info=$(lspci -nn | grep "${pci_addr#0000:}" || true)
name="${pci_info#*: }"
[[ -z "$name" ]] && name="Unknown GPU ($pci_addr)"
label="$(basename "$dev_render")"
[[ -e "$card" ]] && label+=" + $(basename "$card")"
label+=" $name"
msg_debug "[VAAPI DEBUG] Adding VAAPI combo: ${combo_devices[*]} ($label)"
VAAPI_DEVICES+=("$(
IFS=:
echo "${combo_devices[*]}"
)" "$label" "OFF")
done
[[ -e /dev/fb0 ]] && VAAPI_DEVICES+=("/dev/fb0" "fb0 - Framebuffer" "OFF")
GID_VIDEO=$(getent group video | cut -d: -f3)
GID_RENDER=$(getent group render | cut -d: -f3)
[[ -z "$GID_VIDEO" ]] && GID_VIDEO=44 && msg_warn "'video' group not found, falling back to GID 44"
[[ -z "$GID_RENDER" ]] && GID_RENDER=104 && msg_warn "'render' group not found, falling back to GID 104"
if [[ "${#VAAPI_DEVICES[@]}" -eq 0 ]]; then
msg_warn "No VAAPI-compatible devices found."
elif [[ "${#VAAPI_DEVICES[@]}" -eq 3 ]]; then
#msg_info "Only one VAAPI-compatible device found enabling passthrough."
IFS=":" read -ra devices <<<"${VAAPI_DEVICES[0]//\"/}"
IDX=0
DID_MOUNT_DRI=0
for d in "${devices[@]}"; do
if [[ "$CT_TYPE" == "0" ]]; then
if [[ "$DID_MOUNT_DRI" -eq 0 && -d /dev/dri ]]; then
echo "lxc.mount.entry: /dev/dri /dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
DID_MOUNT_DRI=1
fi
if ! major_minor=$(stat -c '%t:%T' "$d" 2>/dev/null | awk -F: '{ printf "%d:%d", "0x"$1, "0x"$2 }'); then
msg_warn "Could not stat $d skipping."
continue
fi
echo "lxc.cgroup2.devices.allow: c $major_minor rwm" >>"$LXC_CONFIG"
echo "lxc.mount.entry: $d $d none bind,optional,create=file" >>"$LXC_CONFIG"
else
GID=$([[ "$d" =~ render ]] && echo "$GID_RENDER" || echo "$GID_VIDEO")
echo "dev${IDX}: $d,gid=${GID}" >>"$LXC_CONFIG"
IDX=$((IDX + 1))
fi
done
else
if [[ "$CT_TYPE" == "0" ]]; then
whiptail --title "VAAPI passthrough" --msgbox "\
✅ VAAPI passthrough has been enabled
GPU hardware acceleration will be available inside the container
(e.g., for Jellyfin, Plex, Frigate, etc.)
Note: You may need to install drivers manually inside the container,
such as 'intel-media-driver', 'libva2', or 'vainfo'." 15 74
else
whiptail --title "VAAPI passthrough (limited)" --msgbox "\
⚠️ VAAPI passthrough in unprivileged containers may be limited
Some drivers (e.g., iHD) require privileged access to DRM subsystems.
If VAAPI fails, consider switching to a privileged container.
Note: You may need to install drivers manually inside the container,
such as 'intel-media-driver', 'libva2', or 'vainfo'." 15 74
fi
SELECTED_DEVICES=$(whiptail --title "VAAPI Device Selection" \
--checklist "Select VAAPI device(s) / GPU(s) to passthrough:" 20 100 6 \
"${VAAPI_DEVICES[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
exit_script
msg_error "VAAPI passthrough selection cancelled by user."
fi
if [[ -n "$SELECTED_DEVICES" ]]; then
IDX=0
DID_MOUNT_DRI=0
for dev in $SELECTED_DEVICES; do
dev="${dev%\"}"
dev="${dev#\"}" # strip quotes
IFS=":" read -ra devices <<<"$dev"
msg_debug "[VAAPI DEBUG] Autopassthrough for devices: ${devices[*]}"
for d in "${devices[@]}"; do
if [[ "$CT_TYPE" == "0" ]]; then
if [[ "$DID_MOUNT_DRI" -eq 0 && -d /dev/dri ]]; then
echo "lxc.mount.entry: /dev/dri /dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
DID_MOUNT_DRI=1
fi
if ! major_minor=$(stat -c '%t:%T' "$d" 2>/dev/null | awk -F: '{ printf "%d:%d", "0x"$1, "0x"$2 }'); then
msg_warn "Could not stat $d skipping."
continue
fi
echo "lxc.cgroup2.devices.allow: c $major_minor rwm" >>"$LXC_CONFIG"
echo "lxc.mount.entry: $d $d none bind,optional,create=file" >>"$LXC_CONFIG"
else
GID=$([[ "$d" =~ render ]] && echo "$GID_RENDER" || echo "$GID_VIDEO")
echo "dev${IDX}: $d,gid=${GID}" >>"$LXC_CONFIG"
IDX=$((IDX + 1))
fi
done
done
else
msg_warn "No VAAPI devices selected passthrough skipped."
fi
fi
fi
# TUN device passthrough
if [ "$ENABLE_TUN" == "yes" ]; then
cat <<EOF >>"$LXC_CONFIG"
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF
fi
# This starts the container and executes <app>-install.sh
msg_info "Starting LXC Container"
pct start "$CTID"
# wait for status 'running'
for i in {1..10}; do
if pct status "$CTID" | grep -q "status: running"; then
msg_ok "Started LXC Container"
break
fi
sleep 1
if [ "$i" -eq 10 ]; then
msg_error "LXC Container did not reach running state"
exit 1
fi
done
if [ "$var_os" != "alpine" ]; then
msg_info "Waiting for network in LXC container"
sleep 2
for i in {1..10}; do
# 1. Primary check: ICMP ping (fastest, but may be blocked by ISP/firewall)
if pct exec "$CTID" -- ping -c1 -W1 deb.debian.org >/dev/null 2>&1; then
msg_ok "Network in LXC is reachable (ping)"
break
fi
# Wait and retry if not reachable yet
if [ "$i" -lt 10 ]; then
if [ "$i" -le 3 ]; then
sleep 2
else
msg_warn "No network in LXC yet (try $i/10) waiting..."
sleep 3
fi
else
# After 10 unsuccessful ping attempts, try HTTP connectivity via wget as fallback
msg_warn "Ping failed 10 times. Trying HTTP connectivity check (wget) as fallback..."
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
msg_ok "Network in LXC is reachable (wget fallback)"
else
msg_error "No network in LXC after all checks."
read -r -p "Set fallback DNS (1.1.1.1/8.8.8.8)? [y/N]: " choice
case "$choice" in
[yY]*)
pct set "$CTID" --nameserver 1.1.1.1
pct set "$CTID" --nameserver 8.8.8.8
# Final attempt with wget after DNS change
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
msg_ok "Network reachable after DNS fallback"
else
msg_error "Still no network/DNS in LXC! Aborting customization."
exit_script
fi
;;
*)
msg_error "Aborted by user no DNS fallback set."
exit_script
;;
esac
fi
break
fi
done
fi
msg_info "Customizing LXC Container"
if [ "$var_os" == "alpine" ]; then
sleep 3
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF'
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
else
sleep 3
pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
echo LANG=\$locale_line >/etc/default/locale && \
locale-gen >/dev/null && \
export LANG=\$locale_line"
if [[ -z "${tz:-}" ]]; then
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
fi
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
else
msg_warn "Skipping timezone setup zone '$tz' not found in container"
fi
pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
msg_error "apt-get base packages installation failed"
exit 1
}
fi
msg_ok "Customized LXC Container"
install_ssh_keys_into_ct
if ! lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)"; then
exit $?
fi
}
destroy_lxc() {
if [[ -n "$CT_ID" ]]; then
read -p "Remove this Container? <y/N> " prompt
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
pct stop "$CT_ID" &>/dev/null
pct destroy "$CT_ID" &>/dev/null
msg_ok "Removed this Container"
else
msg_info "Container was not removed."
fi
else
msg_error "No CT_ID found. Nothing to remove."
fi
}
# ------------------------------------------------------------------------------
# Storage discovery / selection helpers
# ------------------------------------------------------------------------------
# ===== Storage discovery / selection helpers (ported from create_lxc.sh) =====
resolve_storage_preselect() {
local class="$1" preselect="$2" required_content=""
case "$class" in
template) required_content="vztmpl" ;;
container) required_content="rootdir" ;;
*) return 1 ;;
esac
[[ -z "$preselect" ]] && return 1
if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then
msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)"
return 1
fi
local line total used free
line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
if [[ -z "$line" ]]; then
STORAGE_INFO="n/a"
else
total="$(awk '{print $4}' <<<"$line")"
used="$(awk '{print $5}' <<<"$line")"
free="$(awk '{print $6}' <<<"$line")"
local total_h used_h free_h
if command -v numfmt >/dev/null 2>&1; then
total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
else
STORAGE_INFO="Free: ${free} Used: ${used}"
fi
fi
STORAGE_RESULT="$preselect"
return 0
}
check_storage_support() {
local CONTENT="$1" VALID=0
while IFS= read -r line; do
local STORAGE_NAME
STORAGE_NAME=$(awk '{print $1}' <<<"$line")
[[ -n "$STORAGE_NAME" ]] && VALID=1
done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
[[ $VALID -eq 1 ]]
}
select_storage() {
local CLASS=$1 CONTENT CONTENT_LABEL
case $CLASS in
container)
CONTENT='rootdir'
CONTENT_LABEL='Container'
;;
template)
CONTENT='vztmpl'
CONTENT_LABEL='Container template'
;;
iso)
CONTENT='iso'
CONTENT_LABEL='ISO image'
;;
images)
CONTENT='images'
CONTENT_LABEL='VM Disk image'
;;
backup)
CONTENT='backup'
CONTENT_LABEL='Backup'
;;
snippets)
CONTENT='snippets'
CONTENT_LABEL='Snippets'
;;
*)
msg_error "Invalid storage class '$CLASS'"
return 1
;;
esac
declare -A STORAGE_MAP
local -a MENU=()
local COL_WIDTH=0
while read -r TAG TYPE _ TOTAL USED FREE _; do
[[ -n "$TAG" && -n "$TYPE" ]] || continue
local DISPLAY="${TAG} (${TYPE})"
local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
STORAGE_MAP["$DISPLAY"]="$TAG"
MENU+=("$DISPLAY" "$INFO" "OFF")
((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
if [[ ${#MENU[@]} -eq 0 ]]; then
msg_error "No storage found for content type '$CONTENT'."
return 2
fi
if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
STORAGE_INFO="${MENU[1]}"
return 0
fi
local WIDTH=$((COL_WIDTH + 42))
while true; do
local DISPLAY_SELECTED
DISPLAY_SELECTED=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \
--title "Storage Pools" \
--radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
whiptail --msgbox "No valid storage selected. Please try again." 8 58
continue
fi
STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
for ((i = 0; i < ${#MENU[@]}; i += 3)); do
if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
STORAGE_INFO="${MENU[$i + 1]}"
break
fi
done
return 0
done
}
create_lxc_container() {
# ------------------------------------------------------------------------------
# Optional verbose mode (debug tracing)
# ------------------------------------------------------------------------------
if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
# ------------------------------------------------------------------------------
# Helpers (dynamic versioning / template parsing)
# ------------------------------------------------------------------------------
pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
# Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
# Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
# Returns:
# 0 = no upgrade needed
# 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
# 2 = user declined
# 3 = upgrade attempted but failed OR retry failed
offer_lxc_stack_upgrade_and_maybe_retry() {
local do_retry="${1:-no}" # yes|no
local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
_pvec_i="$(pkg_ver pve-container)"
_lxcp_i="$(pkg_ver lxc-pve)"
_pvec_c="$(pkg_cand pve-container)"
_lxcp_c="$(pkg_cand lxc-pve)"
if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
fi
if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
fi
if [[ $need -eq 0 ]]; then
msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
return 0
fi
echo
echo "An update for the Proxmox LXC stack is available:"
echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
echo
read -rp "Do you want to upgrade now? [y/N] " _ans
case "${_ans,,}" in
y | yes)
msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
if apt-get update -qq >/dev/null && apt-get install -y --only-upgrade pve-container lxc-pve >/dev/null; then
msg_ok "LXC stack upgraded."
if [[ "$do_retry" == "yes" ]]; then
msg_info "Retrying container creation after upgrade"
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
msg_ok "Container created successfully after upgrade."
return 1
else
msg_error "pct create still failed after upgrade. See $LOGFILE"
return 3
fi
fi
return 1
else
msg_error "Upgrade failed. Please check APT output."
return 3
fi
;;
*) return 2 ;;
esac
}
# ------------------------------------------------------------------------------
# Required input variables
# ------------------------------------------------------------------------------
[[ "${CTID:-}" ]] || {
msg_error "You need to set 'CTID' variable."
exit 203
}
[[ "${PCT_OSTYPE:-}" ]] || {
msg_error "You need to set 'PCT_OSTYPE' variable."
exit 204
}
msg_debug "CTID=$CTID"
msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
# ID checks
[[ "$CTID" -ge 100 ]] || {
msg_error "ID cannot be less than 100."
exit 205
}
if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
echo -e "ID '$CTID' is already in use."
unset CTID
msg_error "Cannot use ID that is already in use."
exit 206
fi
# Storage capability check
check_storage_support "rootdir" || {
msg_error "No valid storage found for 'rootdir' [Container]"
exit 1
}
check_storage_support "vztmpl" || {
msg_error "No valid storage found for 'vztmpl' [Template]"
exit 1
}
# Template storage selection
if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
TEMPLATE_STORAGE="$STORAGE_RESULT"
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
else
while true; do
if select_storage template; then
TEMPLATE_STORAGE="$STORAGE_RESULT"
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
break
fi
done
fi
# Container storage selection
if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
CONTAINER_STORAGE="$STORAGE_RESULT"
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
else
while true; do
if select_storage container; then
CONTAINER_STORAGE="$STORAGE_RESULT"
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
break
fi
done
fi
# Validate content types
msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
exit 217
}
$STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
else
$STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
fi
# Free space check
STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
[[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
exit 214
}
# Cluster quorum (if cluster)
if [[ -f /etc/pve/corosync.conf ]]; then
msg_info "Checking cluster quorum"
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
exit 210
fi
msg_ok "Cluster is quorate"
fi
# ------------------------------------------------------------------------------
# Template discovery & validation
# ------------------------------------------------------------------------------
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
case "$PCT_OSTYPE" in
debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
*) TEMPLATE_PATTERN="" ;;
esac
msg_info "Searching for template '$TEMPLATE_SEARCH'"
mapfile -t LOCAL_TEMPLATES < <(
pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
awk -v s="$TEMPLATE_SEARCH" -v p="$TEMPLATE_PATTERN" '$1 ~ s && $1 ~ p {print $1}' |
sed 's|.*/||' | sort -t - -k 2 -V
)
pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
mapfile -t ONLINE_TEMPLATES < <(
pveam available -section system 2>/dev/null |
sed -n "s/.*\($TEMPLATE_SEARCH.*$TEMPLATE_PATTERN.*\)/\1/p" |
sort -t - -k 2 -V
)
ONLINE_TEMPLATE=""
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
TEMPLATE_SOURCE="local"
else
TEMPLATE="$ONLINE_TEMPLATE"
TEMPLATE_SOURCE="online"
fi
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
if [[ -z "$TEMPLATE_PATH" ]]; then
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
fi
[[ -n "$TEMPLATE_PATH" ]] || {
msg_error "Unable to resolve template path for $TEMPLATE_STORAGE. Check storage type and permissions."
exit 220
}
msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
NEED_DOWNLOAD=0
if [[ ! -f "$TEMPLATE_PATH" ]]; then
msg_info "Template not present locally will download."
NEED_DOWNLOAD=1
elif [[ ! -r "$TEMPLATE_PATH" ]]; then
msg_error "Template file exists but is not readable check permissions."
exit 221
elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
if [[ -n "$ONLINE_TEMPLATE" ]]; then
msg_warn "Template file too small (<1MB) re-downloading."
NEED_DOWNLOAD=1
else
msg_warn "Template looks too small, but no online version exists. Keeping local file."
fi
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
if [[ -n "$ONLINE_TEMPLATE" ]]; then
msg_warn "Template appears corrupted re-downloading."
NEED_DOWNLOAD=1
else
msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
fi
else
$STD msg_ok "Template $TEMPLATE is present and valid."
fi
if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
TEMPLATE="$ONLINE_TEMPLATE"
NEED_DOWNLOAD=1
else
msg_info "Continuing with local template $TEMPLATE"
fi
fi
if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
for attempt in {1..3}; do
msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
msg_ok "Template download successful."
break
fi
if [[ $attempt -eq 3 ]]; then
msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
exit 222
fi
sleep $((attempt * 5))
done
fi
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
exit 223
fi
# ------------------------------------------------------------------------------
# Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
# ------------------------------------------------------------------------------
if [[ "$PCT_OSTYPE" == "debian" ]]; then
OSVER="$(parse_template_osver "$TEMPLATE")"
if [[ -n "$OSVER" ]]; then
# Proactive, aber ohne Abbruch nur Angebot
offer_lxc_stack_upgrade_and_maybe_retry "no" || true
fi
fi
# ------------------------------------------------------------------------------
# Create LXC Container
# ------------------------------------------------------------------------------
msg_info "Creating LXC container"
# Ensure subuid/subgid entries exist
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
# Assemble pct options
PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
[[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
# Lock by template file (avoid concurrent downloads/creates)
lockfile="/tmp/template.${TEMPLATE}.lock"
exec 9>"$lockfile" || {
msg_error "Failed to create lock file '$lockfile'."
exit 200
}
flock -w 60 9 || {
msg_error "Timeout while waiting for template lock."
exit 211
}
LOGFILE="/tmp/pct_create_${CTID}.log"
msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}"
msg_debug "Logfile: $LOGFILE"
# First attempt
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >"$LOGFILE" 2>&1; then
msg_error "Container creation failed on ${TEMPLATE_STORAGE}. Checking template..."
# Validate template file
if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
msg_warn "Template file too small or missing re-downloading."
rm -f "$TEMPLATE_PATH"
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
if [[ -n "$ONLINE_TEMPLATE" ]]; then
msg_warn "Template appears corrupted re-downloading."
rm -f "$TEMPLATE_PATH"
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
else
msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
fi
fi
# Retry after repair
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
# Fallback to local storage
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
msg_warn "Retrying container creation with fallback to local storage..."
LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
msg_info "Downloading template to local..."
pveam download local "$TEMPLATE" >/dev/null 2>&1
fi
if pct create "$CTID" "local:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >>"$LOGFILE" 2>&1; then
msg_ok "Container successfully created using local fallback."
else
# --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
echo
echo "pct reported 'unsupported ... version' your LXC stack might be too old for this template."
echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
if offer_lxc_stack_upgrade_and_maybe_retry "yes"; then
: # success after retry
else
rc=$?
case $rc in
2) echo "Upgrade was declined. Please update and re-run:
apt update && apt install --only-upgrade pve-container lxc-pve" ;;
3) echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" ;;
esac
exit 231
fi
else
msg_error "Container creation failed even with local fallback. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
set +x
fi
exit 209
fi
fi
else
msg_error "Container creation failed on local storage. See $LOGFILE"
# --- Dynamic stack upgrade + auto-retry on the well-known error pattern ---
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
echo
echo "pct reported 'unsupported ... version' your LXC stack might be too old for this template."
echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
if offer_lxc_stack_upgrade_and_maybe_retry "yes"; then
: # success after retry
else
rc=$?
case $rc in
2) echo "Upgrade was declined. Please update and re-run:
apt update && apt install --only-upgrade pve-container lxc-pve" ;;
3) echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" ;;
esac
exit 231
fi
else
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
bash -x -c "pct create $CTID local:vztmpl/${TEMPLATE} ${PCT_OPTIONS[*]}" 2>&1 | tee -a "$LOGFILE"
set +x
fi
exit 209
fi
fi
fi
fi
# Verify container exists
pct list | awk '{print $1}' | grep -qx "$CTID" || {
msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
exit 215
}
# Verify config rootfs
grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
msg_error "RootFS entry missing in container config. See $LOGFILE"
exit 216
}
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
}
# ------------------------------------------------------------------------------
# description()
#
# - Sets container description with HTML content (logo, links, badges)
# - Restarts ping-instances.service if present
# - Posts status "done" to API
# ------------------------------------------------------------------------------
description() {
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
# Generate LXC Description
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<img src='https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</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/&#x2615;-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
)
pct set "$CTID" -description "$DESCRIPTION"
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service
fi
post_update_to_api "done" "none"
}
# ------------------------------------------------------------------------------
# api_exit_script()
#
# - Exit trap handler
# - Reports exit codes to API with detailed reason
# - Handles known codes (100209) and maps them to errors
# ------------------------------------------------------------------------------
api_exit_script() {
exit_code=$?
if [ $exit_code -ne 0 ]; then
case $exit_code in
100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;;
101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;;
200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;;
201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;;
202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;;
203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;;
204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;;
205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;;
206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;;
207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;;
208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;;
209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;;
*) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;;
esac
fi
}
if command -v pveversion >/dev/null 2>&1; then
trap 'api_exit_script' EXIT
fi
trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM