2025-12-04 07:52:18 +01:00
#!/usr/bin/env bash
2026-01-06 13:28:12 +01:00
# Copyright (c) 2021-2026 community-scripts ORG
2025-12-04 07:52:18 +01:00
# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
# ==============================================================================
# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION
# ==============================================================================
#
# This file provides the main build functions for creating and configuring
# LXC containers in Proxmox VE. It handles:
#
# - Variable initialization and defaults
# - Container creation and resource allocation
# - Storage selection and management
# - Advanced configuration and customization
# - User interaction menus and prompts
#
# Usage:
# - Sourced automatically by CT creation scripts
# - Requires core.func and error_handler.func to be loaded first
#
# ==============================================================================
# ==============================================================================
# SECTION 1: INITIALIZATION & CORE VARIABLES
# ==============================================================================
# ------------------------------------------------------------------------------
# variables()
#
# - Initializes core variables for container creation
# - Normalizes application name (NSAPP = lowercase, no spaces)
# - Builds installer filename (var_install)
# - Defines regex patterns for validation
# - Fetches Proxmox hostname and version
# - Generates unique session ID for tracking and logging
# - Captures app-declared resource defaults (CPU, RAM, Disk)
# ------------------------------------------------------------------------------
2023-03-22 20:48:20 -04:00
variables() {
2025-04-09 15:30:54 +02:00
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.
2025-02-24 11:18:38 +01:00
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.
2025-02-03 23:40:22 +01:00
RANDOM_UUID = " $( cat /proc/sys/kernel/random/uuid) " # generates a random UUID and sets it to the RANDOM_UUID variable.
2025-12-04 07:52:18 +01:00
SESSION_ID = " ${ RANDOM_UUID : 0 : 8 } " # Short session ID (first 8 chars of UUID) for log files
BUILD_LOG = "/tmp/create-lxc- ${ SESSION_ID } .log" # Host-side container creation log
CTTYPE = " ${ CTTYPE :- ${ CT_TYPE :- 1 }} "
# Parse dev_mode early
parse_dev_mode
# Setup persistent log directory if logs mode active
if [[ " ${ DEV_MODE_LOGS :- false } " == "true" ]] ; then
mkdir -p /var/log/community-scripts
BUILD_LOG = "/var/log/community-scripts/create-lxc- ${ SESSION_ID } - $( date +%Y%m%d_%H%M%S) .log"
fi
# Get Proxmox VE version and kernel version
if command -v pveversion >/dev/null 2>& 1; then
PVEVERSION = " $( pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}' ) "
else
PVEVERSION = "N/A"
fi
KERNEL_VERSION = $( uname -r)
# Capture app-declared defaults (for precedence logic)
# These values are set by the app script BEFORE default.vars is loaded
# If app declares higher values than default.vars, app values take precedence
if [[ -n " ${ var_cpu :- } " && " ${ var_cpu } " = ~ ^[ 0-9] +$ ]] ; then
export APP_DEFAULT_CPU = " ${ var_cpu } "
fi
if [[ -n " ${ var_ram :- } " && " ${ var_ram } " = ~ ^[ 0-9] +$ ]] ; then
export APP_DEFAULT_RAM = " ${ var_ram } "
fi
if [[ -n " ${ var_disk :- } " && " ${ var_disk } " = ~ ^[ 0-9] +$ ]] ; then
export APP_DEFAULT_DISK = " ${ var_disk } "
fi
2023-03-22 20:48:20 -04:00
}
2025-02-05 16:29:19 +01:00
2025-04-01 10:25:46 +02:00
source <( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
2023-03-22 20:48:20 -04:00
2025-06-20 13:28:06 +02:00
if command -v curl >/dev/null 2>& 1; then
source <( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
2025-12-04 07:52:18 +01:00
source <( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
2025-06-20 13:28:06 +02:00
load_functions
2025-12-04 07:52:18 +01:00
catch_errors
2025-06-20 13:28:06 +02:00
elif command -v wget >/dev/null 2>& 1; then
source <( wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
2025-12-04 07:52:18 +01:00
source <( wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
2025-06-20 13:28:06 +02:00
load_functions
2025-12-04 07:52:18 +01:00
catch_errors
2025-06-20 13:28:06 +02:00
fi
2025-07-28 11:00:09 +02:00
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION
# ==============================================================================
2025-07-28 11:00:09 +02:00
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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
# ------------------------------------------------------------------------------
2023-03-22 20:48:20 -04:00
2025-01-20 09:19:47 +01:00
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
2025-12-04 07:52:18 +01:00
# Silent success - only show errors if they exist
2023-03-22 20:48:20 -04:00
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 3: CONTAINER SETUP UTILITIES
# ==============================================================================
# ------------------------------------------------------------------------------
# get_current_ip()
#
# - Returns current container IP depending on OS type
# - Debian/Ubuntu: uses `hostname -I`
# - Alpine: parses eth0 via `ip -4 addr`
# - Returns "Unknown" if OS type cannot be determined
# ------------------------------------------------------------------------------
2024-12-28 20:37:58 +01:00
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 "
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# update_motd_ip()
#
# - Updates /etc/motd with current container IP
# - Removes old IP entries to avoid duplicates
# ------------------------------------------------------------------------------
2024-12-28 20:37:58 +01:00
update_motd_ip() {
MOTD_FILE = "/etc/motd"
2025-01-29 14:26:23 +01:00
2024-12-28 20:37:58 +01:00
if [ -f " $MOTD_FILE " ] ; then
# Remove existing IP Address lines to prevent duplication
sed -i '/IP Address:/d' " $MOTD_FILE "
2025-01-29 14:26:23 +01:00
2024-12-28 20:37:58 +01:00
IP = $( get_current_ip)
# Add the new IP address
2025-02-24 11:18:38 +01:00
echo -e " ${ TAB }${ NETWORK }${ YW } IP Address: ${ GN }${ IP }${ CL } " >>" $MOTD_FILE "
2024-12-28 20:37:58 +01:00
fi
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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() {
2025-12-15 13:02:28 +01:00
[[ " ${ SSH :- no } " != "yes" ]] && return 0
# Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped)
: " ${ SSH_KEYS_FILE := } "
2025-12-04 07:52:18 +01:00
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
msg_warn "No SSH keys to install (skipping)."
return 0
}
# ------------------------------------------------------------------------------
# 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)))
2023-03-26 11:11:13 -04:00
fi
2023-03-26 11:06:28 -04:00
fi
2025-12-04 07:52:18 +01:00
FOUND_HOST_KEY_COUNT = " $total "
(
IFS = :
echo " ${ files [*] } "
)
}
2025-12-16 16:52:37 +01:00
# ==============================================================================
# SECTION 3B: IP RANGE SCANNING
# ==============================================================================
# ------------------------------------------------------------------------------
# ip_to_int() / int_to_ip()
#
# - Converts IP address to integer and vice versa for range iteration
# ------------------------------------------------------------------------------
ip_to_int() {
local IFS = .
read -r i1 i2 i3 i4 <<< " $1 "
echo $(( ( i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
}
int_to_ip() {
local ip=$1
echo "$(((ip >> 24 ) & 0 xFF)) .$(( ( ip >> 16 ) & 0 xFF)) .$(( ( ip >> 8 ) & 0 xFF)) .$(( ip & 0 xFF)) "
}
# ------------------------------------------------------------------------------
# resolve_ip_from_range()
#
# - Takes an IP range in format " 10.0.0.1/24-10.0.0.10/24"
# - Pings each IP in the range to find the first available one
# - Returns the first free IP with CIDR notation
# - Sets NET_RESOLVED to the resolved IP or empty on failure
# ------------------------------------------------------------------------------
resolve_ip_from_range() {
local range=" $1 "
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2}) $'
local ip_start ip_end
# Parse range: " 10.0.0.1/24-10.0.0.10/24"
ip_start=" ${ range %%-* } "
ip_end=" ${ range ##*- } "
if [[ ! " $ip_start " =~ $ip_cidr_regex ]] || [[ ! " $ip_end " =~ $ip_cidr_regex ]]; then
NET_RESOLVED=""
return 1
fi
local ip1=" ${ ip_start %%/* } "
local ip2=" ${ ip_end %%/* } "
local cidr=" ${ ip_start ##*/ } "
local start_int= $( ip_to_int " $ip1 " )
local end_int= $( ip_to_int " $ip2 " )
for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do
local ip= $( int_to_ip $ip_int )
msg_info " Checking IP: $ip "
if ! ping -c 1 -W 1 " $ip " >/dev/null 2>&1; then
NET_RESOLVED=" $ip /$cidr "
msg_ok " Found free IP: ${ BGN } $NET_RESOLVED ${ CL } "
return 0
fi
done
NET_RESOLVED=""
msg_error " No free IP found in range $range "
return 1
}
# ------------------------------------------------------------------------------
# is_ip_range()
#
# - Checks if a string is an IP range (contains - and looks like IP/CIDR)
# - Returns 0 if it's a range, 1 otherwise
# ------------------------------------------------------------------------------
is_ip_range() {
local value=" $1 "
local ip_start ip_end
if [[ " $value " == *-* ]] && [[ " $value " != " dhcp" ]]; then
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2}) $'
ip_start=" ${ value %%-* } "
ip_end=" ${ value ##*- } "
if [[ " $ip_start " =~ $ip_cidr_regex ]] && [[ " $ip_end " =~ $ip_cidr_regex ]]; then
return 0
fi
fi
return 1
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 4: STORAGE & RESOURCE MANAGEMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# _write_storage_to_vars()
#
# - Writes storage selection to vars file
# - Removes old entries (commented and uncommented) to avoid duplicates
# - Arguments: vars_file, key (var_container_storage/var_template_storage), value
# ------------------------------------------------------------------------------
_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
# Silent operation - no output message
2023-03-26 11:06:28 -04:00
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# base_settings()
#
# - Defines all base/default variables for container creation
# - Reads from environment variables (var_*)
# - Provides fallback defaults for OS type/version
# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK)
# - Sets up container type, resources, network, SSH, features, and tags
# ------------------------------------------------------------------------------
2024-12-16 12:41:51 +01:00
base_settings() {
# Default Settings
2025-09-08 14:03:34 +02:00
CT_TYPE = ${ var_unprivileged :- "1" }
2025-12-04 07:52:18 +01:00
# Resource allocation: App defaults take precedence if HIGHER
# Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values
local final_disk = " ${ var_disk :- 4 } "
local final_cpu = " ${ var_cpu :- 1 } "
local final_ram = " ${ var_ram :- 1024 } "
# If app declared higher values, use those instead
if [[ -n " ${ APP_DEFAULT_DISK :- } " && " ${ APP_DEFAULT_DISK } " = ~ ^[ 0-9] +$ ]] ; then
if [[ " ${ APP_DEFAULT_DISK } " -gt " ${ final_disk } " ]] ; then
final_disk = " ${ APP_DEFAULT_DISK } "
fi
fi
if [[ -n " ${ APP_DEFAULT_CPU :- } " && " ${ APP_DEFAULT_CPU } " = ~ ^[ 0-9] +$ ]] ; then
if [[ " ${ APP_DEFAULT_CPU } " -gt " ${ final_cpu } " ]] ; then
final_cpu = " ${ APP_DEFAULT_CPU } "
fi
fi
if [[ -n " ${ APP_DEFAULT_RAM :- } " && " ${ APP_DEFAULT_RAM } " = ~ ^[ 0-9] +$ ]] ; then
if [[ " ${ APP_DEFAULT_RAM } " -gt " ${ final_ram } " ]] ; then
final_ram = " ${ APP_DEFAULT_RAM } "
fi
fi
DISK_SIZE = " ${ final_disk } "
CORE_COUNT = " ${ final_cpu } "
RAM_SIZE = " ${ final_ram } "
2025-09-08 14:03:34 +02:00
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" }
2025-12-16 16:52:37 +01:00
# Resolve IP range if NET contains a range (e.g., 192.168.1.100/24-192.168.1.200/24)
if is_ip_range " $NET " ; then
msg_info "Scanning IP range: $NET "
if resolve_ip_from_range " $NET " ; then
NET = " $NET_RESOLVED "
else
msg_error "Could not find free IP in range. Falling back to DHCP."
NET = "dhcp"
fi
fi
2025-09-08 14:03:34 +02:00
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 :- "" }
2025-12-04 07:52:18 +01:00
# Runtime check: Verify APT cacher is reachable if configured
if [[ -n " $APT_CACHER_IP " && " $APT_CACHER " == "yes" ]] ; then
if ! curl -s --connect-timeout 2 "http:// ${ APT_CACHER_IP } :3142" >/dev/null 2>& 1; then
msg_warn "APT Cacher configured but not reachable at ${ APT_CACHER_IP } :3142"
msg_custom "⚠️" " ${ YW } " "Disabling APT Cacher for this installation"
APT_CACHER = ""
APT_CACHER_IP = ""
else
msg_ok "APT Cacher verified at ${ APT_CACHER_IP } :3142"
fi
fi
2025-09-08 14:03:34 +02:00
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 :- "" }
2025-12-04 07:52:18 +01:00
TAGS = "community-script, ${ var_tags :- } "
2025-09-08 14:03:34 +02:00
ENABLE_FUSE = ${ var_fuse :- " ${ 1 :- no } " }
ENABLE_TUN = ${ var_tun :- " ${ 1 :- no } " }
2025-01-13 16:04:59 +01:00
2025-12-15 13:02:28 +01:00
# Additional settings that may be skipped if advanced_settings is not run (e.g., App Defaults)
ENABLE_GPU = ${ var_gpu :- "no" }
ENABLE_NESTING = ${ var_nesting :- "1" }
ENABLE_KEYCTL = ${ var_keyctl :- "0" }
ENABLE_MKNOD = ${ var_mknod :- "0" }
PROTECT_CT = ${ var_protection :- "no" }
CT_TIMEZONE = ${ var_timezone :- " $timezone " }
2025-12-16 11:28:12 +01:00
[[ " ${ CT_TIMEZONE :- } " == Etc/* ]] && CT_TIMEZONE = "host" # pct doesn't accept Etc/* zones
2025-12-15 13:02:28 +01:00
2024-12-16 12:41:51 +01:00
# 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
}
2025-09-08 14:03:34 +02:00
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# load_vars_file()
#
# - Safe parser for KEY=VALUE lines from vars files
# - Used by default_var_settings and app defaults loading
# - Only loads whitelisted var_* keys
2025-12-15 09:46:49 +01:00
# - Optional force parameter to override existing values (for app defaults)
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
load_vars_file() {
local file = " $1 "
2025-12-15 09:46:49 +01:00
local force = " ${ 2 :- no } " # If "yes", override existing variables
2025-12-04 07:52:18 +01:00
[ -f " $file " ] || return 0
msg_info "Loading defaults from ${ file } "
# Allowed var_* keys
local VAR_WHITELIST =(
2025-12-08 13:52:30 +01:00
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl
2025-12-04 07:52:18 +01:00
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
2025-07-14 22:17:48 +02:00
2025-12-04 07:52:18 +01:00
# Whitelist check helper
_is_whitelisted() {
local k = " $1 " w
for w in " ${ VAR_WHITELIST [@] } " ; do [ " $k " = " $w " ] && return 0; done
return 1
}
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 " $var_key " || continue
2025-12-16 17:11:14 +01:00
# Strip inline comments (anything after unquoted #)
# Only strip if not inside quotes
if [[ ! " $var_val " = ~ ^[ \"\' ] ]] ; then
var_val = " ${ var_val %%#* } "
fi
2025-12-04 07:52:18 +01:00
# Strip quotes
if [[ " $var_val " = ~ ^\" ( .*) \" $ ]] ; then
var_val = " ${ BASH_REMATCH [1] } "
elif [[ " $var_val " = ~ ^\' ( .*) \' $ ]] ; then
var_val = " ${ BASH_REMATCH [1] } "
2025-05-16 15:48:18 +02:00
fi
2025-12-04 07:52:18 +01:00
2025-12-16 17:11:14 +01:00
# Trim trailing whitespace
var_val = " ${ var_val % " ${ var_val ##*[![: space :]] } " } "
2025-12-15 09:46:49 +01:00
# Set variable: force mode overrides existing, otherwise only set if empty
if [[ " $force " == "yes" ]] ; then
export " ${ var_key } = ${ var_val } "
else
[[ -z " ${ !var_key+x } " ]] && export " ${ var_key } = ${ var_val } "
fi
2025-05-16 15:48:18 +02:00
fi
2025-12-04 07:52:18 +01:00
done <" $file "
msg_ok "Loaded ${ file } "
2025-05-16 15:48:18 +02:00
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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)
# Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
local VAR_WHITELIST =(
2025-12-08 13:52:30 +01:00
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu var_keyctl
2025-12-04 07:52:18 +01:00
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
)
2024-12-16 12:41:51 +01:00
2025-12-04 07:52:18 +01:00
# 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"
# Silent creation - no msg_info output
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_vlan=
# var_mtu=
# var_mac=
# var_ns=
# SSH
var_ssh=no
# var_ssh_authorized_key=
# APT cacher (optional - with example)
# var_apt_cacher=yes
# var_apt_cacher_ip=192.168.1.10
# Features/Tags/verbosity
var_fuse=no
var_tun=no
# Advanced Settings (Proxmox-official features)
var_nesting=1 # Allow nesting (required for Docker/LXC in CT)
var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround)
var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental)
var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults)
var_protection=no # Prevent accidental deletion of container
var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone)
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 "
# Silent creation - no output message
}
# 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
}
# 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"
2024-12-16 12:41:51 +01:00
fi
2025-12-04 07:52:18 +01:00
# 4) Apply base settings and show summary
METHOD = "mydefaults-global"
base_settings " $VERBOSE "
header_info
echo -e " ${ DEFAULT }${ BOLD }${ BL } Using User Defaults (default.vars) on node $PVEHOST_NAME ${ CL } "
echo_default
2023-03-22 20:48:20 -04:00
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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"
2025-05-06 09:50:11 +02:00
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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
# Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
declare -ag VAR_WHITELIST =(
2025-12-08 13:52:30 +01:00
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_gpu
2025-12-04 07:52:18 +01:00
var_gateway var_hostname var_ipv6_method 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
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# Global whitelist check function (used by _load_vars_file_to_map and others)
_is_whitelisted_key() {
local k = " $1 "
local w
for w in " ${ VAR_WHITELIST [@] } " ; do [ " $k " = " $w " ] && return 0; done
return 1
}
2025-06-20 13:28:06 +02:00
2025-12-04 07:52:18 +01:00
_sanitize_value() {
# Disallow Command-Substitution / Shell-Meta
case " $1 " in
*'$(' * | *'`' * | *';' * | *'&' * | *'<(' *)
echo ""
return 0
;;
esac
echo " $1 "
}
2025-06-20 13:28:06 +02:00
2025-12-04 07:52:18 +01:00
# Map-Parser: read var_* from file into _VARS_IN associative array
# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
# This simplified version is used specifically for diff operations via _VARS_IN array
declare -A _VARS_IN
_load_vars_file_to_map() {
local file = " $1 "
[ -f " $file " ] || return 0
_VARS_IN =() # Clear array
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
_VARS_IN[ " $key " ]= " $val "
2025-06-20 13:28:06 +02:00
fi
2025-12-04 07:52:18 +01:00
;;
esac
done <" $file "
}
2025-06-20 13:28:06 +02:00
2025-12-04 07:52:18 +01:00
# 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
2025-06-20 13:28:06 +02:00
fi
2023-03-22 20:48:20 -04:00
else
2025-12-04 07:52:18 +01:00
out += "- ${ k } \n - old: ${ OLD [ $k ] } \n"
found_change = 1
2023-03-22 20:48:20 -04:00
fi
2024-04-23 06:55:18 -04:00
done
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# Added
for k in " ${ !NEW[@] } " ; do
if [[ ! -v OLD[ " $k " ] ]] ; then
out += "+ ${ k } \n + new: ${ NEW [ $k ] } \n"
found_change = 1
2023-03-22 20:48:20 -04:00
fi
2025-12-04 07:52:18 +01:00
done
if [[ $found_change -eq 0 ]] ; then
out += "(No differences)\n"
2023-03-22 20:48:20 -04:00
fi
2025-12-04 07:52:18 +01:00
printf "%b" " $out "
}
2025-06-19 18:48:35 +02:00
2025-12-04 07:52:18 +01:00
# Build a temporary <app>.vars file from current advanced settings
_build_current_app_vars_tmp() {
tmpf = " $( mktemp /tmp/${ NSAPP :- app } .vars.new.XXXXXX) "
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# NET/GW
_net = " ${ NET :- } "
_gate = ""
case " ${ GATE :- } " in
,gw= *) _gate = $( echo " $GATE " | sed 's/^,gw=//' ) ;;
esac
2025-06-19 18:48:35 +02:00
2025-12-04 07:52:18 +01:00
# IPv6
_ipv6_method = " ${ IPV6_METHOD :- auto } "
_ipv6_static = ""
_ipv6_gateway = ""
if [ " $_ipv6_method " = "static" ] ; then
_ipv6_static = " ${ IPV6_ADDR :- } "
_ipv6_gateway = " ${ IPV6_GATE :- } "
fi
2025-06-19 18:48:35 +02:00
2025-12-04 07:52:18 +01:00
# 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
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# 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
2025-06-19 18:48:35 +02:00
2025-12-04 07:52:18 +01:00
# 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 } "
2025-12-09 16:01:31 +01:00
_gpu = " ${ ENABLE_GPU :- no } "
2025-12-04 07:52:18 +01:00
_nesting = " ${ ENABLE_NESTING :- 1 } "
_keyctl = " ${ ENABLE_KEYCTL :- 0 } "
_mknod = " ${ ENABLE_MKNOD :- 0 } "
_mount_fs = " ${ ALLOW_MOUNT_FS :- } "
_protect = " ${ PROTECT_CT :- no } "
_timezone = " ${ CT_TIMEZONE :- } "
_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 " ) "
# var_ipv6_static removed - static IPs are unique, can't be default
[ -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 " ) "
2025-12-09 16:01:31 +01:00
[ -n " $_gpu " ] && echo "var_gpu= $( _sanitize_value " $_gpu " ) "
2025-12-04 07:52:18 +01:00
[ -n " $_nesting " ] && echo "var_nesting= $( _sanitize_value " $_nesting " ) "
[ -n " $_keyctl " ] && echo "var_keyctl= $( _sanitize_value " $_keyctl " ) "
[ -n " $_mknod " ] && echo "var_mknod= $( _sanitize_value " $_mknod " ) "
[ -n " $_mount_fs " ] && echo "var_mount_fs= $( _sanitize_value " $_mount_fs " ) "
[ -n " $_protect " ] && echo "var_protection= $( _sanitize_value " $_protect " ) "
[ -n " $_timezone " ] && echo "var_timezone= $( _sanitize_value " $_timezone " ) "
[ -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 "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 } "
2023-03-22 20:48:20 -04:00
fi
2025-12-04 07:52:18 +01:00
rm -f " $new_tmp " " $diff_tmp "
return 0
fi
2025-06-19 18:48:35 +02:00
2025-12-04 07:52:18 +01:00
# 2) if file exists → build diff
_build_vars_diff " $app_vars_path " " $new_tmp " >" $diff_tmp "
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# if no differences → do nothing
if grep -q "^(No differences) $" " $diff_tmp " ; then
rm -f " $new_tmp " " $diff_tmp "
return 0
fi
2025-06-19 18:48:35 +02:00
2025-12-04 07:52:18 +01:00
# 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 "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 } "
2025-06-19 18:48:35 +02:00
break
2025-12-04 07:52:18 +01:00
;;
"Keep Current" )
msg_custom "ℹ ️" " ${ BL } " "Keeping current app defaults: ${ app_vars_path } "
break
;;
"View Diff" )
whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "Diff – ${ APP } " \
--scrolltext --textbox " $diff_tmp " 25 100
;;
"Cancel" | *)
msg_custom "🚫" " ${ YW } " "Canceled. No changes to app defaults."
break
;;
esac
2025-06-19 18:48:35 +02:00
done
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
rm -f " $new_tmp " " $diff_tmp "
}
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
ensure_storage_selection_for_vars_file() {
local vf = " $1 "
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
# Read stored values (if any)
local tpl ct
tpl = $( grep -E '^var_template_storage=' " $vf " | cut -d= -f2-)
ct = $( grep -E '^var_container_storage=' " $vf " | cut -d= -f2-)
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
if [[ -n " $tpl " && -n " $ct " ]] ; then
TEMPLATE_STORAGE = " $tpl "
CONTAINER_STORAGE = " $ct "
return 0
fi
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
choose_and_set_storage_for_file " $vf " template
choose_and_set_storage_for_file " $vf " container
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
# Silent operation - no output message
}
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
ensure_global_default_vars_file() {
local vars_path = "/usr/local/community-scripts/default.vars"
if [[ ! -f " $vars_path " ]] ; then
mkdir -p " $( dirname " $vars_path " ) "
touch " $vars_path "
fi
echo " $vars_path "
}
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION
# ==============================================================================
# ------------------------------------------------------------------------------
# advanced_settings()
#
# - Interactive wizard-style configuration with BACK navigation
# - State-machine approach: each step can go forward or backward
# - Cancel at Step 1 = Exit Script, Cancel at other steps = Go Back
# - Allows user to customize all container settings
# ------------------------------------------------------------------------------
advanced_settings() {
# Enter alternate screen buffer to prevent flicker between dialogs
tput smcup 2>/dev/null || true
trap 'tput rmcup 2>/dev/null || true' RETURN
2025-05-13 18:44:06 +02:00
2025-12-04 07:52:18 +01:00
# Initialize defaults
TAGS = "community-script; ${ var_tags :- } "
local STEP = 1
2025-12-09 16:01:31 +01:00
local MAX_STEP = 28
2025-12-04 07:52:18 +01:00
2025-12-09 16:01:31 +01:00
# Store values for back navigation - inherit from var_* app defaults
local _ct_type = " ${ var_unprivileged :- 1 } "
2025-12-04 07:52:18 +01:00
local _pw = ""
local _pw_display = "Automatic Login"
local _ct_id = " $NEXTID "
local _hostname = " $NSAPP "
2025-12-09 16:01:31 +01:00
local _disk_size = " ${ var_disk :- 4 } "
local _core_count = " ${ var_cpu :- 1 } "
local _ram_size = " ${ var_ram :- 1024 } "
local _bridge = " ${ var_brg :- vmbr0 } "
local _net = " ${ var_net :- dhcp } "
local _gate = " ${ var_gateway :- } "
local _ipv6_method = " ${ var_ipv6_method :- auto } "
2025-12-04 07:52:18 +01:00
local _ipv6_addr = ""
local _ipv6_gate = ""
2025-12-09 16:01:31 +01:00
local _apt_cacher = " ${ var_apt_cacher :- no } "
local _apt_cacher_ip = " ${ var_apt_cacher_ip :- } "
local _mtu = " ${ var_mtu :- } "
local _sd = " ${ var_searchdomain :- } "
local _ns = " ${ var_ns :- } "
local _mac = " ${ var_mac :- } "
local _vlan = " ${ var_vlan :- } "
2025-12-04 07:52:18 +01:00
local _tags = " $TAGS "
2025-12-09 16:01:31 +01:00
local _enable_fuse = " ${ var_fuse :- no } "
local _enable_tun = " ${ var_tun :- no } "
2025-12-08 13:52:30 +01:00
local _enable_gpu = " ${ var_gpu :- no } "
2025-12-09 16:01:31 +01:00
local _enable_nesting = " ${ var_nesting :- 1 } "
local _verbose = " ${ var_verbose :- no } "
local _enable_keyctl = " ${ var_keyctl :- 0 } "
local _enable_mknod = " ${ var_mknod :- 0 } "
local _mount_fs = " ${ var_mount_fs :- } "
local _protect_ct = " ${ var_protection :- no } "
2025-12-11 10:52:11 +01:00
2025-12-09 16:01:31 +01:00
# Detect host timezone for default (if not set via var_timezone)
local _host_timezone = ""
if command -v timedatectl >/dev/null 2>& 1; then
_host_timezone = $( timedatectl show --value --property= Timezone 2>/dev/null || echo "" )
elif [ -f /etc/timezone ] ; then
_host_timezone = $( cat /etc/timezone 2>/dev/null || echo "" )
fi
2025-12-16 11:28:12 +01:00
# Map Etc/* timezones to "host" (pct doesn't accept Etc/* zones)
[[ " ${ _host_timezone :- } " == Etc/* ]] && _host_timezone = "host"
2025-12-09 16:01:31 +01:00
local _ct_timezone = " ${ var_timezone :- $_host_timezone } "
2025-12-16 11:28:12 +01:00
[[ " ${ _ct_timezone :- } " == Etc/* ]] && _ct_timezone = "host"
2025-12-04 07:52:18 +01:00
# Helper to show current progress
show_progress() {
local current = $1
local total = $MAX_STEP
echo -e "\n ${ INFO }${ BOLD }${ DGN } Step $current of $total ${ CL } "
}
# Detect available bridges (do this once)
local BRIDGES = ""
local BRIDGE_MENU_OPTIONS =()
_detect_bridges() {
IFACE_FILEPATH_LIST = "/etc/network/interfaces" $'\n' $( find "/etc/network/interfaces.d/" -type f 2>/dev/null)
BRIDGES = ""
local OLD_IFS = $IFS
IFS = $'\n'
for iface_filepath in ${ IFACE_FILEPATH_LIST } ; do
local iface_indexes_tmpfile = $( mktemp -q -u '.iface-XXXX' )
( grep -Pn '^\s*iface' " ${ iface_filepath } " 2>/dev/null | cut -d':' -f1 && wc -l " ${ iface_filepath } " 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >" ${ iface_indexes_tmpfile } " 2>/dev/null || true
if [ -f " ${ iface_indexes_tmpfile } " ] ; then
while read -r pair; do
local start = $( echo " ${ pair } " | cut -d':' -f1)
local end = $( echo " ${ pair } " | cut -d':' -f2)
if awk "NR >= ${ start } && NR <= ${ end } " " ${ iface_filepath } " 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b' ; then
local 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)
# Build bridge menu
BRIDGE_MENU_OPTIONS =()
if [[ -n " $BRIDGES " ]] ; then
while IFS = read -r bridge; do
if [[ -n " $bridge " ]] ; then
local description = $( grep -A 10 "iface $bridge " /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//' )
BRIDGE_MENU_OPTIONS +=( " $bridge " " ${ description :- } " )
fi
done <<< " $BRIDGES "
2025-05-15 10:31:37 +02:00
fi
2025-12-04 07:52:18 +01:00
}
_detect_bridges
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# Main wizard loop
while [ $STEP -le $MAX_STEP ] ; do
case $STEP in
2025-06-30 12:11:44 +02:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 1: Container Type
# ═══════════════════════════════════════════════════════════════════════════
1)
local default_on = "ON"
local default_off = "OFF"
[[ " $_ct_type " == "0" ]] && {
default_on = "OFF"
default_off = "ON"
}
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CONTAINER TYPE" \
--ok-button "Next" --cancel-button "Exit" \
--radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \
"1" "Unprivileged (recommended)" $default_on \
"0" "Privileged" $default_off \
3>& 1 1>& 2 2>& 3) ; then
[[ -n " $result " ]] && _ct_type = " $result "
(( STEP++))
else
exit_script
fi
2025-06-30 12:11:44 +02:00
;;
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 2: Root Password
# ═══════════════════════════════════════════════════════════════════════════
2)
if PW1 = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "ROOT PASSWORD" \
--ok-button "Next" --cancel-button "Back" \
--passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \
3>& 1 1>& 2 2>& 3) ; then
if [[ -z " $PW1 " ]] ; then
_pw = ""
_pw_display = "Automatic Login"
(( STEP++))
elif [[ " $PW1 " == *" " * ]] ; then
whiptail --msgbox "Password cannot contain spaces." 8 58
elif (( ${# PW1 } < 5)) ; then
whiptail --msgbox "Password must be at least 5 characters." 8 58
2025-06-30 12:11:44 +02:00
else
2025-12-04 07:52:18 +01:00
# Verify password
if PW2 = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "PASSWORD VERIFICATION" \
--ok-button "Confirm" --cancel-button "Back" \
--passwordbox "\nVerify Root Password" 10 58 \
3>& 1 1>& 2 2>& 3) ; then
if [[ " $PW1 " == " $PW2 " ]] ; then
_pw = "-password $PW1 "
_pw_display = "********"
(( STEP++))
else
whiptail --msgbox "Passwords do not match. Please try again." 8 58
fi
else
(( STEP--))
fi
2025-06-30 12:11:44 +02:00
fi
2025-12-04 07:52:18 +01:00
else
(( STEP--))
fi
2025-06-30 12:11:44 +02:00
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 3: Container ID
# ═══════════════════════════════════════════════════════════════════════════
3)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CONTAINER ID" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Container ID" 10 58 " $_ct_id " \
3>& 1 1>& 2 2>& 3) ; then
_ct_id = " ${ result :- $NEXTID } "
(( STEP++))
else
(( STEP--))
fi
2025-06-30 12:11:44 +02:00
;;
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 4: Hostname
# ═══════════════════════════════════════════════════════════════════════════
4)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "HOSTNAME" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 " $_hostname " \
3>& 1 1>& 2 2>& 3) ; then
local hn_test = " ${ result :- $NSAPP } "
hn_test = $( echo " ${ hn_test ,, } " | tr -d ' ' )
if [[ " $hn_test " = ~ ^[ a-z0-9]([ -a-z0-9] *[ a-z0-9]) ?$ ]] ; then
_hostname = " $hn_test "
(( STEP++))
2025-06-30 12:11:44 +02:00
else
2025-12-04 07:52:18 +01:00
whiptail --msgbox "Invalid hostname: ' $hn_test '\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58
2025-06-30 12:11:44 +02:00
fi
2025-12-04 07:52:18 +01:00
else
(( STEP--))
fi
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 5: Disk Size
# ═══════════════════════════════════════════════════════════════════════════
5)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "DISK SIZE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Disk Size in GB" 10 58 " $_disk_size " \
3>& 1 1>& 2 2>& 3) ; then
local disk_test = " ${ result :- $var_disk } "
if [[ " $disk_test " = ~ ^[ 1-9][ 0-9] *$ ]] ; then
_disk_size = " $disk_test "
(( STEP++))
2025-06-30 12:11:44 +02:00
else
2025-12-04 07:52:18 +01:00
whiptail --msgbox "Disk size must be a positive integer!" 8 58
fi
else
(( STEP--))
fi
;;
2025-06-30 12:11:44 +02:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 6: CPU Cores
# ═══════════════════════════════════════════════════════════════════════════
6)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CPU CORES" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nAllocate CPU Cores" 10 58 " $_core_count " \
3>& 1 1>& 2 2>& 3) ; then
local cpu_test = " ${ result :- $var_cpu } "
if [[ " $cpu_test " = ~ ^[ 1-9][ 0-9] *$ ]] ; then
_core_count = " $cpu_test "
(( STEP++))
else
whiptail --msgbox "CPU core count must be a positive integer!" 8 58
2025-06-30 12:11:44 +02:00
fi
2025-12-04 07:52:18 +01:00
else
(( STEP--))
fi
2025-06-30 12:11:44 +02:00
;;
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 7: RAM Size
# ═══════════════════════════════════════════════════════════════════════════
7)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "RAM SIZE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nAllocate RAM in MiB" 10 58 " $_ram_size " \
3>& 1 1>& 2 2>& 3) ; then
local ram_test = " ${ result :- $var_ram } "
if [[ " $ram_test " = ~ ^[ 1-9][ 0-9] *$ ]] ; then
_ram_size = " $ram_test "
(( STEP++))
else
whiptail --msgbox "RAM size must be a positive integer!" 8 58
fi
else
(( STEP--))
fi
2025-11-21 08:53:46 +01:00
;;
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 8: Network Bridge
# ═══════════════════════════════════════════════════════════════════════════
8)
if [[ ${# BRIDGE_MENU_OPTIONS [@] } -eq 0 ]] ; then
_bridge = "vmbr0"
(( STEP++))
else
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "NETWORK BRIDGE" \
--ok-button "Next" --cancel-button "Back" \
--menu "\nSelect network bridge:" 16 58 6 \
" ${ BRIDGE_MENU_OPTIONS [@] } " \
3>& 1 1>& 2 2>& 3) ; then
_bridge = " ${ result :- vmbr0 } "
(( STEP++))
else
(( STEP--))
fi
fi
2025-06-30 12:11:44 +02:00
;;
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 9: IPv4 Configuration
# ═══════════════════════════════════════════════════════════════════════════
9)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "IPv4 CONFIGURATION" \
--ok-button "Next" --cancel-button "Back" \
2025-12-16 16:52:37 +01:00
--menu "\nSelect IPv4 Address Assignment:" 16 65 3 \
2025-12-04 07:52:18 +01:00
"dhcp" "Automatic (DHCP, recommended)" \
"static" "Static (manual entry)" \
2025-12-16 16:52:37 +01:00
"range" "IP Range Scan (find first free IP)" \
2025-12-04 07:52:18 +01:00
3>& 1 1>& 2 2>& 3) ; then
if [[ " $result " == "static" ]] ; then
# Get static IP
local static_ip
if static_ip = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "STATIC IPv4 ADDRESS" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
if [[ " $static_ip " = ~ ^([ 0-9]{ 1,3} \. ){ 3}[ 0-9]{ 1,3} /([ 0-9] | [ 1-2][ 0-9] | 3[ 0-2]) $ ]] ; then
# Get gateway
local gateway_ip
if gateway_ip = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "GATEWAY IP" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nEnter Gateway IP address" 10 58 "" \
3>& 1 1>& 2 2>& 3) ; then
if [[ " $gateway_ip " = ~ ^([ 0-9]{ 1,3} \. ){ 3}[ 0-9]{ 1,3} $ ]] ; then
_net = " $static_ip "
_gate = ",gw= $gateway_ip "
(( STEP++))
else
whiptail --msgbox "Invalid Gateway IP format." 8 58
fi
fi
else
whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58
fi
fi
2025-12-16 16:52:37 +01:00
elif [[ " $result " == "range" ]] ; then
# IP Range Scan
local ip_range
if ip_range = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "IP RANGE SCAN" \
--ok-button "Scan" --cancel-button "Back" \
--inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \
3>& 1 1>& 2 2>& 3) ; then
if is_ip_range " $ip_range " ; then
# Exit whiptail screen temporarily to show scan progress
clear
header_info
echo -e " ${ INFO }${ BOLD }${ DGN } Scanning IP range for free address... ${ CL } \n"
if resolve_ip_from_range " $ip_range " ; then
# Get gateway
local gateway_ip
if gateway_ip = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "GATEWAY IP" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nFound free IP: $NET_RESOLVED \n\nEnter Gateway IP address" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
if [[ " $gateway_ip " = ~ ^([ 0-9]{ 1,3} \. ){ 3}[ 0-9]{ 1,3} $ ]] ; then
_net = " $NET_RESOLVED "
_gate = ",gw= $gateway_ip "
(( STEP++))
else
whiptail --msgbox "Invalid Gateway IP format." 8 58
fi
fi
else
whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58
fi
else
whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58
fi
fi
2025-12-04 07:52:18 +01:00
else
_net = "dhcp"
_gate = ""
(( STEP++))
fi
else
(( STEP--))
fi
2025-06-30 12:11:44 +02:00
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 10: IPv6 Configuration
# ═══════════════════════════════════════════════════════════════════════════
10)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "IPv6 CONFIGURATION" \
--ok-button "Next" --cancel-button "Back" \
--menu "\nSelect IPv6 Address Management:" 16 70 5 \
"auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \
"dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \
"static" "Static - Manual IPv6 address configuration" \
"none" "None - No IPv6 assignment (most containers)" \
"disable" "Fully Disabled - (breaks some services)" \
3>& 1 1>& 2 2>& 3) ; then
2024-01-12 15:00:42 -05:00
2025-12-04 07:52:18 +01:00
_ipv6_method = " $result "
case " $result " in
static)
local ipv6_addr
if ipv6_addr = $( whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "STATIC IPv6 ADDRESS" \
--inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
if [[ " $ipv6_addr " = ~ ^([ 0-9a-fA-F:] +:+) +[ 0-9a-fA-F] +( /[ 0-9]{ 1,3}) $ ]] ; then
_ipv6_addr = " $ipv6_addr "
# Optional gateway
_ipv6_gate = $( whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "IPv6 GATEWAY" \
--inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \
3>& 1 1>& 2 2>& 3) || true
(( STEP++))
else
whiptail --msgbox "Invalid IPv6 CIDR format." 8 58
fi
fi
;;
dhcp)
_ipv6_addr = "dhcp"
_ipv6_gate = ""
(( STEP++))
;;
none)
_ipv6_addr = "none"
_ipv6_gate = ""
(( STEP++))
;;
*)
_ipv6_addr = ""
_ipv6_gate = ""
(( STEP++))
;;
esac
else
(( STEP--))
fi
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 11: MTU Size
# ═══════════════════════════════════════════════════════════════════════════
11)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "MTU SIZE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
_mtu = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 12: DNS Search Domain
# ═══════════════════════════════════════════════════════════════════════════
12)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "DNS SEARCH DOMAIN" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
_sd = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 13: DNS Server
# ═══════════════════════════════════════════════════════════════════════════
13)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "DNS SERVER" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
_ns = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 14: MAC Address
# ═══════════════════════════════════════════════════════════════════════════
14)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "MAC ADDRESS" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
_mac = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 15: VLAN Tag
# ═══════════════════════════════════════════════════════════════════════════
15)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "VLAN TAG" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \
3>& 1 1>& 2 2>& 3) ; then
_vlan = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
2025-01-10 13:17:44 +01:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 16: Tags
# ═══════════════════════════════════════════════════════════════════════════
16)
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CONTAINER TAGS" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 " $_tags " \
3>& 1 1>& 2 2>& 3) ; then
_tags = " ${ result :- ; } "
_tags = $( echo " $_tags " | tr -d '[:space:]' )
(( STEP++))
else
(( STEP--))
fi
;;
2025-05-16 09:49:16 +02:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# STEP 17: SSH Settings
# ═══════════════════════════════════════════════════════════════════════════
17)
2025-12-08 14:20:05 +01:00
configure_ssh_settings "Step $STEP / $MAX_STEP "
2025-12-04 07:52:18 +01:00
# configure_ssh_settings handles its own flow, always advance
(( STEP++))
;;
2025-05-16 09:49:16 +02:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2025-12-09 16:01:31 +01:00
# STEP 18: FUSE Support
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
18)
2025-12-09 16:01:31 +01:00
local fuse_default_flag = "--defaultno"
[[ " $_enable_fuse " == "yes" || " $_enable_fuse " == "1" ]] && fuse_default_flag = ""
2025-12-04 07:52:18 +01:00
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "FUSE SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
2025-12-09 16:01:31 +01:00
$fuse_default_flag \
--yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${ var_fuse :- no } )" 14 58; then
2025-12-04 07:52:18 +01:00
_enable_fuse = "yes"
else
if [ $? -eq 1 ] ; then
_enable_fuse = "no"
else
(( STEP--))
continue
fi
fi
2025-12-09 16:01:31 +01:00
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 19: TUN/TAP Support
# ═══════════════════════════════════════════════════════════════════════════
19)
local tun_default_flag = "--defaultno"
[[ " $_enable_tun " == "yes" || " $_enable_tun " == "1" ]] && tun_default_flag = ""
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
2025-12-09 16:01:31 +01:00
--title "TUN/TAP SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$tun_default_flag \
--yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${ var_tun :- no } )" 14 62; then
_enable_tun = "yes"
2025-12-04 07:52:18 +01:00
else
2025-12-09 16:01:31 +01:00
if [ $? -eq 1 ] ; then
_enable_tun = "no"
else
(( STEP--))
continue
fi
2025-12-04 07:52:18 +01:00
fi
(( STEP++))
;;
2025-01-13 16:04:59 +01:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2025-12-09 16:01:31 +01:00
# STEP 20: Nesting Support
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2025-12-09 16:01:31 +01:00
20)
local nesting_default_flag = ""
[[ " $_enable_nesting " == "0" || " $_enable_nesting " == "no" ]] && nesting_default_flag = "--defaultno"
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "NESTING SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$nesting_default_flag \
--yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${ var_nesting :- 1 } )" 14 58; then
_enable_nesting = "1"
else
if [ $? -eq 1 ] ; then
_enable_nesting = "0"
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 21: GPU Passthrough
# ═══════════════════════════════════════════════════════════════════════════
21)
local gpu_default_flag = "--defaultno"
[[ " $_enable_gpu " == "yes" ]] && gpu_default_flag = ""
2025-12-08 13:52:30 +01:00
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "GPU PASSTHROUGH" \
--ok-button "Next" --cancel-button "Back" \
2025-12-09 16:01:31 +01:00
$gpu_default_flag \
--yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${ var_gpu :- no } )" 16 62; then
2025-12-08 13:52:30 +01:00
_enable_gpu = "yes"
else
if [ $? -eq 1 ] ; then
_enable_gpu = "no"
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
2025-12-09 16:01:31 +01:00
# STEP 22: Keyctl Support (Docker/systemd)
2025-12-08 13:52:30 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2025-12-09 16:01:31 +01:00
22)
local keyctl_default_flag = "--defaultno"
[[ " $_enable_keyctl " == "1" ]] && keyctl_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "KEYCTL SUPPORT" \
--ok-button "Next" --cancel-button "Back" \
$keyctl_default_flag \
--yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${ var_keyctl :- 0 } )" 16 62; then
_enable_keyctl = "1"
else
if [ $? -eq 1 ] ; then
_enable_keyctl = "0"
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 23: APT Cacher Proxy
# ═══════════════════════════════════════════════════════════════════════════
23)
local apt_cacher_default_flag = "--defaultno"
[[ " $_apt_cacher " == "yes" ]] && apt_cacher_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "APT CACHER PROXY" \
--ok-button "Next" --cancel-button "Back" \
$apt_cacher_default_flag \
--yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${ var_apt_cacher :- no } )" 14 62; then
_apt_cacher = "yes"
# Ask for IP if enabled
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "APT CACHER IP" \
--inputbox "\nEnter APT Cacher-NG server IP address:" 10 58 " $_apt_cacher_ip " \
3>& 1 1>& 2 2>& 3) ; then
_apt_cacher_ip = " $result "
fi
else
if [ $? -eq 1 ] ; then
_apt_cacher = "no"
_apt_cacher_ip = ""
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 24: Container Timezone
# ═══════════════════════════════════════════════════════════════════════════
24)
local tz_hint = " $_ct_timezone "
[[ -z " $tz_hint " ]] && tz_hint = "(empty - will use host timezone)"
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CONTAINER TIMEZONE" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${ _host_timezone :- unknown } \n\nLeave empty to inherit from host." 16 62 " $_ct_timezone " \
3>& 1 1>& 2 2>& 3) ; then
_ct_timezone = " $result "
2025-12-16 11:28:12 +01:00
[[ " ${ _ct_timezone :- } " == Etc/* ]] && _ct_timezone = "host" # pct doesn't accept Etc/* zones
2025-12-09 16:01:31 +01:00
(( STEP++))
else
(( STEP--))
fi
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 25: Container Protection
# ═══════════════════════════════════════════════════════════════════════════
25)
local protect_default_flag = "--defaultno"
[[ " $_protect_ct " == "yes" || " $_protect_ct " == "1" ]] && protect_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CONTAINER PROTECTION" \
--ok-button "Next" --cancel-button "Back" \
$protect_default_flag \
--yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${ var_protection :- no } )" 14 62; then
_protect_ct = "yes"
else
if [ $? -eq 1 ] ; then
_protect_ct = "no"
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 26: Device Node Creation (mknod)
# ═══════════════════════════════════════════════════════════════════════════
26)
local mknod_default_flag = "--defaultno"
[[ " $_enable_mknod " == "1" ]] && mknod_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "DEVICE NODE CREATION" \
--ok-button "Next" --cancel-button "Back" \
$mknod_default_flag \
--yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${ var_mknod :- 0 } )" 14 62; then
_enable_mknod = "1"
else
if [ $? -eq 1 ] ; then
_enable_mknod = "0"
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 27: Mount Filesystems
# ═══════════════════════════════════════════════════════════════════════════
27)
local mount_hint = ""
[[ -n " $_mount_fs " ]] && mount_hint = " $_mount_fs " || mount_hint = "(none)"
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "MOUNT FILESYSTEMS" \
--ok-button "Next" --cancel-button "Back" \
--inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint " 14 62 " $_mount_fs " \
3>& 1 1>& 2 2>& 3) ; then
_mount_fs = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 28: Verbose Mode & Confirmation
# ═══════════════════════════════════════════════════════════════════════════
28)
local verbose_default_flag = "--defaultno"
[[ " $_verbose " == "yes" ]] && verbose_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "VERBOSE MODE" \
$verbose_default_flag \
--yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then
_verbose = "yes"
else
_verbose = "no"
fi
2025-12-04 07:52:18 +01:00
# Build summary
local ct_type_desc = "Unprivileged"
[[ " $_ct_type " == "0" ]] && ct_type_desc = "Privileged"
2025-12-09 16:01:31 +01:00
local nesting_desc = "Disabled"
[[ " $_enable_nesting " == "1" ]] && nesting_desc = "Enabled"
local keyctl_desc = "Disabled"
[[ " $_enable_keyctl " == "1" ]] && keyctl_desc = "Enabled"
local protect_desc = "No"
[[ " $_protect_ct " == "yes" || " $_protect_ct " == "1" ]] && protect_desc = "Yes"
local tz_display = " ${ _ct_timezone :- Host TZ } "
local apt_display = " ${ _apt_cacher :- no } "
[[ " $_apt_cacher " == "yes" && -n " $_apt_cacher_ip " ]] && apt_display = " $_apt_cacher_ip "
2025-12-04 07:52:18 +01:00
local summary = "Container Type: $ct_type_desc
Container ID: $_ct_id
Hostname: $_hostname
Resources:
Disk: ${ _disk_size } GB
CPU: $_core_count cores
RAM: $_ram_size MiB
Network:
Bridge: $_bridge
IPv4: $_net
IPv6: $_ipv6_method
2025-12-09 16:01:31 +01:00
Features:
FUSE: $_enable_fuse | TUN: $_enable_tun
Nesting: $nesting_desc | Keyctl: $keyctl_desc
GPU: $_enable_gpu | Protection: $protect_desc
Advanced:
Timezone: $tz_display
APT Cacher: $apt_display
2025-12-04 07:52:18 +01:00
Verbose: $_verbose "
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "CONFIRM SETTINGS" \
--ok-button "Create LXC" --cancel-button "Back" \
2025-12-09 16:01:31 +01:00
--yesno " $summary \n\nCreate ${ APP } LXC with these settings?" 32 62; then
2025-12-04 07:52:18 +01:00
(( STEP++))
else
(( STEP--))
fi
;;
esac
done
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ═══════════════════════════════════════════════════════════════════════════
# Apply all collected values to global variables
# ═══════════════════════════════════════════════════════════════════════════
CT_TYPE = " $_ct_type "
PW = " $_pw "
CT_ID = " $_ct_id "
HN = " $_hostname "
DISK_SIZE = " $_disk_size "
CORE_COUNT = " $_core_count "
RAM_SIZE = " $_ram_size "
BRG = " $_bridge "
NET = " $_net "
GATE = " $_gate "
IPV6_METHOD = " $_ipv6_method "
IPV6_ADDR = " $_ipv6_addr "
IPV6_GATE = " $_ipv6_gate "
TAGS = " $_tags "
ENABLE_FUSE = " $_enable_fuse "
2025-12-09 16:01:31 +01:00
ENABLE_TUN = " $_enable_tun "
2025-12-08 13:52:30 +01:00
ENABLE_GPU = " $_enable_gpu "
2025-12-09 16:01:31 +01:00
ENABLE_NESTING = " $_enable_nesting "
ENABLE_KEYCTL = " $_enable_keyctl "
ENABLE_MKNOD = " $_enable_mknod "
ALLOW_MOUNT_FS = " $_mount_fs "
PROTECT_CT = " $_protect_ct "
CT_TIMEZONE = " $_ct_timezone "
APT_CACHER = " $_apt_cacher "
APT_CACHER_IP = " $_apt_cacher_ip "
2025-12-04 07:52:18 +01:00
VERBOSE = " $_verbose "
2025-12-09 16:01:31 +01:00
# Update var_* based on user choice (for functions that check these)
2025-12-08 13:52:30 +01:00
var_gpu = " $_enable_gpu "
2025-12-09 16:01:31 +01:00
var_fuse = " $_enable_fuse "
var_tun = " $_enable_tun "
var_nesting = " $_enable_nesting "
var_keyctl = " $_enable_keyctl "
var_mknod = " $_enable_mknod "
var_mount_fs = " $_mount_fs "
var_protection = " $_protect_ct "
var_timezone = " $_ct_timezone "
var_apt_cacher = " $_apt_cacher "
var_apt_cacher_ip = " $_apt_cacher_ip "
2025-12-08 13:52:30 +01:00
2025-12-04 07:52:18 +01:00
# Format optional values
[[ -n " $_mtu " ]] && MTU = ",mtu= $_mtu " || MTU = ""
[[ -n " $_sd " ]] && SD = "-searchdomain= $_sd " || SD = ""
[[ -n " $_ns " ]] && NS = "-nameserver= $_ns " || NS = ""
[[ -n " $_mac " ]] && MAC = ",hwaddr= $_mac " || MAC = ""
[[ -n " $_vlan " ]] && VLAN = ",tag= $_vlan " || VLAN = ""
# Alpine UDHCPC fix
if [ " $var_os " == "alpine" ] && [ " $NET " == "dhcp" ] && [ -n " $_ns " ] ; then
UDHCPC_FIX = "yes"
2023-03-22 20:48:20 -04:00
else
2025-12-04 07:52:18 +01:00
UDHCPC_FIX = "no"
2023-03-22 20:48:20 -04:00
fi
2025-12-04 07:52:18 +01:00
export UDHCPC_FIX
export SSH_KEYS_FILE
2025-12-08 13:52:30 +01:00
# Exit alternate screen buffer before showing summary (so output remains visible)
tput rmcup 2>/dev/null || true
trap - RETURN
2025-12-04 07:52:18 +01:00
# Display final summary
echo -e "\n ${ INFO }${ BOLD }${ DGN } PVE Version ${ PVEVERSION } (Kernel: ${ KERNEL_VERSION } ) ${ CL } "
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 " == "1" ] && echo "Unprivileged" || echo "Privileged" ) ${ CL } "
echo -e " ${ CONTAINERID }${ BOLD }${ DGN } Container ID: ${ BGN } $CT_ID ${ CL } "
echo -e " ${ HOSTNAME }${ BOLD }${ DGN } Hostname: ${ BGN } $HN ${ 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 } "
echo -e " ${ BRIDGE }${ BOLD }${ DGN } Bridge: ${ BGN } $BRG ${ CL } "
echo -e " ${ NETWORK }${ BOLD }${ DGN } IPv4: ${ BGN } $NET ${ CL } "
echo -e " ${ NETWORK }${ BOLD }${ DGN } IPv6: ${ BGN } $IPV6_METHOD ${ CL } "
2025-12-15 13:02:28 +01:00
echo -e " ${ FUSE }${ BOLD }${ DGN } FUSE Support: ${ BGN }${ ENABLE_FUSE :- no }${ CL } "
[[ " ${ ENABLE_TUN :- no } " == "yes" ]] && echo -e " ${ NETWORK }${ BOLD }${ DGN } TUN/TAP Support: ${ BGN } $ENABLE_TUN ${ CL } "
echo -e " ${ CONTAINERTYPE }${ BOLD }${ DGN } Nesting: ${ BGN } $( [ " ${ ENABLE_NESTING :- 1 } " == "1" ] && echo "Enabled" || echo "Disabled" ) ${ CL } "
[[ " ${ ENABLE_KEYCTL :- 0 } " == "1" ]] && echo -e " ${ CONTAINERTYPE }${ BOLD }${ DGN } Keyctl: ${ BGN } Enabled ${ CL } "
echo -e " ${ GPU }${ BOLD }${ DGN } GPU Passthrough: ${ BGN }${ ENABLE_GPU :- no }${ CL } "
[[ " ${ PROTECT_CT :- no } " == "yes" || " ${ PROTECT_CT :- no } " == "1" ]] && echo -e " ${ CONTAINERTYPE }${ BOLD }${ DGN } Protection: ${ BGN } Enabled ${ CL } "
[[ -n " ${ CT_TIMEZONE :- } " ]] && echo -e " ${ INFO }${ BOLD }${ DGN } Timezone: ${ BGN } $CT_TIMEZONE ${ CL } "
2025-12-09 16:01:31 +01:00
[[ " $APT_CACHER " == "yes" ]] && echo -e " ${ INFO }${ BOLD }${ DGN } APT Cacher: ${ BGN } $APT_CACHER_IP ${ CL } "
2025-12-04 07:52:18 +01:00
echo -e " ${ SEARCH }${ BOLD }${ DGN } Verbose Mode: ${ BGN } $VERBOSE ${ CL } "
echo -e " ${ CREATING }${ BOLD }${ RD } Creating a ${ APP } LXC using the above advanced settings ${ CL } "
2023-03-22 20:48:20 -04:00
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 7: USER INTERFACE & DIAGNOSTICS
# ==============================================================================
# ------------------------------------------------------------------------------
# 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
# - Creates file if missing with default DIAGNOSTICS=yes
# - Reads current diagnostics setting from file
# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
# ------------------------------------------------------------------------------
2025-02-24 11:18:38 +01:00
diagnostics_check() {
2025-01-29 14:26:23 +01:00
if ! [ -d "/usr/local/community-scripts" ] ; then
2025-02-24 11:18:38 +01:00
mkdir -p /usr/local/community-scripts
fi
2025-01-29 14:26:23 +01:00
2025-02-24 11:18:38 +01:00
if ! [ -f "/usr/local/community-scripts/diagnostics" ] ; then
if ( whiptail --backtitle "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
2025-01-29 14:26:23 +01:00
DIAGNOSTICS=yes
#This file is used to store the diagnostics settings for the Community-Scripts API.
#https://github.com/community-scripts/ProxmoxVE/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"
2025-02-03 10:05:41 +01:00
#"status"
2025-01-29 14:26:23 +01:00
#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/ProxmoxVE/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"
2025-02-03 10:05:41 +01:00
#"status"
2025-01-29 14:26:23 +01:00
#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
2025-12-04 07:52:18 +01:00
}
diagnostics_menu() {
if [ " ${ DIAGNOSTICS :- no } " = "yes" ] ; then
if whiptail --backtitle "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 "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
}
2025-01-29 14:26:23 +01:00
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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 " ${ INFO }${ BOLD }${ DGN } PVE Version ${ PVEVERSION } (Kernel: ${ KERNEL_VERSION } ) ${ CL } "
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 } "
2025-12-08 13:52:30 +01:00
if [[ -n " ${ var_gpu :- } " && " ${ var_gpu } " == "yes" ]] ; then
echo -e " ${ GPU }${ BOLD }${ DGN } GPU Passthrough: ${ BGN } Enabled ${ CL } "
fi
2025-12-04 07:52:18 +01:00
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 " "
2025-01-29 14:26:23 +01:00
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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
# ------------------------------------------------------------------------------
2023-03-22 20:48:20 -04:00
install_script() {
2024-02-21 18:26:12 -05:00
pve_check
2024-02-21 19:51:36 -05:00
shell_check
root_check
arch_check
ssh_check
2025-01-20 09:19:47 +01:00
maxkeys_check
2025-01-29 14:26:23 +01:00
diagnostics_check
2024-02-21 19:51:36 -05:00
2023-04-30 05:56:05 -04:00
if systemctl is-active -q ping-instances.service; then
2023-10-15 11:19:54 -04:00
systemctl -q stop ping-instances.service
2023-04-30 05:56:05 -04:00
fi
2025-12-04 07:52:18 +01:00
2023-03-22 20:48:20 -04:00
NEXTID = $( pvesh get /cluster/nextid)
2025-12-04 07:52:18 +01:00
# Get timezone using timedatectl (Debian 13+ compatible)
# Fallback to /etc/timezone for older systems
if command -v timedatectl >/dev/null 2>& 1; then
timezone = $( timedatectl show --value --property= Timezone 2>/dev/null || echo "UTC" )
elif [ -f /etc/timezone ] ; then
2025-11-22 17:56:50 +01:00
timezone = $( cat /etc/timezone)
else
2025-12-04 07:52:18 +01:00
timezone = "UTC"
2025-11-22 17:56:50 +01:00
fi
2025-12-16 11:28:12 +01:00
[[ " ${ timezone :- } " == Etc/* ]] && timezone = "host" # pct doesn't accept Etc/* zones
2025-12-04 07:52:18 +01:00
# Show APP Header
2023-03-22 20:48:20 -04:00
header_info
2025-01-29 14:26:23 +01:00
2025-12-04 07:52:18 +01:00
# --- Support CLI argument as direct preset (default, advanced, …) ---
CHOICE = " ${ mode :- ${ 1 :- }} "
# If no CLI argument → show whiptail menu
# Build menu dynamically based on available options
local appdefaults_option = ""
local settings_option = ""
local menu_items =(
"1" "Default Install"
"2" "Advanced Install"
"3" "User Defaults"
)
if [ -f " $( get_app_defaults_path) " ] ; then
appdefaults_option = "4"
menu_items +=( "4" "App Defaults for ${ APP } " )
settings_option = "5"
menu_items +=( "5" "Settings" )
else
settings_option = "4"
menu_items +=( "4" "Settings" )
fi
2024-12-16 12:41:51 +01:00
2025-12-04 07:52:18 +01:00
APPDEFAULTS_OPTION = " $appdefaults_option "
SETTINGS_OPTION = " $settings_option "
2025-07-15 13:38:25 +02:00
2025-12-04 07:52:18 +01:00
# Main menu loop - allows returning from Settings
while true; do
if [ -z " $CHOICE " ] ; then
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 ---
local defaults_target = ""
local run_maybe_offer = "no"
case " $CHOICE " in
1 | default | DEFAULT)
2025-01-29 14:26:23 +01:00
header_info
echo -e " ${ DEFAULT }${ BOLD }${ BL } Using Default Settings on node $PVEHOST_NAME ${ CL } "
2025-06-20 13:28:06 +02:00
VERBOSE = "no"
2025-01-29 14:26:23 +01:00
METHOD = "default"
2025-06-20 13:28:06 +02:00
base_settings " $VERBOSE "
2025-01-29 14:26:23 +01:00
echo_default
2025-12-04 07:52:18 +01:00
defaults_target = " $( ensure_global_default_vars_file) "
2025-01-29 14:26:23 +01:00
break
;;
2025-12-04 07:52:18 +01:00
2 | advanced | ADVANCED)
2025-01-29 14:26:23 +01:00
header_info
2025-12-04 07:52:18 +01:00
echo -e " ${ ADVANCED }${ BOLD }${ RD } Using Advanced Install on node $PVEHOST_NAME ${ CL } "
echo -e " ${ INFO }${ BOLD }${ DGN } PVE Version ${ PVEVERSION } (Kernel: ${ KERNEL_VERSION } ) ${ CL } "
2025-01-29 14:26:23 +01:00
METHOD = "advanced"
2025-02-13 15:03:18 +01:00
base_settings
2025-01-29 14:26:23 +01:00
advanced_settings
2025-12-04 07:52:18 +01:00
defaults_target = " $( ensure_global_default_vars_file) "
run_maybe_offer = "yes"
2025-01-29 14:26:23 +01:00
break
;;
2025-12-04 07:52:18 +01:00
3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS)
default_var_settings || {
msg_error "Failed to apply default.vars"
exit 1
}
defaults_target = "/usr/local/community-scripts/default.vars"
2025-04-10 21:06:27 +02:00
break
;;
2025-12-04 07:52:18 +01:00
" $APPDEFAULTS_OPTION " | 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"
2025-12-15 09:46:49 +01:00
load_vars_file " $( get_app_defaults_path) " "yes" # Force override script defaults
2025-12-15 09:37:01 +01:00
base_settings
2025-12-04 07:52:18 +01:00
echo_default
defaults_target = " $( get_app_defaults_path) "
break
2025-02-24 11:18:38 +01:00
else
2025-12-04 07:52:18 +01:00
msg_error "No App Defaults available for ${ APP } "
exit 1
2025-02-24 11:18:38 +01:00
fi
;;
2025-12-04 07:52:18 +01:00
" $SETTINGS_OPTION " | settings | SETTINGS)
settings_menu
# After settings menu, show main menu again
header_info
CHOICE = ""
2025-01-29 14:26:23 +01:00
;;
*)
2025-12-04 07:52:18 +01:00
echo -e " ${ CROSS }${ RD } Invalid option: $CHOICE ${ CL } "
exit 1
;;
esac
done
if [[ -n " $defaults_target " ]] ; then
ensure_storage_selection_for_vars_file " $defaults_target "
fi
if [[ " $run_maybe_offer " == "yes" ]] ; then
maybe_offer_save_app_defaults
fi
}
edit_default_storage() {
local vf = "/usr/local/community-scripts/default.vars"
# Ensure file exists
if [[ ! -f " $vf " ]] ; then
mkdir -p " $( dirname " $vf " ) "
touch " $vf "
fi
# Let ensure_storage_selection_for_vars_file handle everything
ensure_storage_selection_for_vars_file " $vf "
}
settings_menu() {
while true; do
local settings_items =(
"1" "Manage API-Diagnostic Setting"
"2" "Edit Default.vars"
)
if [ -f " $( get_app_defaults_path) " ] ; then
settings_items +=( "3" "Edit App.vars for ${ APP } " )
settings_items +=( "4" "Back to Main Menu" )
else
settings_items +=( "3" "Back to Main Menu" )
fi
local choice
choice = $( whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "Community-Scripts SETTINGS Menu" \
--ok-button "Select" --cancel-button "Exit Script" \
--menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \
" ${ settings_items [@] } " \
3>& 1 1>& 2 2>& 3) || exit_script
case " $choice " in
1) diagnostics_menu ;;
2) nano /usr/local/community-scripts/default.vars ;;
3)
if [ -f " $( get_app_defaults_path) " ] ; then
nano " $( get_app_defaults_path) "
else
# Back was selected (no app.vars available)
return
fi
;;
4)
# Back to main menu
return
2025-01-29 14:26:23 +01:00
;;
2024-12-16 12:41:51 +01:00
esac
done
2023-03-22 20:48:20 -04:00
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# check_container_resources()
#
# - Compares host RAM/CPU with required values
# - Warns if under-provisioned and asks user to continue or abort
# ------------------------------------------------------------------------------
2024-11-15 18:13:14 +01:00
check_container_resources() {
2024-12-07 22:42:40 +01:00
current_ram = $( free -m | awk 'NR==2{print $2}' )
2025-01-13 16:04:59 +01:00
current_cpu = $( nproc)
2024-11-15 18:13:14 +01:00
if [[ " $current_ram " -lt " $var_ram " ]] || [[ " $current_cpu " -lt " $var_cpu " ]] ; then
2024-12-16 12:41:51 +01:00
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 } "
2024-11-15 18:13:14 +01:00
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"
2025-12-04 07:52:18 +01:00
echo -ne " ${ INFO }${ HOLD } May cause data loss! ${ INFO } Continue update with under-provisioned LXC? <yes/No> "
2025-02-10 17:29:33 +08:00
read -r prompt
2025-12-04 07:52:18 +01:00
if [[ ! ${ prompt ,, } = ~ ^( yes) $ ]] ; then
2024-12-16 12:41:51 +01:00
echo -e " ${ CROSS }${ HOLD } ${ YWB } Exiting based on user input. ${ CL } "
exit 1
fi
2024-11-15 18:13:14 +01:00
else
echo -e ""
fi
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# check_container_storage()
#
# - Checks /boot partition usage
# - Warns if usage >80% and asks user confirmation before proceeding
# ------------------------------------------------------------------------------
2024-11-15 18:13:14 +01:00
check_container_storage() {
total_size = $( df /boot --output= size | tail -n 1)
local used_size = $( df /boot --output= used | tail -n 1)
2025-02-24 11:18:38 +01:00
usage = $(( 100 * used_size / total_size))
if (( usage > 80)) ; then
2024-12-16 12:41:51 +01:00
echo -e " ${ INFO }${ HOLD } ${ YWB } Warning: Storage is dangerously low ( ${ usage } %). ${ CL } "
2025-12-04 07:52:18 +01:00
echo -ne "Continue anyway? <y/N> "
2025-02-10 17:29:33 +08:00
read -r prompt
2025-12-04 07:52:18 +01:00
if [[ ! ${ prompt ,, } = ~ ^( y| yes) $ ]] ; then
2024-12-16 12:41:51 +01:00
echo -e " ${ CROSS }${ HOLD }${ YWB } Exiting based on user input. ${ CL } "
2024-11-15 18:13:14 +01:00
exit 1
fi
fi
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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}
# bare format: type base64 [comment]
/^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
# with options: find from first key-type onward
{
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 [@] } "
}
configure_ssh_settings() {
2025-12-08 14:20:05 +01:00
local step_info = " ${ 1 :- } "
local backtitle = "Proxmox VE Helper Scripts"
[[ -n " $step_info " ]] && backtitle = "Proxmox VE Helper Scripts [ ${ step_info } ]"
2025-12-04 07:52:18 +01:00
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 [@] } "
local default_key_count = " $COUNT "
local ssh_key_mode
if [[ " $default_key_count " -gt 0 ]] ; then
2025-12-08 14:20:05 +01:00
ssh_key_mode = $( whiptail --backtitle " $backtitle " --title "SSH KEY SOURCE" --menu \
2025-12-04 07:52:18 +01:00
"Provision SSH keys for root:" 14 72 4 \
"found" "Select from detected keys ( ${ default_key_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
2025-06-20 13:28:06 +02:00
else
2025-12-08 14:20:05 +01:00
ssh_key_mode = $( whiptail --backtitle " $backtitle " --title "SSH KEY SOURCE" --menu \
2025-12-04 07:52:18 +01:00
"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)
local selection
2025-12-08 14:20:05 +01:00
selection = $( whiptail --backtitle " $backtitle " --title "SELECT HOST KEYS" \
2025-12-04 07:52:18 +01:00
--checklist "Select one or more keys to import:" 20 140 10 " ${ CHOICES [@] } " 3>& 1 1>& 2 2>& 3) || exit_script
for tag in $selection ; do
tag = " ${ tag % \" } "
tag = " ${ tag # \" } "
local line
line = $( grep -E "^ ${ tag } \|" " $MAPFILE " | head -n1 | cut -d'|' -f2-)
[[ -n " $line " ]] && printf '%s\n' " $line " >>" $SSH_KEYS_FILE "
done
;;
manual)
2025-12-08 14:20:05 +01:00
SSH_AUTHORIZED_KEY = " $( whiptail --backtitle " $backtitle " \
2025-12-04 07:52:18 +01:00
--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)
local glob_path
2025-12-08 14:20:05 +01:00
glob_path = $( whiptail --backtitle " $backtitle " \
2025-12-04 07:52:18 +01:00
--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
local folder_selection
2025-12-08 14:20:05 +01:00
folder_selection = $( whiptail --backtitle " $backtitle " --title "SELECT FOLDER KEYS" \
2025-12-04 07:52:18 +01:00
--checklist "Select key(s) to import:" 20 78 10 " ${ CHOICES [@] } " 3>& 1 1>& 2 2>& 3) || exit_script
for tag in $folder_selection ; do
tag = " ${ tag % \" } "
tag = " ${ tag # \" } "
local line
line = $( grep -E "^ ${ tag } \|" " $MAPFILE " | head -n1 | cut -d'|' -f2-)
[[ -n " $line " ]] && printf '%s\n' " $line " >>" $SSH_KEYS_FILE "
done
else
2025-12-08 14:20:05 +01:00
whiptail --backtitle " $backtitle " --msgbox "No keys found in: $glob_path " 8 60
2025-12-04 07:52:18 +01:00
fi
else
2025-12-08 14:20:05 +01:00
whiptail --backtitle " $backtitle " --msgbox "Path/glob returned no files." 8 60
2025-12-04 07:52:18 +01:00
fi
fi
;;
none)
:
;;
esac
if [[ -s " $SSH_KEYS_FILE " ]] ; then
sort -u -o " $SSH_KEYS_FILE " " $SSH_KEYS_FILE "
printf '\n' >>" $SSH_KEYS_FILE "
fi
2025-12-08 14:20:05 +01:00
# Always show SSH access dialog - user should be able to enable SSH even without keys
if ( whiptail --backtitle " $backtitle " --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58) ; then
SSH = "yes"
2025-12-04 07:52:18 +01:00
else
SSH = "no"
fi
}
# ------------------------------------------------------------------------------
# start()
#
# - Entry point of script
# - On Proxmox host: calls install_script
2025-12-19 22:44:03 +01:00
# - In silent mode: runs update_script with automatic cleanup
# - Otherwise: shows update/setting menu and runs update_script with cleanup
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
start() {
source <( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/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
2025-12-19 22:44:03 +01:00
cleanup_lxc
2025-12-04 07:52:18 +01:00
else
CHOICE = $( whiptail --backtitle "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)
2025-02-24 11:18:38 +01:00
case " $CHOICE " in
1)
2025-06-20 13:28:06 +02:00
VERBOSE = "no"
2025-02-24 11:18:38 +01:00
set_std_mode
;;
2)
2025-06-20 13:28:06 +02:00
VERBOSE = "yes"
2025-02-24 11:18:38 +01:00
set_std_mode
;;
3)
2023-03-22 20:48:20 -04:00
clear
2024-12-16 12:41:51 +01:00
exit_script
2023-03-22 20:48:20 -04:00
exit
2025-02-24 11:18:38 +01:00
;;
esac
2023-03-22 20:48:20 -04:00
update_script
2025-12-19 22:44:03 +01:00
cleanup_lxc
2023-03-22 20:48:20 -04:00
fi
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 8: CONTAINER CREATION & DEPLOYMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# build_container()
#
# - Main function for creating and configuring LXC container
# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6)
# - Creates container via pct create with all specified settings
# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough
# - Starts container and waits for network connectivity
# - Installs base packages (curl, sudo, etc.)
# - Injects SSH keys if configured
# - Executes <app>-install.sh inside container
# - Posts installation telemetry to API if diagnostics enabled
# ------------------------------------------------------------------------------
2023-03-22 20:48:20 -04:00
build_container() {
2025-06-20 13:28:06 +02:00
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
2023-03-23 07:33:03 -04:00
2025-12-04 07:52:18 +01:00
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 (always required, default 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
2025-06-30 12:11:44 +02:00
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
2025-12-04 07:52:18 +01:00
2025-12-09 16:01:31 +01:00
# Build FEATURES string based on container type and user choices
FEATURES = ""
# Nesting support (user configurable, default enabled)
if [ " ${ ENABLE_NESTING :- 1 } " == "1" ] ; then
2023-12-06 20:13:12 -05:00
FEATURES = "nesting=1"
2023-03-23 07:33:03 -04:00
fi
2025-12-09 16:01:31 +01:00
# Keyctl for unprivileged containers (needed for Docker)
if [ " $CT_TYPE " == "1" ] ; then
[ -n " $FEATURES " ] && FEATURES = " $FEATURES ,"
FEATURES = " ${ FEATURES } keyctl=1"
fi
2025-05-22 14:32:27 +02:00
if [ " $ENABLE_FUSE " == "yes" ] ; then
2025-12-09 16:01:31 +01:00
[ -n " $FEATURES " ] && FEATURES = " $FEATURES ,"
FEATURES = " ${ FEATURES } fuse=1"
2025-05-22 14:32:27 +02:00
fi
2025-12-04 07:52:18 +01:00
# Build PCT_OPTIONS as string for export
2023-03-22 20:48:20 -04:00
TEMP_DIR = $( mktemp -d)
2025-04-09 15:30:54 +02:00
pushd " $TEMP_DIR " >/dev/null
2023-03-23 12:51:53 -04:00
if [ " $var_os " == "alpine" ] ; then
2025-04-01 10:25:46 +02:00
export FUNCTIONS_FILE_PATH = " $( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func) "
2023-03-23 12:51:53 -04:00
else
2025-04-01 10:25:46 +02:00
export FUNCTIONS_FILE_PATH = " $( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func) "
2024-01-12 12:09:32 -05:00
fi
2025-06-10 09:24:20 +02:00
2025-12-04 07:52:18 +01:00
# Core exports for install.func
2025-06-10 09:24:20 +02:00
export DIAGNOSTICS = " $DIAGNOSTICS "
2025-02-04 13:23:20 +01:00
export RANDOM_UUID = " $RANDOM_UUID "
2025-12-04 07:52:18 +01:00
export SESSION_ID = " $SESSION_ID "
2024-01-12 15:00:42 -05:00
export CACHER = " $APT_CACHER "
export CACHER_IP = " $APT_CACHER_IP "
2023-03-22 20:48:20 -04:00
export tz = " $timezone "
export APPLICATION = " $APP "
2023-08-17 09:26:44 -04:00
export app = " $NSAPP "
2023-05-15 05:53:35 -04:00
export PASSWORD = " $PW "
2025-06-20 13:28:06 +02:00
export VERBOSE = " $VERBOSE "
2023-03-22 20:48:20 -04:00
export SSH_ROOT = " ${ SSH } "
2025-01-15 15:01:24 +01:00
export SSH_AUTHORIZED_KEY
2023-03-22 20:48:20 -04:00
export CTID = " $CT_ID "
2023-05-14 11:43:17 -04:00
export CTTYPE = " $CT_TYPE "
2025-05-22 14:32:27 +02:00
export ENABLE_FUSE = " $ENABLE_FUSE "
export ENABLE_TUN = " $ENABLE_TUN "
2023-03-22 20:48:20 -04:00
export PCT_OSTYPE = " $var_os "
export PCT_OSVERSION = " $var_version "
export PCT_DISK_SIZE = " $DISK_SIZE "
2026-01-04 17:11:29 -05:00
export IPV6_METHOD = " $IPV6_METHOD "
2026-01-06 20:15:22 +01:00
export ENABLE_GPU = " $ENABLE_GPU "
2025-12-04 07:52:18 +01:00
# DEV_MODE exports (optional, for debugging)
export BUILD_LOG = " $BUILD_LOG "
export INSTALL_LOG = "/root/.install- ${ SESSION_ID } .log"
export dev_mode = " ${ dev_mode :- } "
export DEV_MODE_MOTD = " ${ DEV_MODE_MOTD :- false } "
export DEV_MODE_KEEP = " ${ DEV_MODE_KEEP :- false } "
export DEV_MODE_TRACE = " ${ DEV_MODE_TRACE :- false } "
export DEV_MODE_PAUSE = " ${ DEV_MODE_PAUSE :- false } "
export DEV_MODE_BREAKPOINT = " ${ DEV_MODE_BREAKPOINT :- false } "
export DEV_MODE_LOGS = " ${ DEV_MODE_LOGS :- false } "
export DEV_MODE_DRYRUN = " ${ DEV_MODE_DRYRUN :- false } "
# Build PCT_OPTIONS as multi-line string
2025-12-11 10:52:11 +01:00
PCT_OPTIONS_STRING = " -hostname $HN
2025-12-04 07:52:18 +01:00
-tags $TAGS "
2025-12-11 10:52:11 +01:00
# Only add -features if FEATURES is not empty
if [ -n " $FEATURES " ] ; then
PCT_OPTIONS_STRING = " -features $FEATURES
$PCT_OPTIONS_STRING "
fi
2025-12-04 07:52:18 +01:00
# Add storage if specified
if [ -n " $SD " ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
$SD "
fi
# Add nameserver if specified
if [ -n " $NS " ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
$NS "
fi
# Network configuration
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
$NET_STRING
-onboot 1
-cores $CORE_COUNT
-memory $RAM_SIZE
-unprivileged $CT_TYPE "
# Protection flag (if var_protection was set)
if [ " ${ PROTECT_CT :- } " == "1" ] || [ " ${ PROTECT_CT :- } " == "yes" ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
-protection 1"
fi
2025-12-16 11:28:12 +01:00
# Timezone (map Etc/* to "host" as pct doesn't accept them)
2025-12-04 07:52:18 +01:00
if [ -n " ${ CT_TIMEZONE :- } " ] ; then
2025-12-16 11:28:12 +01:00
local _pct_timezone = " $CT_TIMEZONE "
[[ " $_pct_timezone " == Etc/* ]] && _pct_timezone = "host"
2025-12-04 07:52:18 +01:00
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
2025-12-16 11:28:12 +01:00
-timezone $_pct_timezone "
2025-12-04 07:52:18 +01:00
fi
# Password (already formatted)
if [ -n " $PW " ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
$PW "
fi
# Export as string (this works, unlike arrays!)
export PCT_OPTIONS = " $PCT_OPTIONS_STRING "
export TEMPLATE_STORAGE = " ${ var_template_storage :- } "
export CONTAINER_STORAGE = " ${ var_container_storage :- } "
create_lxc_container || exit $?
2023-03-23 04:22:01 -04:00
2025-07-04 21:22:50 +02:00
LXC_CONFIG = "/etc/pve/lxc/ ${ CTID } .conf"
2025-12-04 07:52:18 +01:00
# ============================================================================
# GPU/USB PASSTHROUGH CONFIGURATION
# ============================================================================
2025-12-08 13:52:30 +01:00
# Check if GPU passthrough is enabled
# Returns true only if var_gpu is explicitly set to "yes"
# Can be set via:
# - Environment variable: var_gpu=yes bash -c "..."
# - CT script default: var_gpu="${var_gpu:-no}"
# - Advanced settings wizard
# - App defaults file: /usr/local/community-scripts/defaults/<app>.vars
2025-12-04 07:52:18 +01:00
is_gpu_app() {
2025-12-08 13:52:30 +01:00
[[ " ${ var_gpu :- no } " == "yes" ]] && return 0
2025-12-04 07:52:18 +01:00
return 1
}
# Detect all available GPU devices
detect_gpu_devices() {
INTEL_DEVICES =()
AMD_DEVICES =()
NVIDIA_DEVICES =()
# Store PCI info to avoid multiple calls
local pci_vga_info = $( lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" )
# Check for Intel GPU - look for Intel vendor ID [8086]
if echo " $pci_vga_info " | grep -q "\[8086:" ; then
msg_custom "🎮" " ${ BL } " "Detected Intel GPU"
if [[ -d /dev/dri ]] ; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e " $d " ]] && INTEL_DEVICES +=( " $d " )
done
fi
fi
# Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
if echo " $pci_vga_info " | grep -qE "\[1002:|\[1022:" ; then
msg_custom "🎮" " ${ RD } " "Detected AMD GPU"
if [[ -d /dev/dri ]] ; then
# Only add if not already claimed by Intel
if [[ ${# INTEL_DEVICES [@] } -eq 0 ]] ; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e " $d " ]] && AMD_DEVICES +=( " $d " )
done
fi
fi
fi
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
if echo " $pci_vga_info " | grep -q "\[10de:" ; then
msg_custom "🎮" " ${ GN } " "Detected NVIDIA GPU"
# Simple passthrough - just bind /dev/nvidia* devices if they exist
2025-12-05 12:44:23 +01:00
# Only include character devices (-c), skip directories like /dev/nvidia-caps
for d in /dev/nvidia*; do
[[ -c " $d " ]] && NVIDIA_DEVICES +=( " $d " )
2025-12-04 07:52:18 +01:00
done
2025-12-05 12:44:23 +01:00
# Also check for devices inside /dev/nvidia-caps/ directory
if [[ -d /dev/nvidia-caps ]] ; then
for d in /dev/nvidia-caps/*; do
[[ -c " $d " ]] && NVIDIA_DEVICES +=( " $d " )
done
fi
2025-12-04 07:52:18 +01:00
if [[ ${# NVIDIA_DEVICES [@] } -gt 0 ]] ; then
msg_custom "🎮" " ${ GN } " "Found ${# NVIDIA_DEVICES [@] } NVIDIA device(s) for passthrough"
else
msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found"
msg_custom "ℹ ️" " ${ YW } " "Skipping NVIDIA passthrough (host drivers may not be loaded)"
fi
fi
# Debug output
msg_debug "Intel devices: ${ INTEL_DEVICES [*] } "
msg_debug "AMD devices: ${ AMD_DEVICES [*] } "
msg_debug "NVIDIA devices: ${ NVIDIA_DEVICES [*] } "
}
# 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)"
2025-04-09 15:30:54 +02:00
cat <<EOF >>"$LXC_CONFIG"
2025-12-04 07:52:18 +01:00
# Automatic USB passthrough (privileged container)
2023-03-22 20:48:20 -04:00
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
2025-12-04 07:52:18 +01:00
msg_ok "USB passthrough configured"
}
# Configure GPU passthrough
configure_gpu_passthrough() {
2025-12-08 13:52:30 +01:00
# Skip if:
# GPU passthrough is enabled when var_gpu="yes":
# - Set via environment variable: var_gpu=yes bash -c "..."
# - Set in CT script: var_gpu="${var_gpu:-no}"
# - Enabled in advanced_settings wizard
# - Configured in app defaults file
if ! is_gpu_app " $APP " ; then
2025-12-04 07:52:18 +01:00
return 0
fi
2023-03-23 00:09:04 -04:00
2025-12-04 07:52:18 +01:00
detect_gpu_devices
2025-07-04 21:22:50 +02:00
2025-12-04 07:52:18 +01:00
# Count available GPU types
local gpu_count = 0
local available_gpus =()
2025-07-04 21:22:50 +02:00
2025-12-04 07:52:18 +01:00
if [[ ${# INTEL_DEVICES [@] } -gt 0 ]] ; then
available_gpus +=( "INTEL" )
gpu_count = $(( gpu_count + 1 ))
fi
2025-07-04 21:22:50 +02:00
2025-12-04 07:52:18 +01:00
if [[ ${# AMD_DEVICES [@] } -gt 0 ]] ; then
available_gpus +=( "AMD" )
gpu_count = $(( gpu_count + 1 ))
2025-07-04 21:22:50 +02:00
fi
2025-12-04 07:52:18 +01:00
if [[ ${# NVIDIA_DEVICES [@] } -gt 0 ]] ; then
available_gpus +=( "NVIDIA" )
gpu_count = $(( gpu_count + 1 ))
2025-07-09 16:10:13 +02:00
fi
2025-07-04 21:22:50 +02:00
2025-12-04 07:52:18 +01:00
if [[ $gpu_count -eq 0 ]] ; then
msg_custom "ℹ ️" " ${ YW } " "No GPU devices found for passthrough"
return 0
2025-07-09 16:10:13 +02:00
fi
2025-12-04 07:52:18 +01:00
local selected_gpu = ""
if [[ $gpu_count -eq 1 ]] ; then
# Automatic selection for single GPU
selected_gpu = " ${ available_gpus [0] } "
msg_ok "Automatically configuring ${ selected_gpu } GPU passthrough"
else
# Multiple GPUs - ask user
echo -e "\n ${ INFO } Multiple GPU types detected:"
for gpu in " ${ available_gpus [@] } " ; do
echo " - $gpu "
done
read -rp "Which GPU type to passthrough? ( ${ available_gpus [*] } ): " selected_gpu
selected_gpu = " ${ selected_gpu ^^ } "
# Validate selection
local valid = 0
for gpu in " ${ available_gpus [@] } " ; do
[[ " $selected_gpu " == " $gpu " ]] && valid = 1
done
if [[ $valid -eq 0 ]] ; then
msg_warn "Invalid selection. Skipping GPU passthrough."
return 0
fi
2024-04-25 14:36:47 -04:00
fi
2023-03-23 00:09:04 -04:00
2025-12-04 07:52:18 +01:00
# Apply passthrough configuration based on selection
local dev_idx = 0
case " $selected_gpu " in
INTEL | AMD)
local devices =()
[[ " $selected_gpu " == "INTEL" ]] && devices =( " ${ INTEL_DEVICES [@] } " )
[[ " $selected_gpu " == "AMD" ]] && devices =( " ${ AMD_DEVICES [@] } " )
# Use pct set to add devices with proper dev0/dev1 format
# GIDs will be detected and set after container starts
local dev_index = 0
for dev in " ${ devices [@] } " ; do
# Add to config using pct set (will be visible in GUI)
echo "dev ${ dev_index } : ${ dev } ,gid=44" >>" $LXC_CONFIG "
dev_index = $(( dev_index + 1 ))
done
export GPU_TYPE = " $selected_gpu "
msg_ok " ${ selected_gpu } GPU passthrough configured ( ${# devices [@] } devices)"
;;
NVIDIA)
if [[ ${# NVIDIA_DEVICES [@] } -eq 0 ]] ; then
msg_warn "No NVIDIA devices available for passthrough"
return 0
fi
# Use pct set for NVIDIA devices
local dev_index = 0
for dev in " ${ NVIDIA_DEVICES [@] } " ; do
echo "dev ${ dev_index } : ${ dev } ,gid=44" >>" $LXC_CONFIG "
dev_index = $(( dev_index + 1 ))
done
export GPU_TYPE = "NVIDIA"
msg_ok "NVIDIA GPU passthrough configured ( ${# NVIDIA_DEVICES [@] } devices) - install drivers in container if needed"
;;
esac
}
# Additional device passthrough
configure_additional_devices() {
# TUN device passthrough
if [ " $ENABLE_TUN " == "yes" ] ; then
cat <<EOF >>"$LXC_CONFIG"
2025-05-22 14:32:27 +02:00
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF
2025-12-04 07:52:18 +01:00
fi
# Coral TPU passthrough
if [[ -e /dev/apex_0 ]] ; then
msg_custom "🔌" " ${ BL } " "Detected Coral TPU - configuring passthrough"
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>" $LXC_CONFIG "
fi
}
# Execute pre-start configurations
configure_usb_passthrough
configure_gpu_passthrough
configure_additional_devices
# ============================================================================
# START CONTAINER AND INSTALL USERLAND
# ============================================================================
2025-05-22 14:32:27 +02:00
2025-06-22 20:25:01 +02:00
msg_info "Starting LXC Container"
pct start " $CTID "
2025-07-22 14:46:50 +02:00
2025-12-04 07:52:18 +01:00
# Wait for container to be running
2025-07-22 14:46:50 +02:00
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
2025-12-04 07:52:18 +01:00
# Wait for network (skip for Alpine initially)
2025-07-22 14:46:50 +02:00
if [ " $var_os " != "alpine" ] ; then
msg_info "Waiting for network in LXC container"
2025-12-04 07:52:18 +01:00
# Wait for IP assignment (IPv4 or IPv6)
local ip_in_lxc = ""
for i in { 1..20} ; do
# Try IPv4 first
ip_in_lxc = $( pct exec " $CTID " -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
# Fallback to IPv6 if IPv4 not available
if [ -z " $ip_in_lxc " ] ; then
ip_in_lxc = $( pct exec " $CTID " -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
2025-08-02 12:52:21 -03:00
fi
2025-12-04 07:52:18 +01:00
[ -n " $ip_in_lxc " ] && break
sleep 1
done
if [ -z " $ip_in_lxc " ] ; then
msg_error "No IP assigned to CT $CTID after 20s"
echo -e " ${ YW } Troubleshooting: ${ CL } "
echo " • Verify bridge ${ BRG } exists and has connectivity"
echo " • Check if DHCP server is reachable (if using DHCP)"
echo " • Verify static IP configuration (if using static IP)"
echo " • Check Proxmox firewall rules"
echo " • If using Tailscale: Disable MagicDNS temporarily"
exit 1
fi
# Verify basic connectivity (ping test)
local ping_success = false
for retry in { 1..3} ; do
if pct exec " $CTID " -- ping -c 1 -W 2 1.1.1.1 & >/dev/null ||
pct exec " $CTID " -- ping -c 1 -W 2 8.8.8.8 & >/dev/null ||
pct exec " $CTID " -- ping6 -c 1 -W 2 2606:4700:4700::1111 & >/dev/null; then
ping_success = true
2025-08-04 14:36:02 +02:00
break
2025-07-22 14:46:50 +02:00
fi
2025-12-04 07:52:18 +01:00
sleep 2
2025-07-22 14:46:50 +02:00
done
2025-12-04 07:52:18 +01:00
if [ " $ping_success " = false ] ; then
msg_warn "Network configured (IP: $ip_in_lxc ) but connectivity test failed"
echo -e " ${ YW } Container may have limited internet access. Installation will continue... ${ CL } "
else
msg_ok "Network in LXC is reachable (ping)"
fi
2025-07-22 14:46:50 +02:00
fi
2025-12-04 07:52:18 +01:00
# Function to get correct GID inside container
get_container_gid() {
local group = " $1 "
local gid = $( pct exec " $CTID " -- getent group " $group " 2>/dev/null | cut -d: -f3)
echo " ${ gid :- 44 } " # Default to 44 if not found
}
fix_gpu_gids
2025-06-22 20:53:02 +02:00
2025-12-04 07:52:18 +01:00
# Continue with standard container setup
2025-06-22 20:43:25 +02:00
msg_info "Customizing LXC Container"
2025-12-04 07:52:18 +01:00
# # 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
2025-06-22 20:25:01 +02:00
if [ " $var_os " == "alpine" ] ; then
sleep 3
pct exec " $CTID " -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
2024-06-30 09:35:33 -04:00
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
2024-01-16 22:24:33 -05:00
EOF'
2025-07-22 14:46:50 +02:00
pct exec " $CTID " -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
2025-06-22 20:43:25 +02:00
else
sleep 3
2025-12-28 19:04:05 +11:00
LANG = ${ LANG :- en_US .UTF-8 }
pct exec " $CTID " -- bash -c "sed -i \"/ $LANG / s/^# //\" /etc/locale.gen"
2025-06-22 20:43:25 +02:00
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"
2025-07-04 21:22:50 +02:00
if [[ -z " ${ tz :- } " ]] ; then
2025-12-16 11:28:12 +01:00
tz = $( timedatectl show --property= Timezone --value 2>/dev/null || echo "UTC" )
2025-07-04 21:22:50 +02:00
fi
2025-12-16 11:28:12 +01:00
[[ " ${ tz :- } " == Etc/* ]] && tz = "UTC" # Normalize Etc/* to UTC for container setup
2025-12-04 07:52:18 +01:00
2025-06-22 20:53:02 +02:00
if pct exec " $CTID " -- test -e "/usr/share/zoneinfo/ $tz " ; then
2025-12-04 07:52:18 +01:00
# Set timezone using symlink (Debian 13+ compatible)
# Create /etc/timezone for backwards compatibility with older scripts
pct exec " $CTID " -- bash -c "tz=' $tz '; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true"
2025-06-22 20:53:02 +02:00
else
2025-07-04 21:22:50 +02:00
msg_warn "Skipping timezone setup – zone ' $tz ' not found in container"
2025-06-22 20:53:02 +02:00
fi
2025-06-22 20:43:25 +02:00
2025-12-04 07:52:18 +01:00
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
}
2025-06-20 13:28:06 +02:00
fi
2025-12-04 07:52:18 +01:00
2025-06-22 20:43:25 +02:00
msg_ok "Customized LXC Container"
2025-06-22 19:54:55 +02:00
2025-12-04 07:52:18 +01:00
# Install SSH keys
install_ssh_keys_into_ct
# Run application installer
# Disable error trap - container errors are handled internally via flag file
set +Eeuo pipefail # Disable ALL error handling temporarily
trap - ERR # Remove ERR trap completely
2025-08-04 14:36:02 +02:00
lxc-attach -n " $CTID " -- bash -c " $( curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${ var_install } .sh) "
2025-12-04 07:52:18 +01:00
local lxc_exit = $?
set -Eeuo pipefail # Re-enable error handling
trap 'error_handler' ERR # Restore ERR trap
# Check for error flag file in container (more reliable than lxc-attach exit code)
local install_exit_code = 0
if [[ -n " ${ SESSION_ID :- } " ]] ; then
local error_flag = "/root/.install- ${ SESSION_ID } .failed"
if pct exec " $CTID " -- test -f " $error_flag " 2>/dev/null; then
install_exit_code = $( pct exec " $CTID " -- cat " $error_flag " 2>/dev/null || echo "1" )
pct exec " $CTID " -- rm -f " $error_flag " 2>/dev/null || true
fi
fi
# Fallback to lxc-attach exit code if no flag file
if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]] ; then
install_exit_code = $lxc_exit
fi
# Installation failed?
if [[ $install_exit_code -ne 0 ]] ; then
msg_error "Installation failed in container ${ CTID } (exit code: ${ install_exit_code } )"
# Copy both logs from container before potential deletion
local build_log_copied = false
local install_log_copied = false
if [[ -n " $CTID " && -n " ${ SESSION_ID :- } " ]] ; then
# Copy BUILD_LOG (creation log) if it exists
if [[ -f " ${ BUILD_LOG } " ]] ; then
cp " ${ BUILD_LOG } " "/tmp/create-lxc- ${ CTID } - ${ SESSION_ID } .log" 2>/dev/null && build_log_copied = true
fi
# Copy INSTALL_LOG from container
if pct pull " $CTID " "/root/.install- ${ SESSION_ID } .log" "/tmp/install-lxc- ${ CTID } - ${ SESSION_ID } .log" 2>/dev/null; then
install_log_copied = true
fi
# Show available logs
echo ""
[[ " $build_log_copied " == true ]] && echo -e " ${ GN } ✔ ${ CL } Container creation log: ${ BL } /tmp/create-lxc- ${ CTID } - ${ SESSION_ID } .log ${ CL } "
[[ " $install_log_copied " == true ]] && echo -e " ${ GN } ✔ ${ CL } Installation log: ${ BL } /tmp/install-lxc- ${ CTID } - ${ SESSION_ID } .log ${ CL } "
fi
# Dev mode: Keep container or open breakpoint shell
if [[ " ${ DEV_MODE_KEEP :- false } " == "true" ]] ; then
msg_dev "Keep mode active - container ${ CTID } preserved"
return 0
elif [[ " ${ DEV_MODE_BREAKPOINT :- false } " == "true" ]] ; then
msg_dev "Breakpoint mode - opening shell in container ${ CTID } "
echo -e " ${ YW } Type 'exit' to return to host ${ CL } "
pct enter " $CTID "
echo ""
echo -en " ${ YW } Container ${ CTID } still running. Remove now? (y/N): ${ CL } "
if read -r response && [[ " $response " = ~ ^[ Yy] $ ]] ; then
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
msg_ok "Container ${ CTID } removed"
else
msg_dev "Container ${ CTID } kept for debugging"
fi
exit $install_exit_code
fi
# Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner)
echo ""
echo -en " ${ YW } Remove broken container ${ CTID } ? (Y/n) [auto-remove in 60s]: ${ CL } "
if read -t 60 -r response; then
if [[ -z " $response " || " $response " = ~ ^[ Yy] $ ]] ; then
# Remove container
echo -e "\n ${ TAB }${ HOLD }${ YW } Removing container ${ CTID }${ CL } "
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
elif [[ " $response " = ~ ^[ Nn] $ ]] ; then
echo -e "\n ${ TAB }${ YW } Container ${ CTID } kept for debugging ${ CL } "
# Dev mode: Setup MOTD/SSH for debugging access to broken container
if [[ " ${ DEV_MODE_MOTD :- false } " == "true" ]] ; then
echo -e " ${ TAB }${ HOLD }${ DGN } Setting up MOTD and SSH for debugging... ${ CL } "
if pct exec " $CTID " -- bash -c "
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)
declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true
" >/dev/null 2>& 1; then
local ct_ip = $( pct exec " $CTID " ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
echo -e " ${ BFR }${ CM }${ GN } MOTD/SSH ready - SSH into container: ssh root@ ${ ct_ip }${ CL } "
fi
fi
fi
else
# Timeout - auto-remove
echo -e "\n ${ YW } No response - auto-removing container ${ CL } "
echo -e " ${ TAB }${ HOLD }${ YW } Removing container ${ CTID }${ CL } "
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
fi
exit $install_exit_code
fi
}
destroy_lxc() {
if [[ -z " $CT_ID " ]] ; then
msg_error "No CT_ID found. Nothing to remove."
return 1
fi
# Abort on 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 returns non-zero on 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_custom "ℹ ️" " ${ BL } " "Container was not removed."
;;
*)
msg_warn "Invalid response. Container was not removed."
;;
esac
}
# ------------------------------------------------------------------------------
# Storage discovery / selection helpers
# ------------------------------------------------------------------------------
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
}
fix_gpu_gids() {
if [[ -z " ${ GPU_TYPE :- } " ]] ; then
return 0
fi
msg_info "Detecting and setting correct GPU group IDs"
# Get actual GIDs from container
local video_gid = $( pct exec " $CTID " -- sh -c "getent group video 2>/dev/null | cut -d: -f3" )
local render_gid = $( pct exec " $CTID " -- sh -c "getent group render 2>/dev/null | cut -d: -f3" )
# Create groups if they don't exist
if [[ -z " $video_gid " ]] ; then
pct exec " $CTID " -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>& 1
video_gid = $( pct exec " $CTID " -- sh -c "getent group video 2>/dev/null | cut -d: -f3" )
[[ -z " $video_gid " ]] && video_gid = "44"
fi
if [[ -z " $render_gid " ]] ; then
pct exec " $CTID " -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>& 1
render_gid = $( pct exec " $CTID " -- sh -c "getent group render 2>/dev/null | cut -d: -f3" )
[[ -z " $render_gid " ]] && render_gid = "104"
fi
# Stop container to update config
pct stop " $CTID " >/dev/null 2>& 1
sleep 1
# Update dev entries with correct GIDs
sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid= ${ render_gid } |g" " $LXC_CONFIG "
sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid= ${ video_gid } |g" " $LXC_CONFIG "
# Restart container
pct start " $CTID " >/dev/null 2>& 1
sleep 2
msg_ok "GPU passthrough configured (video: ${ video_gid } , render: ${ render_gid } )"
# For privileged containers: also fix permissions inside container
if [[ " $CT_TYPE " == "0" ]] ; then
2025-12-05 09:17:19 +01:00
pct exec " $CTID " -- sh -c "
2025-12-04 07:52:18 +01:00
if [ -d /dev/dri ]; then
for dev in /dev/dri/*; do
if [ -e \"\$dev\" ]; then
2025-12-05 09:17:19 +01:00
case \"\$dev\" in
*renderD*) chgrp ${ render_gid } \"\$dev\" 2>/dev/null || true ;;
*) chgrp ${ video_gid } \"\$dev\" 2>/dev/null || true ;;
esac
2025-12-04 07:52:18 +01:00
chmod 660 \"\$dev\" 2>/dev/null || true
fi
done
fi
" >/dev/null 2>& 1
fi
}
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 "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 $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; 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 0
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 [[ -z " ${ var_template_storage :- } " ]] ; then
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
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
if [[ -z " ${ var_container_storage :- } " ]] ; then
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]"
fi
fi
fi
2025-12-04 19:41:01 +01:00
msg_info "Validating storage ' $CONTAINER_STORAGE '"
STORAGE_TYPE = $( grep -E "^[^:]+: $CONTAINER_STORAGE $" /etc/pve/storage.cfg | cut -d: -f1 | head -1)
case " $STORAGE_TYPE " in
iscsidirect) exit 212 ;;
iscsi | zfs) exit 213 ;;
cephfs) exit 219 ;;
pbs) exit 224 ;;
linstor | rbd | nfs | cifs)
pvesm status -storage " $CONTAINER_STORAGE " & >/dev/null || exit 217
;;
esac
2025-12-04 07:52:18 +01:00
2025-12-04 19:41:01 +01:00
pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx " $CONTAINER_STORAGE " || exit 213
msg_ok "Storage ' $CONTAINER_STORAGE ' ( $STORAGE_TYPE ) validated"
2025-12-04 07:52:18 +01:00
2025-12-04 19:41:01 +01:00
msg_info "Validating template storage ' $TEMPLATE_STORAGE '"
2025-12-04 07:52:18 +01:00
TEMPLATE_TYPE = $( grep -E "^[^:]+: $TEMPLATE_STORAGE $" /etc/pve/storage.cfg | cut -d: -f1)
2025-12-04 19:41:01 +01:00
if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx " $TEMPLATE_STORAGE " ; then
msg_warn "Template storage ' $TEMPLATE_STORAGE ' may not support 'vztmpl'"
2025-12-04 07:52:18 +01:00
fi
2025-12-04 19:41:01 +01:00
msg_ok "Template storage ' $TEMPLATE_STORAGE ' validated"
2025-12-04 07:52:18 +01:00
# 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)."
2025-12-04 19:41:01 +01:00
exit 210
2025-12-04 07:52:18 +01:00
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 |
2025-12-24 14:23:10 +01:00
awk -v search = " ${ TEMPLATE_SEARCH } " -v pattern = " ${ TEMPLATE_PATTERN } " '$1 ~ search && $1 ~ pattern {print $1}' |
2025-12-04 07:52:18 +01:00
sed 's|.*/||' | sort -t - -k 2 -V
)
pveam update >/dev/null 2>& 1 || msg_warn "Could not update template catalog (pveam update failed)."
msg_ok "Template search completed"
set +u
2025-12-24 14:23:10 +01:00
mapfile -t ONLINE_TEMPLATES < <( pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^ ${ TEMPLATE_SEARCH } .* ${ TEMPLATE_PATTERN } " | sort -t - -k 2 -V 2>/dev/null || true )
2025-12-04 07:52:18 +01:00
set -u
ONLINE_TEMPLATE = ""
[[ ${# ONLINE_TEMPLATES [@] } -gt 0 ]] && ONLINE_TEMPLATE = " ${ ONLINE_TEMPLATES [-1] } "
2025-12-04 19:41:01 +01:00
if [[ ${# ONLINE_TEMPLATES [@] } -gt 0 ]] ; then
count = 0
for idx in " ${ !ONLINE_TEMPLATES[@] } " ; do
(( count++))
[[ $count -ge 3 ]] && break
done
fi
2025-12-04 07:52:18 +01:00
if [[ ${# LOCAL_TEMPLATES [@] } -gt 0 ]] ; then
TEMPLATE = " ${ LOCAL_TEMPLATES [-1] } "
TEMPLATE_SOURCE = "local"
else
TEMPLATE = " $ONLINE_TEMPLATE "
TEMPLATE_SOURCE = "online"
fi
# If still no template, try to find alternatives
if [[ -z " $TEMPLATE " ]] ; then
echo ""
echo "[DEBUG] No template found for ${ PCT_OSTYPE } ${ PCT_OSVERSION } , searching for alternatives..."
# Get all available versions for this OS type
mapfile -t AVAILABLE_VERSIONS < <(
pveam available -section system 2>/dev/null |
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
awk -F'\t' '{print $1}' |
grep "^ ${ PCT_OSTYPE } -" |
sed -E "s/.* ${ PCT_OSTYPE } -([0-9]+(\.[0-9]+)?).*/\1/" |
sort -u -V 2>/dev/null
)
if [[ ${# AVAILABLE_VERSIONS [@] } -gt 0 ]] ; then
echo ""
echo " ${ BL } Available ${ PCT_OSTYPE } versions: ${ CL } "
for i in " ${ !AVAILABLE_VERSIONS[@] } " ; do
echo " [ $(( i + 1 )) ] ${ AVAILABLE_VERSIONS [ $i ] } "
done
echo ""
read -p "Select version [1- ${# AVAILABLE_VERSIONS [@] } ] or press Enter to cancel: " choice
if [[ " $choice " = ~ ^[ 0-9] +$ ]] && [[ " $choice " -ge 1 ]] && [[ " $choice " -le ${# AVAILABLE_VERSIONS [@] } ]] ; then
PCT_OSVERSION = " ${ AVAILABLE_VERSIONS [ $(( choice - 1 )) ] } "
TEMPLATE_SEARCH = " ${ PCT_OSTYPE } - ${ PCT_OSVERSION } "
mapfile -t ONLINE_TEMPLATES < <(
pveam available -section system 2>/dev/null |
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
2025-12-24 14:23:10 +01:00
awk '{print $2}' |
grep -E "^ ${ TEMPLATE_SEARCH } -.* ${ TEMPLATE_PATTERN } " |
2025-12-04 07:52:18 +01:00
sort -t - -k 2 -V 2>/dev/null || true
)
if [[ ${# ONLINE_TEMPLATES [@] } -gt 0 ]] ; then
TEMPLATE = " ${ ONLINE_TEMPLATES [-1] } "
TEMPLATE_SOURCE = "online"
else
msg_error "No templates available for ${ PCT_OSTYPE } ${ PCT_OSVERSION } "
exit 225
fi
else
msg_custom "🚫" " ${ YW } " "Installation cancelled"
exit 0
fi
else
msg_error "No ${ PCT_OSTYPE } templates available at all"
exit 225
fi
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
# If we still don't have a path but have a valid template name, construct it
if [[ -z " $TEMPLATE_PATH " && -n " $TEMPLATE " ]] ; then
TEMPLATE_PATH = "/var/lib/vz/template/cache/ $TEMPLATE "
fi
[[ -n " $TEMPLATE_PATH " ]] || {
if [[ -z " $TEMPLATE " ]] ; then
msg_error "Template ${ PCT_OSTYPE } ${ PCT_OSVERSION } not available"
# Get available versions
mapfile -t AVAILABLE_VERSIONS < <(
pveam available -section system 2>/dev/null |
grep "^ ${ PCT_OSTYPE } -" |
sed -E 's/.*' " ${ PCT_OSTYPE } " '-([0-9]+\.[0-9]+).*/\1/' |
grep -E '^[0-9]+\.[0-9]+$' |
sort -u -V 2>/dev/null || sort -u
)
if [[ ${# AVAILABLE_VERSIONS [@] } -gt 0 ]] ; then
echo -e "\n ${ BL } Available versions: ${ CL } "
for i in " ${ !AVAILABLE_VERSIONS[@] } " ; do
echo " [ $(( i + 1 )) ] ${ AVAILABLE_VERSIONS [ $i ] } "
done
echo ""
read -p "Select version [1- ${# AVAILABLE_VERSIONS [@] } ] or Enter to exit: " choice
if [[ " $choice " = ~ ^[ 0-9] +$ ]] && [[ " $choice " -ge 1 ]] && [[ " $choice " -le ${# AVAILABLE_VERSIONS [@] } ]] ; then
export var_version = " ${ AVAILABLE_VERSIONS [ $(( choice - 1 )) ] } "
export PCT_OSVERSION = " $var_version "
msg_ok "Switched to ${ PCT_OSTYPE } ${ var_version } "
# Retry template search with new version
TEMPLATE_SEARCH = " ${ PCT_OSTYPE } - ${ PCT_OSVERSION :- } "
mapfile -t LOCAL_TEMPLATES < <(
pveam list " $TEMPLATE_STORAGE " 2>/dev/null |
2025-12-24 14:23:10 +01:00
awk -v search = " ${ TEMPLATE_SEARCH } -" -v pattern = " ${ TEMPLATE_PATTERN } " '$1 ~ search && $1 ~ pattern {print $1}' |
2025-12-04 07:52:18 +01:00
sed 's|.*/||' | sort -t - -k 2 -V
)
mapfile -t ONLINE_TEMPLATES < <(
pveam available -section system 2>/dev/null |
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
2025-12-24 14:23:10 +01:00
awk '{print $2}' |
grep -E "^ ${ TEMPLATE_SEARCH } -.* ${ TEMPLATE_PATTERN } " |
2025-12-04 07:52:18 +01:00
sort -t - -k 2 -V 2>/dev/null || true
)
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
# If we still don't have a path but have a valid template name, construct it
if [[ -z " $TEMPLATE_PATH " && -n " $TEMPLATE " ]] ; then
TEMPLATE_PATH = "/var/lib/vz/template/cache/ $TEMPLATE "
fi
[[ -n " $TEMPLATE_PATH " ]] || {
msg_error "Template still not found after version change"
exit 220
}
else
msg_custom "🚫" " ${ YW } " "Installation cancelled"
exit 1
fi
else
msg_error "No ${ PCT_OSTYPE } templates available"
exit 220
fi
fi
}
# Validate that we found a template
if [[ -z " $TEMPLATE " ]] ; then
msg_error "No template found for ${ PCT_OSTYPE } ${ PCT_OSVERSION } "
msg_custom "ℹ ️" " ${ YW } " "Please check:"
msg_custom " •" " ${ YW } " "Is pveam catalog available? (run: pveam available -section system)"
msg_custom " •" " ${ YW } " "Does the template exist for your OS version?"
exit 225
fi
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_custom "ℹ ️" " ${ BL } " "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
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
# PCT_OPTIONS is now a string (exported from build_container)
# Add rootfs if not already specified
if [[ ! " $PCT_OPTIONS " = ~ "-rootfs" ]] ; then
PCT_OPTIONS = " $PCT_OPTIONS
-rootfs $CONTAINER_STORAGE : ${ PCT_DISK_SIZE :- 8 } "
fi
# 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."
2025-12-04 19:41:01 +01:00
exit 211
2025-12-04 07:52:18 +01:00
}
LOGFILE = "/tmp/pct_create_ ${ CTID } _ $( date +%Y%m%d_%H%M%S) _ ${ SESSION_ID } .log"
msg_debug "pct create command: pct create $CTID ${ TEMPLATE_STORAGE } :vztmpl/ ${ TEMPLATE } $PCT_OPTIONS "
msg_debug "Logfile: $LOGFILE "
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
if ! pct create " $CTID " " ${ TEMPLATE_STORAGE } :vztmpl/ ${ TEMPLATE } " $PCT_OPTIONS >" $LOGFILE " 2>& 1; then
msg_debug "Container creation failed on ${ TEMPLATE_STORAGE } . Validating 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 not already on local
if [[ " $TEMPLATE_STORAGE " != "local" ]] ; then
msg_info "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
# Local fallback also failed - check for LXC stack version issue
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."
offer_lxc_stack_upgrade_and_maybe_retry "yes"
rc = $?
case $rc in
0) : ;; # success - container created, continue
2)
echo "Upgrade was declined. Please update and re-run:
2025-12-04 19:41:01 +01:00
apt update && apt install --only-upgrade pve-container lxc-pve"
exit 231
2025-12-04 07:52:18 +01:00
;;
3)
echo "Upgrade and/or retry failed. Please inspect: $LOGFILE "
2025-12-04 19:41:01 +01:00
exit 231
2025-12-04 07:52:18 +01:00
;;
esac
else
msg_error "Container creation failed. 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
pct create " $CTID " "local:vztmpl/ ${ TEMPLATE } " $PCT_OPTIONS 2>& 1 | tee -a " $LOGFILE "
set +x
fi
exit 209
fi
else
msg_ok "Container successfully created using local fallback."
fi
else
# Already on local storage and still failed - check LXC stack version
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."
offer_lxc_stack_upgrade_and_maybe_retry "yes"
rc = $?
case $rc in
0) : ;; # success - container created, continue
2)
echo "Upgrade was declined. Please update and re-run:
2025-12-04 19:41:01 +01:00
apt update && apt install --only-upgrade pve-container lxc-pve"
exit 231
2025-12-04 07:52:18 +01:00
;;
3)
echo "Upgrade and/or retry failed. Please inspect: $LOGFILE "
2025-12-04 19:41:01 +01:00
exit 231
2025-12-04 07:52:18 +01:00
;;
esac
else
msg_error "Container creation failed. 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
pct create " $CTID " "local:vztmpl/ ${ TEMPLATE } " $PCT_OPTIONS 2>& 1 | tee -a " $LOGFILE "
set +x
fi
exit 209
fi
fi
else
msg_ok "Container successfully created after template repair."
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."
# Report container creation to API
post_to_api
2025-06-22 20:25:01 +02:00
}
2023-03-22 20:48:20 -04:00
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 9: POST-INSTALLATION & FINALIZATION
# ==============================================================================
# ------------------------------------------------------------------------------
# description()
#
# - Sets container description with formatted HTML content
# - Includes:
# * Community-Scripts logo
# * Application name
# * Links to GitHub, Discussions, Issues
# * Ko-fi donation badge
# - Restarts ping-instances.service if present (monitoring)
# - Posts final "done" status to API telemetry
# ------------------------------------------------------------------------------
2023-03-22 20:48:20 -04:00
description() {
IP = $( pct exec " $CTID " ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
2023-11-27 09:01:41 -05:00
2024-11-12 16:54:39 +01:00
# Generate LXC Description
2025-01-29 14:26:23 +01:00
DESCRIPTION = $(
cat <<EOF
2024-11-12 16:54:39 +01:00
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/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/☕-Buy us a coffee-blue' alt='spend Coffee' />
</a>
</p>
2025-05-13 18:44:06 +02:00
2024-11-12 16:54:39 +01:00
<span style='margin: 0 10px;'>
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
</span>
</div>
EOF
2025-01-29 14:26:23 +01:00
)
2024-11-12 16:54:39 +01:00
pct set " $CTID " -description " $DESCRIPTION "
2023-11-27 09:01:41 -05:00
2023-04-30 05:56:05 -04:00
if [[ -f /etc/systemd/system/ping-instances.service ]] ; then
2023-05-06 04:24:18 -04:00
systemctl start ping-instances.service
2023-04-30 05:56:05 -04:00
fi
2025-02-24 11:18:38 +01:00
2025-02-04 13:23:20 +01:00
post_update_to_api "done" "none"
2024-01-13 21:11:58 -05:00
}
2025-01-30 18:19:05 +01:00
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 10: ERROR HANDLING & EXIT TRAPS
# ==============================================================================
# ------------------------------------------------------------------------------
# api_exit_script()
#
# - Exit trap handler for reporting to API telemetry
# - Captures exit code and reports to API using centralized error descriptions
# - Uses explain_exit_code() from error_handler.func for consistent error messages
# - Posts failure status with exit code to API (error description added automatically)
# - Only executes on non-zero exit codes
# ------------------------------------------------------------------------------
2025-05-06 09:50:11 +02:00
api_exit_script() {
2025-12-04 07:52:18 +01:00
exit_code = $?
2025-03-27 11:51:40 +01:00
if [ $exit_code -ne 0 ] ; then
2025-12-04 07:52:18 +01:00
post_update_to_api "failed" " $exit_code "
2025-02-24 11:18:38 +01:00
fi
2025-02-05 16:29:19 +01:00
}
2025-07-04 21:22:50 +02:00
if command -v pveversion >/dev/null 2>& 1; then
trap 'api_exit_script' EXIT
fi
2025-02-05 16:29:19 +01:00
trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
2025-02-24 11:18:38 +01:00
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
2025-02-05 16:29:19 +01:00
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM