ProxmoxVED/misc/build.func
Michel Roegl-Brunner 8f98298b68
Some checks failed
Bump build.func Revision / bump-revision (push) Has been cancelled
Clean up diagnostics comments in build.func
Removed commented lines related to diagnostics information.
2025-09-23 14:50:22 +02:00

3175 lines
108 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

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.
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
#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
catch_errors
#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
catch_errors
#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
}
# ------------------------------------------------------------------------------
# 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:
#"disk_size"
#"core_count"
#"ram_size"
#"os_type"
#"os_version"
#"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:
#"disk_size"
#"core_count"
#"ram_size"
#"os_type"
#"os_version"
#"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
}
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
if [[ -n "$MAC" ]]; then
case "$MAC" in
,hwaddr=*) NET_STRING+="$MAC" ;;
*) NET_STRING+=",hwaddr=$MAC" ;;
esac
fi
# IP (immer zwingend, Standard dhcp)
NET_STRING+=",ip=${NET:-dhcp}"
# Gateway
if [[ -n "$GATE" ]]; then
case "$GATE" in
,gw=*) NET_STRING+="$GATE" ;;
*) NET_STRING+=",gw=$GATE" ;;
esac
fi
# VLAN
if [[ -n "$VLAN" ]]; then
case "$VLAN" in
,tag=*) NET_STRING+="$VLAN" ;;
*) NET_STRING+=",tag=$VLAN" ;;
esac
fi
# MTU
if [[ -n "$MTU" ]]; then
case "$MTU" in
,mtu=*) NET_STRING+="$MTU" ;;
*) NET_STRING+=",mtu=$MTU" ;;
esac
fi
# IPv6 Handling
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
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 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"
# ============================================================================
# GPU/USB PASSTHROUGH CONFIGURATION
# ============================================================================
# List of applications that benefit from GPU acceleration
GPU_APPS=(
"immich" "channels" "emby" "ersatztv" "frigate"
"jellyfin" "plex" "scrypted" "tdarr" "unmanic"
"ollama" "fileflows" "open-webui" "tunarr" "debian"
"handbrake" "sunshine" "moonlight" "kodi" "stremio"
)
# ------------------------------------------------------------------------------
# Helper Functions for GPU/USB Configuration
# ------------------------------------------------------------------------------
# Get device GID dynamically
get_device_gid() {
local group="$1"
local gid
gid=$(getent group "$group" 2>/dev/null | cut -d: -f3)
if [[ -z "$gid" ]]; then
case "$group" in
video) gid=44 ;;
render) gid=104 ;;
*) gid=44 ;;
esac
fi
echo "$gid"
}
# Check if app needs GPU
is_gpu_app() {
local app="${1,,}"
for gpu_app in "${GPU_APPS[@]}"; do
[[ "$app" == "${gpu_app,,}" ]] && return 0
done
return 1
}
# Detect available GPU devices
detect_gpu_devices() {
VAAPI_DEVICES=()
NVIDIA_DEVICES=()
# Detect VAAPI devices (Intel/AMD)
if [[ -d /dev/dri ]]; then
for device in /dev/dri/renderD* /dev/dri/card*; do
[[ -e "$device" ]] && VAAPI_DEVICES+=("$device")
done
fi
# Detect NVIDIA devices
for device in /dev/nvidia*; do
[[ -e "$device" ]] && NVIDIA_DEVICES+=("$device")
done
}
# Configure USB passthrough for privileged containers
configure_usb_passthrough() {
if [[ "$CT_TYPE" != "0" ]]; then
return 0
fi
msg_info "Configuring automatic USB passthrough (privileged container)"
cat <<EOF >>"$LXC_CONFIG"
# USB passthrough (privileged container)
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
msg_ok "USB passthrough configured"
}
# Configure VAAPI device
configure_vaapi_device() {
local device="$1"
local dev_index="$2"
if [[ "$CT_TYPE" == "0" ]]; then
# Privileged container
local major minor
major=$(stat -c '%t' "$device")
minor=$(stat -c '%T' "$device")
major=$((0x$major))
minor=$((0x$minor))
echo "lxc.cgroup2.devices.allow: c $major:$minor rwm" >>"$LXC_CONFIG"
echo "lxc.mount.entry: $device dev/$(basename "$device") none bind,optional,create=file" >>"$LXC_CONFIG"
else
# Unprivileged container
local gid
if [[ "$device" =~ renderD ]]; then
gid=$(get_device_gid "render")
else
gid=$(get_device_gid "video")
fi
echo "dev${dev_index}: $device,gid=$gid" >>"$LXC_CONFIG"
fi
}
# Configure NVIDIA devices
configure_nvidia_devices() {
for device in "${NVIDIA_DEVICES[@]}"; do
if [[ "$CT_TYPE" == "0" ]]; then
local major minor
major=$(stat -c '%t' "$device")
minor=$(stat -c '%T' "$device")
major=$((0x$major))
minor=$((0x$minor))
echo "lxc.cgroup2.devices.allow: c $major:$minor rwm" >>"$LXC_CONFIG"
echo "lxc.mount.entry: $device dev/$(basename "$device") none bind,optional,create=file" >>"$LXC_CONFIG"
else
msg_warn "NVIDIA passthrough to unprivileged container may require additional configuration"
fi
done
if [[ -d /dev/dri ]] && [[ "$CT_TYPE" == "0" ]]; then
echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
fi
}
# Main GPU configuration logic
configure_gpu_passthrough() {
detect_gpu_devices
# Check if we should configure GPU
local should_configure=false
if [[ "$CT_TYPE" == "0" ]] || is_gpu_app "$APP"; then
should_configure=true
fi
if [[ "$should_configure" == "false" ]]; then
return 0
fi
# No GPU devices available
if [[ ${#VAAPI_DEVICES[@]} -eq 0 ]] && [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
msg_info "No GPU devices detected on host"
return 0
fi
# Build selection options
local choices=()
local SELECTED_GPUS=()
if [[ ${#VAAPI_DEVICES[@]} -gt 0 ]]; then
choices+=("VAAPI" "Intel/AMD GPU (${#VAAPI_DEVICES[@]} devices)" "OFF")
fi
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
choices+=("NVIDIA" "NVIDIA GPU (${#NVIDIA_DEVICES[@]} devices)" "OFF")
fi
# Auto-select if only one type available
if [[ ${#choices[@]} -eq 3 ]]; then
SELECTED_GPUS=("${choices[0]}")
msg_info "Auto-configuring ${choices[0]} GPU passthrough"
elif [[ ${#choices[@]} -gt 3 ]]; then
# Show selection dialog
local selected
selected=$(whiptail --title "GPU Passthrough Selection" \
--checklist "Multiple GPU types detected. Select which to pass through:" \
12 60 $((${#choices[@]} / 3)) \
"${choices[@]}" 3>&1 1>&2 2>&3) || {
msg_info "GPU passthrough skipped"
return 0
}
for item in $selected; do
SELECTED_GPUS+=("${item//\"/}")
done
fi
# Apply configuration for selected GPUs
local dev_index=0
for gpu_type in "${SELECTED_GPUS[@]}"; do
case "$gpu_type" in
VAAPI)
msg_info "Configuring VAAPI passthrough (${#VAAPI_DEVICES[@]} devices)"
for device in "${VAAPI_DEVICES[@]}"; do
configure_vaapi_device "$device" "$dev_index"
((dev_index++))
done
if [[ "$CT_TYPE" == "0" ]] && [[ -d /dev/dri ]]; then
echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
fi
export ENABLE_VAAPI=1
;;
NVIDIA)
msg_info "Configuring NVIDIA passthrough"
configure_nvidia_devices
export ENABLE_NVIDIA=1
;;
esac
done
[[ ${#SELECTED_GPUS[@]} -gt 0 ]] && msg_ok "GPU passthrough configured"
}
# ------------------------------------------------------------------------------
# Apply all hardware passthrough configurations
# ------------------------------------------------------------------------------
# USB passthrough (automatic for privileged)
configure_usb_passthrough
# GPU passthrough (based on container type and app)
configure_gpu_passthrough
# 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
# Coral TPU passthrough (if available)
if [[ -e /dev/apex_0 ]]; then
msg_info "Detected Coral TPU - configuring passthrough"
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
fi
# ============================================================================
# START CONTAINER AND INSTALL USERLAND
# ============================================================================
msg_info "Starting LXC Container"
pct start "$CTID"
# Wait for container to be 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
# Wait for network (skip for Alpine initially)
if [ "$var_os" != "alpine" ]; then
msg_info "Waiting for network in LXC container"
# Wait for IP
for i in {1..20}; do
ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
[ -n "$ip_in_lxc" ] && break
sleep 1
done
if [ -z "$ip_in_lxc" ]; then
msg_error "No IP assigned to CT $CTID after 20s"
exit 1
fi
# Try to reach gateway
gw_ok=0
for i in {1..10}; do
if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then
gw_ok=1
break
fi
sleep 1
done
if [ "$gw_ok" -eq 1 ]; then
msg_ok "Network in LXC is reachable (IP $ip_in_lxc)"
else
msg_warn "Network reachable but gateway check failed"
fi
fi
# Install GPU userland packages
install_gpu_userland() {
local gpu_type="$1"
if [ "$var_os" == "alpine" ]; then
case "$gpu_type" in
VAAPI)
msg_info "Installing VAAPI packages in Alpine container"
pct exec "$CTID" -- ash -c '
apk add --no-cache \
mesa-dri-gallium \
mesa-va-gallium \
intel-media-driver \
libva-utils >/dev/null 2>&1
' || msg_warn "Some VAAPI packages may not be available in Alpine"
;;
NVIDIA)
msg_warn "NVIDIA drivers are not readily available in Alpine Linux"
;;
esac
else
case "$gpu_type" in
VAAPI)
msg_info "Installing VAAPI userland packages"
pct exec "$CTID" -- bash -c '
. /etc/os-release || true
if [[ "${VERSION_CODENAME:-}" == "trixie" ]]; then
cat >/etc/apt/sources.list.d/non-free.sources <<EOF
Types: deb deb-src
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates trixie-security
Components: non-free non-free-firmware
EOF
fi
apt-get update >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive apt-get install -y \
intel-media-va-driver-non-free \
mesa-va-drivers \
libvpl2 \
vainfo \
ocl-icd-libopencl1 \
mesa-opencl-icd \
intel-gpu-tools >/dev/null 2>&1
' && msg_ok "VAAPI userland installed" || msg_warn "Some VAAPI packages failed to install"
;;
NVIDIA)
msg_info "Installing NVIDIA userland packages"
pct exec "$CTID" -- bash -c '
apt-get update >/dev/null 2>&1
DEBIAN_FRONTEND=noninteractive apt-get install -y \
nvidia-driver \
nvidia-utils \
libnvidia-encode1 \
libcuda1 >/dev/null 2>&1
' && msg_ok "NVIDIA userland installed" || msg_warn "Some NVIDIA packages failed to install"
;;
esac
fi
}
# Customize container
msg_info "Customizing LXC Container"
# Install GPU userland if configured
if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
install_gpu_userland "VAAPI"
fi
if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
install_gpu_userland "NVIDIA"
fi
# Continue with standard container setup
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"
# Verify GPU access if enabled
if [[ "${ENABLE_VAAPI:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
pct exec "$CTID" -- bash -c "vainfo >/dev/null 2>&1" &&
msg_ok "VAAPI verified working" ||
msg_warn "VAAPI verification failed - may need additional configuration"
fi
if [[ "${ENABLE_NVIDIA:-0}" == "1" ]] && [ "$var_os" != "alpine" ]; then
pct exec "$CTID" -- bash -c "nvidia-smi >/dev/null 2>&1" &&
msg_ok "NVIDIA verified working" ||
msg_warn "NVIDIA verification failed - may need additional configuration"
fi
# Install SSH keys
install_ssh_keys_into_ct
# Run application installer
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 [[ -z "$CT_ID" ]]; then
msg_error "No CT_ID found. Nothing to remove."
return 1
fi
# Abbruch bei Ctrl-C / Ctrl-D / ESC
trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
local prompt
if ! read -rp "Remove this Container? <y/N> " prompt; then
# read gibt != 0 zurück bei Ctrl-D/ESC
msg_error "Aborted input (Ctrl-D/ESC)"
return 130
fi
case "${prompt,,}" in
y | yes)
if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
msg_ok "Removed Container $CT_ID"
else
msg_error "Failed to remove Container $CT_ID"
return 1
fi
;;
"" | n | no)
msg_info "Container was not removed."
;;
*)
msg_warn "Invalid response. Container was not removed."
;;
esac
}
# ------------------------------------------------------------------------------
# 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