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() {
2026-02-24 14:22:07 +01: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.
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 = "no" # Safe default: no telemetry until user consents via diagnostics_check()
METHOD = "default" # sets the METHOD variable to "default", used for the API call.
RANDOM_UUID = " $( cat /proc/sys/kernel/random/uuid) " # generates a random UUID and sets it to the RANDOM_UUID variable.
EXECUTION_ID = " ${ RANDOM_UUID } " # Unique execution ID for telemetry record identification (unique-indexed in PocketBase)
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
# NOTE: combined_log is constructed locally in build_container() and ensure_log_on_host()
# as "/tmp/${NSAPP}-${CTID}-${SESSION_ID}.log" (requires CTID, not available here)
2025-12-04 07:52:18 +01:00
CTTYPE = " ${ CTTYPE :- ${ CT_TYPE :- 1 }} "
2026-06-11 16:14:32 +10:00
# ARM64 Template default variables
DEBIAN_DEFAULT_CODENAME = "trixie"
UBUNTU_DEFAULT_CODENAME = "noble"
ALPINE_DEFAULT_VERSION = "3.23"
2025-12-04 07:52:18 +01:00
# 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
2026-07-29 14:11:08 +02:00
# `source <(curl ...)` hides the download exit code, so a failed fetch would
# silently source an empty stream and leave the helpers undefined.
_bootstrap_die() {
if declare -f msg_error >/dev/null 2>& 1; then
msg_error " $1 "
else
echo "FATAL: $1 " >& 2
fi
exit 115
}
_bootstrap_source() {
local url = " $1 " probe = " $2 " content = ""
if command -v curl >/dev/null 2>& 1; then
content = $( curl -fsSL --connect-timeout 10 --retry 5 --retry-connrefused --retry-delay 2 " $url " ) || content = ""
elif command -v wget >/dev/null 2>& 1; then
content = $( wget -qO- --tries= 5 --timeout= 10 " $url " ) || content = ""
else
_bootstrap_die "Neither curl nor wget available"
fi
[[ -n " $content " ]] || _bootstrap_die "Failed to download ${ url ##*/ } "
source /dev/stdin <<< " $content "
declare -f " $probe " >/dev/null 2>& 1 ||
_bootstrap_die " ${ url ##*/ } loaded but incomplete (missing ${ probe } )"
}
2023-03-22 20:48:20 -04:00
2026-07-29 14:11:08 +02:00
_FUNC_BASE = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc"
_bootstrap_source " $_FUNC_BASE /api.func" post_to_api
_bootstrap_source " $_FUNC_BASE /core.func" load_functions
_bootstrap_source " $_FUNC_BASE /error_handler.func" catch_errors
load_functions
catch_errors
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
2026-02-27 13:59:02 +01:00
msg_error "Unable to read kernel key parameters. Ensure proper permissions."
2026-03-02 10:55:20 +01:00
exit 107
2025-01-20 09:19:47 +01:00
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
2026-02-27 13:59:02 +01:00
msg_warn "Key usage is near the limit ( ${ used_lxc_keys } / ${ per_user_maxkeys } )"
2025-01-20 09:19:47 +01:00
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
2026-02-27 13:59:02 +01:00
msg_warn "Key byte usage is near the limit ( ${ used_lxc_bytes } / ${ per_user_maxbytes } )"
2025-01-20 09:19:47 +01:00
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
2026-02-27 13:59:02 +01:00
msg_error "Kernel key limits exceeded - see suggestions above"
2026-03-02 10:55:20 +01:00
exit 108
2025-01-20 09:19:47 +01:00
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`
2026-01-23 09:05:17 +01:00
# - Alpine: parses eth0 via `ip -4 addr` or `ip -6 addr`
# - Supports IPv6-only environments as fallback
2025-12-04 07:52:18 +01:00
# - Returns "Unknown" if OS type cannot be determined
# ------------------------------------------------------------------------------
2024-12-28 20:37:58 +01:00
get_current_ip() {
2026-01-23 09:05:17 +01:00
CURRENT_IP = ""
2024-12-28 20:37:58 +01:00
if [ -f /etc/os-release ] ; then
# Check for Debian/Ubuntu (uses hostname -I)
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
2026-01-23 09:05:17 +01:00
# Try IPv4 first
2026-03-25 13:06:21 +01:00
CURRENT_IP = $( hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true )
2026-01-23 09:05:17 +01:00
# Fallback to IPv6 if no IPv4
if [[ -z " $CURRENT_IP " ]] ; then
2026-03-25 13:06:21 +01:00
CURRENT_IP = $( hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1 || true )
2026-01-23 09:05:17 +01:00
fi
2024-12-28 20:37:58 +01:00
# Check for Alpine (uses ip command)
elif grep -q 'ID=alpine' /etc/os-release; then
2026-01-23 09:05:17 +01:00
# Try IPv4 first
CURRENT_IP = $( ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
# Fallback to IPv6 if no IPv4
if [[ -z " $CURRENT_IP " ]] ; then
CURRENT_IP = $( ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n 1)
fi
2024-12-28 20:37:58 +01:00
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
2026-01-28 13:26:20 +01:00
# - Regenerates /etc/profile.d/00_lxc-details.sh with dynamic OS/IP info
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
2024-12-28 20:37:58 +01:00
update_motd_ip() {
MOTD_FILE = "/etc/motd"
2026-01-28 13:26:20 +01:00
PROFILE_FILE = "/etc/profile.d/00_lxc-details.sh"
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
2026-01-28 13:26:20 +01:00
# Update dynamic LXC details profile if values changed (e.g., after OS upgrade)
# Only update if file exists and is from community-scripts
if [ -f " $PROFILE_FILE " ] && grep -q "community-scripts" " $PROFILE_FILE " 2>/dev/null; then
# Get current values
local current_hostname = " $( hostname) "
local current_ip = " $( hostname -I | awk '{print $1}' ) "
2026-02-14 13:41:09 +01:00
2026-03-23 22:22:23 +01:00
# Escape sed special chars in replacement strings (& \ |)
2026-03-24 09:57:43 +01:00
current_hostname = " ${ current_hostname // \\ / \\\\ } "
current_hostname = " ${ current_hostname //&/ \\ & } "
current_ip = " ${ current_ip // \\ / \\\\ } "
current_ip = " ${ current_ip //&/ \\ & } "
2026-03-23 22:22:23 +01:00
2026-01-28 13:26:20 +01:00
# Update only if values actually changed
if ! grep -q "Hostname:.* $current_hostname " " $PROFILE_FILE " 2>/dev/null; then
sed -i "s|Hostname:.*|Hostname: \${GN} $current_hostname \${CL}\\\"|" " $PROFILE_FILE "
fi
if ! grep -q "IP Address:.* $current_ip " " $PROFILE_FILE " 2>/dev/null; then
sed -i "s|IP Address:.*|IP Address: \${GN} $current_ip \${CL}\\\"|" " $PROFILE_FILE "
fi
fi
2024-12-28 20:37:58 +01:00
}
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
2026-01-23 09:05:17 +01:00
# - Supports both SSH_KEYS_FILE (from advanced settings) and SSH_AUTHORIZED_KEY (from user defaults)
2025-12-04 07:52:18 +01:00
# - 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
2026-01-23 09:05:17 +01:00
# If SSH_KEYS_FILE doesn't exist but SSH_AUTHORIZED_KEY is set (from user defaults),
# create a temporary SSH_KEYS_FILE with the key
if [[ -z " $SSH_KEYS_FILE " || ! -s " $SSH_KEYS_FILE " ]] && [[ -n " ${ SSH_AUTHORIZED_KEY :- } " ]] ; then
SSH_KEYS_FILE = " $( mktemp) "
printf '%s\n' " $SSH_AUTHORIZED_KEY " >" $SSH_KEYS_FILE "
fi
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"
2026-03-26 16:07:38 +01:00
return 252
2025-12-04 07:52:18 +01:00
}
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"
2026-03-26 16:07:38 +01:00
return 252
2025-12-04 07:52:18 +01:00
}
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
}
2026-01-12 14:04:28 +01:00
# ------------------------------------------------------------------------------
# validate_container_id()
#
2026-02-14 15:28:47 +01:00
# - Validates if a container ID is available for use (CLUSTER-WIDE)
# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes
# - Falls back to local config file check if pvesh unavailable
2026-01-12 14:04:28 +01:00
# - Checks if ID is used in LVM logical volumes
# - Returns 0 if ID is available, 1 if already in use
# ------------------------------------------------------------------------------
validate_container_id() {
local ctid = " $1 "
# Check if ID is numeric
if ! [[ " $ctid " = ~ ^[ 0-9] +$ ]] ; then
return 1
fi
2026-02-14 15:28:47 +01:00
# CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes
# This catches IDs used on other nodes in the cluster
# NOTE: Works on single-node too - Proxmox always has internal cluster structure
# Falls back gracefully if pvesh unavailable or returns empty
if command -v pvesh & >/dev/null; then
local cluster_ids
2026-02-17 12:14:46 +01:00
cluster_ids = $( pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
2026-02-14 15:28:47 +01:00
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true )
if [[ -n " $cluster_ids " ]] && echo " $cluster_ids " | grep -qw " $ctid " ; then
return 1
fi
2026-01-12 14:04:28 +01:00
fi
2026-02-14 15:28:47 +01:00
# LOCAL FALLBACK: Check if config file exists for VM or LXC
# This handles edge cases where pvesh might not return all info
if [[ -f "/etc/pve/qemu-server/ ${ ctid } .conf" ]] || [[ -f "/etc/pve/lxc/ ${ ctid } .conf" ]] ; then
2026-01-12 14:04:28 +01:00
return 1
fi
2026-02-14 15:28:47 +01:00
# Check ALL nodes in cluster for config files (handles pmxcfs sync delays)
# NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works
if [[ -d "/etc/pve/nodes" ]] ; then
for node_dir in /etc/pve/nodes/*/; do
if [[ -f " ${ node_dir } qemu-server/ ${ ctid } .conf" ]] || [[ -f " ${ node_dir } lxc/ ${ ctid } .conf" ]] ; then
return 1
fi
done
2026-01-12 14:04:28 +01:00
fi
# Check if ID is used in LVM logical volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_]) ${ ctid } ( $|[-_])" ; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# get_valid_container_id()
#
2026-02-14 15:28:47 +01:00
# - Returns a valid, unused container ID (CLUSTER-AWARE)
# - Uses pvesh /cluster/nextid as starting point (already cluster-aware)
2026-01-12 14:04:28 +01:00
# - If provided ID is valid, returns it
2026-02-14 15:28:47 +01:00
# - Otherwise increments until a free one is found across entire cluster
2026-01-12 14:04:28 +01:00
# - Calls validate_container_id() to check availability
# ------------------------------------------------------------------------------
get_valid_container_id() {
2026-02-14 15:28:47 +01:00
local suggested_id = " ${ 1 :-$( pvesh get /cluster/nextid 2>/dev/null || echo 100) } "
# Ensure we have a valid starting ID
if ! [[ " $suggested_id " = ~ ^[ 0-9] +$ ]] ; then
suggested_id = $( pvesh get /cluster/nextid 2>/dev/null || echo 100)
fi
local max_attempts = 1000
local attempts = 0
2026-01-12 14:04:28 +01:00
while ! validate_container_id " $suggested_id " ; do
suggested_id = $(( suggested_id + 1 ))
2026-02-14 15:28:47 +01:00
attempts = $(( attempts + 1 ))
if [[ $attempts -ge $max_attempts ]] ; then
msg_error "Could not find available container ID after $max_attempts attempts"
2026-03-02 10:55:20 +01:00
exit 109
2026-02-14 15:28:47 +01:00
fi
2026-01-12 14:04:28 +01:00
done
echo " $suggested_id "
}
2026-01-19 22:39:57 +01:00
# ------------------------------------------------------------------------------
# validate_hostname()
#
# - Validates hostname/FQDN according to RFC 1123/952
# - Checks total length (max 253 characters for FQDN)
# - Validates each label (max 63 chars, alphanumeric + hyphens)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_hostname() {
local hostname = " $1 "
# Check total length (max 253 for FQDN)
if [[ ${# hostname } -gt 253 ]] || [[ -z " $hostname " ]] ; then
return 1
fi
# Split by dots and validate each label
local IFS = '.'
2026-02-14 13:41:09 +01:00
read -ra labels <<< " $hostname "
2026-01-19 22:39:57 +01:00
for label in " ${ labels [@] } " ; do
# Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end)
if [[ -z " $label " ]] || [[ ${# label } -gt 63 ]] ; then
return 1
fi
if [[ ! " $label " = ~ ^[ a-z0-9]([ a-z0-9-] *[ a-z0-9]) ?$ ]] && [[ ! " $label " = ~ ^[ a-z0-9] $ ]] ; then
return 1
fi
done
return 0
}
2026-01-20 15:03:14 +01:00
# ------------------------------------------------------------------------------
# validate_mac_address()
#
# - Validates MAC address format (XX:XX:XX:XX:XX:XX)
# - Empty value is allowed (auto-generated)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_mac_address() {
local mac = " $1 "
[[ -z " $mac " ]] && return 0
if [[ ! " $mac " = ~ ^([ 0-9A-Fa-f]{ 2} :){ 5}[ 0-9A-Fa-f]{ 2} $ ]] ; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# validate_vlan_tag()
#
# - Validates VLAN tag (1-4094)
# - Empty value is allowed (no VLAN)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_vlan_tag() {
local vlan = " $1 "
[[ -z " $vlan " ]] && return 0
if ! [[ " $vlan " = ~ ^[ 0-9] +$ ]] || (( vlan < 1 || vlan > 4094)) ; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# validate_mtu()
#
# - Validates MTU size (576-65535, common values: 1500, 9000)
# - Empty value is allowed (default 1500)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_mtu() {
local mtu = " $1 "
[[ -z " $mtu " ]] && return 0
if ! [[ " $mtu " = ~ ^[ 0-9] +$ ]] || (( mtu < 576 || mtu > 65535)) ; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# validate_ipv6_address()
#
# - Validates IPv6 address with optional CIDR notation
# - Supports compressed (::) and full notation
# - Empty value is allowed
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_ipv6_address() {
local ipv6 = " $1 "
[[ -z " $ipv6 " ]] && return 0
# Extract address and CIDR
local addr = " ${ ipv6 %%/* } "
local cidr = " ${ ipv6 ##*/ } "
# Validate CIDR if present (1-128)
if [[ " $ipv6 " == */* ]] ; then
if ! [[ " $cidr " = ~ ^[ 0-9] +$ ]] || (( cidr < 1 || cidr > 128)) ; then
return 1
fi
fi
# Basic IPv6 validation - check for valid characters and structure
# Must contain only hex digits and colons
if [[ ! " $addr " = ~ ^[ 0-9a-fA-F:] +$ ]] ; then
return 1
fi
# Must contain at least one colon
if [[ ! " $addr " == *:* ]] ; then
return 1
fi
# Check for valid double-colon usage (only one :: allowed)
if [[ " $addr " == *::*::* ]] ; then
return 1
fi
# Check that no segment exceeds 4 hex chars
local IFS = ':'
local -a segments
2026-02-14 13:41:09 +01:00
read -ra segments <<< " $addr "
2026-01-20 15:03:14 +01:00
for seg in " ${ segments [@] } " ; do
if [[ ${# seg } -gt 4 ]] ; then
return 1
fi
done
return 0
}
# ------------------------------------------------------------------------------
# validate_bridge()
#
# - Validates that network bridge exists and is active
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_bridge() {
local bridge = " $1 "
[[ -z " $bridge " ]] && return 1
# Check if bridge interface exists
2026-05-03 21:35:53 +01:00
if ! ip link show dev " $bridge " & >/dev/null; then
2026-01-20 15:03:14 +01:00
return 1
fi
return 0
}
2026-06-24 23:21:12 +02:00
# ------------------------------------------------------------------------------
# validate_sdn_vnet()
#
# - Validates that an SDN vnet exists in the cluster config
# ------------------------------------------------------------------------------
validate_sdn_vnet() {
local vnet = " $1 "
[[ -z " $vnet " ]] && return 1
[[ -f /etc/pve/sdn/vnets.cfg ]] && grep -qE "^vnet:[[:space:]]* ${ vnet } ([[:space:]]| $)" /etc/pve/sdn/vnets.cfg && return 0
command -v pvesh & >/dev/null && pvesh get "/cluster/sdn/vnets/ ${ vnet } " & >/dev/null && return 0
return 1
}
2026-01-20 15:03:14 +01:00
# ------------------------------------------------------------------------------
# validate_gateway_in_subnet()
#
# - Validates that gateway IP is in the same subnet as static IP
# - Arguments: static_ip (with CIDR), gateway_ip
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_gateway_in_subnet() {
local static_ip = " $1 "
local gateway = " $2 "
[[ -z " $static_ip " || -z " $gateway " ]] && return 0
# Extract IP and CIDR
local ip = " ${ static_ip %%/* } "
local cidr = " ${ static_ip ##*/ } "
2026-03-23 20:41:55 +01:00
# /31 and /32 are valid point-to-point / zero-trust DMZ configurations
# where the gateway is technically outside the subnet — skip validation
(( cidr >= 31)) && return 0
2026-01-20 15:03:14 +01:00
# Convert CIDR to netmask bits
local mask = $(( 0 xFFFFFFFF << ( 32 - cidr) & 0 xFFFFFFFF))
# Convert IPs to integers
local IFS = '.'
2026-02-14 13:41:09 +01:00
read -r i1 i2 i3 i4 <<< " $ip "
read -r g1 g2 g3 g4 <<< " $gateway "
2026-01-20 15:03:14 +01:00
2026-02-14 13:41:09 +01:00
local ip_int = $(( ( i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
local gw_int=$(((g1 << 24 ) + ( g2 << 16) + (g3 << 8) + g4))
2026-01-20 15:03:14 +01:00
# Check if both are in same network
2026-02-14 13:41:09 +01:00
if (((ip_int & mask) != (gw_int & mask))); then
2026-01-20 15:03:14 +01:00
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# validate_ip_address()
#
# - Validates IPv4 address with CIDR notation
# - Checks each octet is 0-255
# - Checks CIDR is 1-32
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_ip_address() {
local ip="$1"
[[ -z "$ip" ]] && return 1
# Check format with CIDR
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]]; then
return 1
fi
local o1="${BASH_REMATCH[1]}"
local o2="${BASH_REMATCH[2]}"
local o3="${BASH_REMATCH[3]}"
local o4="${BASH_REMATCH[4]}"
local cidr="${BASH_REMATCH[5]}"
# Validate octets (0-255)
for octet in "$o1" "$o2" "$o3" "$o4"; do
if ((octet > 255)); then
return 1
fi
done
# Validate CIDR (1-32)
if ((cidr < 1 || cidr > 32)); then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# validate_gateway_ip()
#
# - Validates gateway IPv4 address (without CIDR)
# - Checks each octet is 0-255
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_gateway_ip() {
local ip="$1"
[[ -z "$ip" ]] && return 0
# Check format without CIDR
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
return 1
fi
local o1="${BASH_REMATCH[1]}"
local o2="${BASH_REMATCH[2]}"
local o3="${BASH_REMATCH[3]}"
local o4="${BASH_REMATCH[4]}"
# Validate octets (0-255)
for octet in "$o1" "$o2" "$o3" "$o4"; do
if ((octet > 255)); then
return 1
fi
done
return 0
}
# ------------------------------------------------------------------------------
# validate_timezone()
#
# - Validates timezone string against system zoneinfo
# - Empty value or "host" is allowed
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_timezone() {
local tz="$1"
[[ -z "$tz" || "$tz" == "host" ]] && return 0
# Check if timezone file exists
if [[ ! -f "/usr/share/zoneinfo/$tz" ]]; then
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# validate_tags()
#
# - Validates Proxmox tags format
# - Only alphanumeric, hyphens, underscores, and semicolons allowed
# - Empty value is allowed
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_tags() {
local tags="$1"
[[ -z "$tags" ]] && return 0
# Tags can only contain alphanumeric, -, _, and ; (separator)
if [[ ! "$tags" =~ ^[a-zA-Z0-9_\;-]+$ ]]; then
return 1
fi
return 0
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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}
2026-02-25 01:23:14 -06:00
' | grep -E -c "$re" || true)
2025-12-04 07:52:18 +01:00
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) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((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 "
2026-03-26 16:07:38 +01:00
return 65
2025-12-04 07:52:18 +01:00
;;
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 = ""
2026-01-12 18:12:55 +01:00
# Validate storage space for auto-picked container storage
if [[ " $class " == "container" && -n " ${ DISK_SIZE :- } " ]] ; then
validate_storage_space " $STORAGE_RESULT " " $DISK_SIZE " "yes"
# Continue even if validation fails - user was warned
fi
2025-12-04 07:52:18 +01:00
else
# If the current value is preselectable, we could show it, but per your requirement we always offer selection
2026-03-26 16:07:38 +01:00
select_storage " $class " || return 150
2025-12-04 07:52:18 +01:00
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
# ==============================================================================
2026-07-27 19:32:09 +02:00
# ------------------------------------------------------------------------------
# Distinct versions of OS type $1 in the cached pveam catalog (no network).
# `pveam available` prints "<section>\t<template>" - the name is field 2.
# Must never fail: callers capture this via mapfile < <(...).
# ------------------------------------------------------------------------------
_list_os_versions() {
local ostype = " ${ 1 :- } "
[[ -n " $ostype " ]] || return 0
2026-08-06 14:33:22 +02:00
local arch
arch = " $( dpkg --print-architecture 2>/dev/null || echo amd64) "
2026-07-27 19:32:09 +02:00
pveam available -section system 2>/dev/null |
2026-08-06 21:03:28 +02:00
grep -E "_ ${ arch } \.(tar\.zst|tar\.xz|tar\.gz)([[:space:]]| $)" |
2026-07-27 19:32:09 +02:00
awk '{print $2}' |
sed -nE "s/^ ${ ostype } -([0-9]+(\.[0-9]+)?).*/\1/p" |
sort -u -V || true
}
# ------------------------------------------------------------------------------
# Catalog templates matching prefix $1 and variant $2 ("debian-13" +
# "-standard_"), oldest first. Same no-fail contract as _list_os_versions.
# ------------------------------------------------------------------------------
_list_templates() {
local search = " ${ 1 :- } " pattern = " ${ 2 :- } "
[[ -n " $search " ]] || return 0
2026-08-06 14:33:22 +02:00
local arch
arch = " $( dpkg --print-architecture 2>/dev/null || echo amd64) "
2026-07-27 19:32:09 +02:00
pveam available -section system 2>/dev/null |
2026-08-06 21:03:28 +02:00
grep -E "_ ${ arch } \.(tar\.zst|tar\.xz|tar\.gz)([[:space:]]| $)" |
2026-07-27 19:32:09 +02:00
awk '{print $2}' |
grep -E "^ ${ search } .* ${ pattern } " |
sort -t - -k 2 -V || true
}
# ------------------------------------------------------------------------------
# Maps var_version onto a version this host's catalog actually has, before the
# settings summary is printed. Availability comes from `pveam available` only -
# never inferred from the PVE release (an updated 8.4 may carry templates an
# older 8.x never saw, and entries drop out again at EOL).
# Picks the nearest version, newer wins on a tie. Interactive: menu with that
# candidate preselected; unattended: takes it with a warning.
# ------------------------------------------------------------------------------
resolve_os_version() {
local ostype = " ${ var_os :- } " want = " ${ var_version :- } "
[[ -n " $ostype " && -n " $want " ]] || return 0
# ARM64 pulls from linuxcontainers.org, the pveam catalog says nothing there
[[ " $( dpkg --print-architecture 2>/dev/null) " == "arm64" ]] && return 0
local -a versions =()
mapfile -t versions < <( _list_os_versions " $ostype " )
[[ ${# versions [@] } -gt 0 ]] || return 0
local v
for v in " ${ versions [@] } " ; do
[[ " $v " == " $want " ]] && return 0
done
# Already downloaded counts as available even if the catalog dropped it
if [[ -n " ${ TEMPLATE_STORAGE :- } " ]] &&
pveam list " $TEMPLATE_STORAGE " 2>/dev/null | grep -q "/ ${ ostype } - ${ want } -" ; then
return 0
fi
local candidate
candidate = $( printf '%s\n' " ${ versions [@] } " | awk -v want = " $want " '
function num(v, a) { split(v, a, "."); return a[1] * 10000 + (a[2] + 0) }
BEGIN { w = num(want); best = ""; bestd = -1 }
{
d = num($0) - w
ad = (d < 0) ? -d : d
if (bestd < 0 || ad < bestd || (ad == bestd && d > 0)) { bestd = ad; best = $0 }
}
END { print best }
' )
[[ -n " $candidate " ]] || return 0
msg_warn " ${ ostype ^ } ${ want } is not in this host's template catalog (PVE ${ PVEVERSION :- unknown } )"
if [[ " ${ PHS_SILENT :- 0 } " != "1" ]] && command -v whiptail & >/dev/null && [ -t 0 ] && [[ " $TERM " != "dumb" ]] ; then
local -a menu =()
while read -r v; do
if [[ " $v " == " $candidate " ]] ; then
menu +=( " $v " "closest to ${ want } (recommended)" )
else
menu +=( " $v " " " )
fi
done < <( printf '%s\n' " ${ versions [@] } " | sort -rV)
local choice
choice = $( whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "OS VERSION NOT AVAILABLE" \
--ok-button "Use" --cancel-button "Exit" \
--menu "\n ${ ostype ^ } ${ want } is not available on this Proxmox host.\n\nSelect the version to use instead:" \
20 70 8 " ${ menu [@] } " --default-item " $candidate " \
3>& 1 1>& 2 2>& 3) || exit_script
candidate = " $choice "
else
msg_warn "Unattended run - continuing with ${ ostype } ${ candidate } "
fi
var_version = " $candidate "
export var_version
msg_ok "Using ${ ostype } ${ var_version } instead of ${ want } "
return 0 # last call in base_settings(), which runs under errexit
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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 } " }
2026-01-18 21:19:53 +01:00
PW = ""
if [[ -n " ${ var_pw :- } " ]] ; then
local _pw_raw = " ${ var_pw } "
case " $_pw_raw " in
--password\ *) _pw_raw = " ${ _pw_raw #--password } " ;;
-password\ *) _pw_raw = " ${ _pw_raw #-password } " ;;
esac
while [[ " $_pw_raw " == -* ]] ; do
_pw_raw = " ${ _pw_raw #- } "
done
if [[ -z " $_pw_raw " ]] ; then
msg_warn "Password was only dashes after cleanup; leaving empty."
else
PW = "--password $_pw_raw "
fi
fi
2026-01-12 18:11:25 +01:00
2026-01-12 14:04:28 +01:00
# Validate and set Container ID
local requested_id = " ${ var_ctid :- $NEXTID } "
if ! validate_container_id " $requested_id " ; then
2026-01-12 18:52:08 +01:00
# Only show warning if user manually specified an ID (not auto-assigned)
if [[ -n " ${ var_ctid :- } " ]] ; then
msg_warn "Container ID $requested_id is already in use. Using next available ID: $( get_valid_container_id " $requested_id " ) "
fi
2026-01-12 14:04:28 +01:00
requested_id = $( get_valid_container_id " $requested_id " )
fi
CT_ID = " $requested_id "
2026-01-12 18:11:25 +01:00
2026-01-19 22:39:57 +01:00
# Validate and set Hostname/FQDN
local requested_hostname = " ${ var_hostname :- $NSAPP } "
requested_hostname = $( echo " ${ requested_hostname ,, } " | tr -d ' ' )
if ! validate_hostname " $requested_hostname " ; then
if [[ -n " ${ var_hostname :- } " ]] ; then
msg_warn "Invalid hostname ' $requested_hostname '. Using default: $NSAPP "
fi
requested_hostname = " $NSAPP "
fi
HN = " $requested_hostname "
2025-09-08 14:03:34 +02:00
BRG = ${ var_brg :- "vmbr0" }
2026-06-24 23:21:12 +02:00
SDN_VNET = ${ var_sdn_vnet :- "" }
2025-09-08 14:03:34 +02:00
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" }
GATE = ${ var_gateway :- "" }
2026-06-15 10:08:24 +02:00
# Guard against invalid gateway combinations from defaults/app vars:
# - DHCP must not force a static gateway
# - Static IPv4 must use a gateway in the same subnet
if [[ " $NET " == "dhcp" && -n " $GATE " ]] ; then
msg_warn "Ignoring var_gateway ' $GATE ' because var_net is 'dhcp'"
GATE = ""
elif [[ " $NET " != "dhcp" && -n " $GATE " ]] ; then
if ! validate_gateway_in_subnet " $NET " " $GATE " ; then
msg_warn "Ignoring var_gateway ' $GATE ' because it is not in subnet of var_net ' $NET '"
GATE = ""
fi
fi
2025-09-08 14:03:34 +02:00
APT_CACHER = ${ var_apt_cacher :- "" }
APT_CACHER_IP = ${ var_apt_cacher_ip :- "" }
2026-07-30 10:31:09 +02:00
INHERIT_HOST_CA = " ${ var_inherit_host_ca :- no } "
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
2026-04-03 21:17:07 +02:00
local _check_host _check_port _check_url
_check_host = $( echo " $APT_CACHER_IP " | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1)
_check_port = $( echo " $APT_CACHER_IP " | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2)
if [[ " $APT_CACHER_IP " = ~ ^https?:// ]] ; then
_check_url = " $APT_CACHER_IP "
_check_port = " ${ _check_port :- 80 } "
else
_check_port = " ${ _check_port :- 3142 } "
_check_url = "http:// ${ APT_CACHER_IP } : ${ _check_port } "
fi
if ! curl -s --connect-timeout 2 " ${ _check_url } " >/dev/null 2>& 1; then
msg_warn "APT Cacher configured but not reachable at ${ _check_url } "
2025-12-04 07:52:18 +01:00
msg_custom "⚠️" " ${ YW } " "Disabling APT Cacher for this installation"
APT_CACHER = ""
APT_CACHER_IP = ""
else
2026-04-03 21:17:07 +02:00
msg_ok "APT Cacher verified at ${ _check_url } "
2025-12-04 07:52:18 +01:00
fi
fi
2026-06-24 06:58:32 +02:00
HTTP_PROXY = " ${ var_http_proxy :- } "
HTTP_NO_PROXY = " ${ var_http_no_proxy :- } "
2025-09-08 14:03:34 +02:00
MTU = ${ var_mtu :- "" }
2026-03-21 21:13:15 +01:00
_sd_val = " ${ var_searchdomain :- "" } "
[[ -n " $_sd_val " ]] && SD = "-searchdomain= $_sd_val " || SD = ""
_ns_val = " ${ var_ns :- "" } "
[[ -n " $_ns_val " ]] && NS = "-nameserver= $_ns_val " || NS = ""
2025-09-08 14:03:34 +02:00
MAC = ${ var_mac :- "" }
VLAN = ${ var_vlan :- "" }
SSH = ${ var_ssh :- "no" }
SSH_AUTHORIZED_KEY = ${ var_ssh_authorized_key :- "" }
2026-04-17 15:19:52 +02:00
# Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates
if [[ " ${ var_tags :- } " == *community-script* ]] ; then
TAGS = " ${ var_tags :- community -script } "
else
TAGS = "community-script ${ var_tags :+; ${ var_tags }} "
fi
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" }
2026-04-16 22:23:15 +02:00
ALLOW_MOUNT_FS = ${ var_mount_fs :- "" }
2025-12-15 13:02:28 +01:00
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
2026-07-27 19:32:09 +02:00
var_version = "13"
2024-12-16 12:41:51 +01:00
fi
2026-07-27 19:32:09 +02:00
# every install path passes here, and still before any summary is printed
resolve_os_version
2024-12-16 12:41:51 +01:00
}
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)
2026-08-24 07:44:47 +02:00
# - Optional protected list preserves genuinely user-exported var_* values
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
load_vars_file() {
local file = " $1 "
2026-08-24 07:44:47 +02:00
local force = " ${ 2 :- no } " # If "yes", override existing variables
local protected = " ${ 3 :- } " # space-separated var_* keys the user genuinely exported before this file loaded; never overwritten
2025-12-04 07:52:18 +01:00
[ -f " $file " ] || return 0
msg_info "Loading defaults from ${ file } "
# Allowed var_* keys
local VAR_WHITELIST =(
2026-07-30 10:31:09 +02:00
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_http_no_proxy var_http_proxy var_inherit_host_ca 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
2026-03-05 15:28:07 +01:00
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
2026-06-24 23:21:12 +02:00
var_post_install var_sdn_vnet
2025-12-04 07:52:18 +01:00
)
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
}
2026-08-24 07:44:47 +02:00
# Protected check helper (genuinely user-exported vars, see $protected above)
_is_protected() {
local k = " $1 " p
for p in $protected ; do [ " $k " = " $p " ] && return 0; done
return 1
}
2025-12-04 07:52:18 +01:00
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 :]] } " } "
2026-01-20 15:03:14 +01:00
# Validate values before setting (skip empty values - they use defaults)
if [[ -n " $var_val " ]] ; then
case " $var_key " in
2026-02-14 13:41:09 +01:00
var_mac)
if ! validate_mac_address " $var_val " ; then
msg_warn "Invalid MAC address ' $var_val ' in $file , ignoring"
continue
fi
;;
var_vlan)
if ! validate_vlan_tag " $var_val " ; then
msg_warn "Invalid VLAN tag ' $var_val ' in $file (must be 1-4094), ignoring"
continue
fi
;;
var_mtu)
if ! validate_mtu " $var_val " ; then
msg_warn "Invalid MTU ' $var_val ' in $file (must be 576-65535), ignoring"
continue
fi
;;
var_tags)
if ! validate_tags " $var_val " ; then
msg_warn "Invalid tags ' $var_val ' in $file (alphanumeric, -, _, ; only), ignoring"
continue
fi
;;
var_timezone)
if ! validate_timezone " $var_val " ; then
msg_warn "Invalid timezone ' $var_val ' in $file , ignoring"
continue
fi
;;
var_brg)
if ! validate_bridge " $var_val " ; then
msg_warn "Bridge ' $var_val ' not found in $file , ignoring"
continue
fi
;;
var_gateway)
if ! validate_gateway_ip " $var_val " ; then
msg_warn "Invalid gateway IP ' $var_val ' in $file , ignoring"
continue
fi
;;
var_hostname)
if ! validate_hostname " $var_val " ; then
msg_warn "Invalid hostname ' $var_val ' in $file , ignoring"
continue
fi
;;
var_cpu)
if ! [[ " $var_val " = ~ ^[ 0-9] +$ ]] || (( var_val < 1 || var_val > 128)) ; then
msg_warn "Invalid CPU count ' $var_val ' in $file (must be 1-128), ignoring"
continue
fi
;;
var_ram)
if ! [[ " $var_val " = ~ ^[ 0-9] +$ ]] || (( var_val < 256)) ; then
msg_warn "Invalid RAM ' $var_val ' in $file (must be >= 256 MiB), ignoring"
continue
fi
;;
var_disk)
if ! [[ " $var_val " = ~ ^[ 0-9] +$ ]] || (( var_val < 1)) ; then
msg_warn "Invalid disk size ' $var_val ' in $file (must be >= 1 GB), ignoring"
continue
fi
;;
var_unprivileged)
if [[ " $var_val " != "0" && " $var_val " != "1" ]] ; then
msg_warn "Invalid unprivileged value ' $var_val ' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_nesting)
if [[ " $var_val " != "0" && " $var_val " != "1" ]] ; then
msg_warn "Invalid nesting value ' $var_val ' in $file (must be 0 or 1), ignoring"
continue
fi
# Warn about potential issues with systemd-based OS when nesting is disabled via vars file
if [[ " $var_val " == "0" && " ${ var_os :- debian } " != "alpine" ]] ; then
msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
fi
;;
var_keyctl)
if [[ " $var_val " != "0" && " $var_val " != "1" ]] ; then
msg_warn "Invalid keyctl value ' $var_val ' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_net)
# var_net can be: dhcp, static IP/CIDR, or IP range
if [[ " $var_val " != "dhcp" ]] ; then
if is_ip_range " $var_val " ; then
: # IP range is valid, will be resolved at runtime
elif ! validate_ip_address " $var_val " ; then
msg_warn "Invalid network ' $var_val ' in $file (must be dhcp or IP/CIDR), ignoring"
2026-01-20 15:03:14 +01:00
continue
fi
2026-02-14 13:41:09 +01:00
fi
;;
var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection)
if [[ " $var_val " != "yes" && " $var_val " != "no" ]] ; then
msg_warn "Invalid boolean ' $var_val ' for $var_key in $file (must be yes/no), ignoring"
continue
fi
;;
2026-04-16 22:23:15 +02:00
var_mknod)
if [[ " $var_val " != "0" && " $var_val " != "1" ]] ; then
msg_warn "Invalid mknod value ' $var_val ' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_mount_fs)
2026-04-17 13:04:52 +02:00
# Normalize: strip spaces, trailing commas
var_val = " ${ var_val // / } "
var_val = " ${ var_val %%, } "
var_val = " ${ var_val ##, } "
if [[ -n " $var_val " ]] && [[ ! " $var_val " = ~ ^[ a-zA-Z0-9] +( ,[ a-zA-Z0-9] +) *$ ]] ; then
2026-04-16 22:23:15 +02:00
msg_warn "Invalid mount_fs value ' $var_val ' in $file (comma-separated fs names only, e.g. nfs,cifs), ignoring"
continue
fi
;;
2026-02-14 13:41:09 +01:00
var_ipv6_method)
if [[ " $var_val " != "auto" && " $var_val " != "dhcp" && " $var_val " != "static" && " $var_val " != "none" ]] ; then
msg_warn "Invalid IPv6 method ' $var_val ' in $file (must be auto/dhcp/static/none), ignoring"
continue
fi
2026-03-12 09:17:18 +01:00
;;
2026-04-03 21:17:07 +02:00
var_apt_cacher_ip)
# Allow: plain IP/hostname, http://host, https://host:port
if [[ -n " $var_val " ]] && ! [[ " $var_val " = ~ ^( https?://) ?[ a-zA-Z0-9._-] +( :[ 0-9] +) ?( /.*) ?$ ]] ; then
msg_warn "Invalid APT Cacher address ' $var_val ' in $file , ignoring"
continue
fi
;;
2026-06-24 23:21:12 +02:00
var_sdn_vnet)
if [[ -n " $var_val " ]] && ! validate_sdn_vnet " $var_val " ; then
msg_warn "SDN vnet ' $var_val ' from $file not found, ignoring"
2026-06-24 23:44:26 +02:00
continue
fi
;;
2026-06-24 06:58:32 +02:00
var_http_proxy)
if [[ -n " $var_val " ]] && ! [[ " $var_val " = ~ ^https?://[ ^[ :space:]] +( :[ 0-9] +) ?/?$ ]] ; then
msg_warn "Invalid HTTP proxy URL ' $var_val ' in $file , ignoring"
continue
fi
;;
var_http_no_proxy)
if [[ -n " $var_val " ]] && [[ ! " $var_val " = ~ ^[ a-zA-Z0-9.*_:/-] +( ,[ a-zA-Z0-9.*_:/-] +) *$ ]] ; then
msg_warn "Invalid no_proxy value ' $var_val ' in $file , ignoring"
continue
fi
;;
2026-07-30 10:31:09 +02:00
var_inherit_host_ca)
if [[ " $var_val " != "yes" && " $var_val " != "no" && " $var_val " != "auto" ]] ; then
msg_warn "Invalid host CA inheritance value ' $var_val ' in $file (must be yes/no/auto), ignoring"
continue
fi
;;
2026-03-12 09:17:18 +01:00
var_container_storage | var_template_storage)
# Validate that the storage exists and is active on the current node
local _storage_status
_storage_status = $( pvesm status 2>/dev/null | awk -v s = " $var_val " '$1 == s { print $3 }' )
if [[ -z " $_storage_status " ]] ; then
msg_warn "Storage ' $var_val ' from $file not found on this node, ignoring"
continue
elif [[ " $_storage_status " == "disabled" ]] ; then
msg_warn "Storage ' $var_val ' from $file is disabled on this node, ignoring"
continue
fi
2026-02-14 13:41:09 +01:00
;;
2026-01-20 15:03:14 +01:00
esac
fi
2026-08-24 07:44:47 +02:00
# Set variable: force mode overrides existing, otherwise only set if empty.
# Exception: var_cpu/var_ram/var_disk are always applied here (unless the
# user genuinely exported them beforehand, per $protected) even though the
# app script already declared its own baseline for them - base_settings()
# reconciles the final floor against APP_DEFAULT_* afterward, so this file
# must be allowed to raise them instead of being silently blocked by the
# app's own pre-set value.
2025-12-15 09:46:49 +01:00
if [[ " $force " == "yes" ]] ; then
export " ${ var_key } = ${ var_val } "
2026-08-24 07:44:47 +02:00
elif _is_protected " $var_key " ; then
:
elif [[ " $var_key " == "var_cpu" || " $var_key " == "var_ram" || " $var_key " == "var_disk" ]] ; then
export " ${ var_key } = ${ var_val } "
2025-12-15 09:46:49 +01:00
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 =(
2026-07-30 10:31:09 +02:00
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_http_no_proxy var_http_proxy var_inherit_host_ca 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
2026-03-05 15:28:07 +01:00
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
2026-06-24 23:21:12 +02:00
var_post_install var_sdn_vnet
2025-12-04 07:52:18 +01:00
)
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
2026-03-26 16:07:38 +01:00
return 252
2025-12-04 07:52:18 +01:00
}
# 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=
2026-04-03 21:17:07 +02:00
# APT cacher (optional - IP or URL)
2025-12-04 07:52:18 +01:00
# var_apt_cacher=yes
# var_apt_cacher_ip=192.168.1.10
2026-04-03 21:17:07 +02:00
# var_apt_cacher_ip=http://proxy.local
# var_apt_cacher_ip=https://proxy.local:443
2025-12-04 07:52:18 +01:00
2026-06-24 06:58:32 +02:00
# HTTP/HTTPS proxy (optional - for networks requiring a proxy)
# var_http_proxy=http://proxy.local:8080
# var_http_no_proxy=localhost,127.0.0.1,.local
2026-07-30 10:31:09 +02:00
# var_inherit_host_ca=no
2026-06-24 06:58:32 +02:00
2025-12-04 07:52:18 +01:00
# 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=
2026-04-10 11:28:52 +02:00
# GitHub Personal Access Token (optional – avoids API rate limits during installs)
# Create at https://github.com/settings/tokens – read-only public access is sufficient
# var_github_token=ghp_your_token_here
2026-05-11 15:54:38 +02:00
# Optional post-install script (host-side path to a *.sh on the Proxmox host)
# Runs ON THE HOST after the container is fully provisioned.
# Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG
# var_post_install=/opt/post-install/myhook.sh
2025-12-04 07:52:18 +01:00
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"
2026-03-26 16:07:38 +01:00
return 252
2025-12-04 07:52:18 +01:00
}
2026-08-24 07:44:47 +02:00
load_vars_file " $dv " "no" " ${ !_HARD_ENV[*] } "
2025-12-04 07:52:18 +01:00
# 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
2026-04-10 11:28:52 +02:00
# 4) Map var_github_token → GITHUB_TOKEN (only if not already set in environment)
if [[ -z " ${ GITHUB_TOKEN :- } " && -n " ${ var_github_token :- } " ]] ; then
export GITHUB_TOKEN = " ${ var_github_token } "
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 =(
2026-07-30 10:31:09 +02:00
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_http_no_proxy var_http_proxy var_inherit_host_ca var_keyctl
2026-04-16 22:23:15 +02:00
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
2026-06-24 23:21:12 +02:00
var_post_install var_sdn_vnet
2025-12-04 07:52:18 +01:00
)
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 :- } "
2026-06-24 06:58:32 +02:00
_http_proxy = " ${ HTTP_PROXY :- ${ var_http_proxy :- }} "
_http_no_proxy = " ${ HTTP_NO_PROXY :- ${ var_http_no_proxy :- }} "
2026-07-30 10:31:09 +02:00
_inherit_host_ca = " ${ INHERIT_HOST_CA :- ${ var_inherit_host_ca :- no }} "
2025-12-04 07:52:18 +01:00
_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
2026-03-05 15:28:07 +01:00
echo "var_os= $( _sanitize_value " ${ var_os :- } " ) "
echo "var_version= $( _sanitize_value " ${ var_version :- } " ) "
2025-12-04 07:52:18 +01:00
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 " ) "
2026-06-24 06:58:32 +02:00
[ -n " $_http_proxy " ] && echo "var_http_proxy= $( _sanitize_value " $_http_proxy " ) "
[ -n " $_http_no_proxy " ] && echo "var_http_no_proxy= $( _sanitize_value " $_http_no_proxy " ) "
2026-07-30 10:31:09 +02:00
[ -n " $_inherit_host_ca " ] && echo "var_inherit_host_ca= $( _sanitize_value " $_inherit_host_ca " ) "
2025-12-04 07:52:18 +01:00
[ -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 " ) "
2026-06-24 23:21:12 +02:00
[ -n " ${ var_sdn_vnet :- } " ] && echo "var_sdn_vnet= $( _sanitize_value " ${ var_sdn_vnet } " ) "
2025-12-04 07:52:18 +01:00
[ -n " $_tpl_storage " ] && echo "var_template_storage= $( _sanitize_value " $_tpl_storage " ) "
[ -n " $_ct_storage " ] && echo "var_container_storage= $( _sanitize_value " $_ct_storage " ) "
2026-05-11 15:54:38 +02:00
[ -n " ${ var_post_install :- } " ] && echo "var_post_install= $( _sanitize_value " ${ var_post_install } " ) "
2025-12-04 07:52:18 +01:00
} >" $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 " ) "
2026-03-26 10:11:47 +01:00
/usr/bin/install -m 0644 " $new_tmp " " $app_vars_path "
2025-12-04 07:52:18 +01:00
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" )
2026-03-26 10:11:47 +01:00
/usr/bin/install -m 0644 " $new_tmp " " $app_vars_path "
2025-12-04 07:52:18 +01:00
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
2026-03-25 13:06:21 +01:00
tpl = $( grep -E '^var_template_storage=' " $vf " | cut -d= -f2- || true )
ct = $( grep -E '^var_container_storage=' " $vf " | cut -d= -f2- || true )
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 "
2026-01-12 18:12:55 +01:00
# Validate storage space for loaded container storage
if [[ -n " ${ DISK_SIZE :- } " ]] ; then
validate_storage_space " $ct " " $DISK_SIZE " "yes"
# Continue even if validation fails - user was warned
fi
2025-12-04 07:52:18 +01:00
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
2026-04-17 15:19:52 +02:00
# Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates
if [[ " ${ var_tags :- } " == *community-script* ]] ; then
TAGS = " ${ var_tags :- community -script } "
else
TAGS = "community-script ${ var_tags :+; ${ var_tags }} "
fi
2025-12-04 07:52:18 +01:00
local STEP = 1
2026-07-30 10:31:09 +02:00
local MAX_STEP = 31
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 } "
2026-06-24 23:21:12 +02:00
local _sdn_vnet = " ${ var_sdn_vnet :- } "
2025-12-09 16:01:31 +01:00
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 :- } "
2026-06-24 06:58:32 +02:00
local _http_proxy = " ${ var_http_proxy :- } "
local _http_no_proxy = " ${ var_http_no_proxy :- } "
2026-07-30 10:31:09 +02:00
local _inherit_host_ca = " ${ var_inherit_host_ca :- no } "
2025-12-09 16:01:31 +01:00
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 } "
2026-05-11 15:54:38 +02:00
local _post_install = " ${ var_post_install :- } "
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
2026-03-25 13:06:21 +01:00
local description = $( grep -A 10 "iface $bridge " /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//' || true )
2025-12-04 07:52:18 +01:00
BRIDGE_MENU_OPTIONS +=( " $bridge " " ${ description :- } " )
fi
done <<< " $BRIDGES "
2025-05-15 10:31:37 +02:00
fi
2026-06-24 23:21:12 +02:00
if [[ -f /etc/pve/sdn/vnets.cfg ]] ; then
while IFS = read -r vnet; do
[[ -n " $vnet " ]] && BRIDGE_MENU_OPTIONS +=( "sdn: ${ vnet } " "[SDN] ${ vnet } " )
done < <( awk '/^vnet:/{print $2}' /etc/pve/sdn/vnets.cfg 2>/dev/null)
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
2026-06-11 16:14:32 +10:00
# ═══════════════════════════════════════════════════════════════════════════
2025-12-04 07:52:18 +01:00
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
2025-06-30 12:11:44 +02:00
else
2026-01-18 21:19:53 +01:00
local _pw1_clean = " $PW1 "
while [[ " $_pw1_clean " == -* ]] ; do
_pw1_clean = " ${ _pw1_clean #- } "
done
if [[ -z " $_pw1_clean " ]] ; then
whiptail --msgbox "Password cannot be only '-' characters." 8 58
continue
elif (( ${# _pw1_clean } < 5)) ; then
whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70
continue
fi
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
2026-01-18 21:19:53 +01:00
local _pw2_clean = " $PW2 "
while [[ " $_pw2_clean " == -* ]] ; do
_pw2_clean = " ${ _pw2_clean #- } "
done
if [[ " $_pw1_clean " == " $_pw2_clean " ]] ; then
_pw = "--password $_pw1_clean "
2025-12-04 07:52:18 +01:00
_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
2026-01-12 14:04:28 +01:00
local input_id = " ${ result :- $NEXTID } "
# Validate that ID is numeric
if ! [[ " $input_id " = ~ ^[ 0-9] +$ ]] ; then
whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58
continue
fi
# Check if ID is already in use
if ! validate_container_id " $input_id " ; then
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \
--yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ( $( get_valid_container_id " $input_id " ) )?" 10 58; then
_ct_id = $( get_valid_container_id " $input_id " )
else
continue
fi
else
_ct_id = " $input_id "
fi
2025-12-04 07:52:18 +01:00
(( 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" \
2026-01-19 22:39:57 +01:00
--inputbox "\nSet Hostname (or FQDN, e.g. host.example.com)" 10 58 " $_hostname " \
2025-12-04 07:52:18 +01:00
3>& 1 1>& 2 2>& 3) ; then
local hn_test = " ${ result :- $NSAPP } "
hn_test = $( echo " ${ hn_test ,, } " | tr -d ' ' )
2026-01-19 22:39:57 +01:00
if validate_hostname " $hn_test " ; then
2025-12-04 07:52:18 +01:00
_hostname = " $hn_test "
(( STEP++))
2025-06-30 12:11:44 +02:00
else
2026-01-19 22:39:57 +01:00
whiptail --msgbox "Invalid hostname: ' $hn_test '\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60
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
2026-01-20 15:03:14 +01:00
# Validate default bridge exists
if validate_bridge "vmbr0" ; then
_bridge = "vmbr0"
(( STEP++))
else
whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58
2026-02-27 13:59:02 +01:00
msg_error "Default bridge 'vmbr0' not found"
2026-03-02 10:55:20 +01:00
exit 116
2026-01-20 15:03:14 +01:00
fi
2025-12-04 07:52:18 +01:00
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
2026-01-20 15:03:14 +01:00
local bridge_test = " ${ result :- vmbr0 } "
2026-01-28 17:35:24 +01:00
# Skip separator entries (e.g., __other__) - re-display menu
if [[ " $bridge_test " == "__other__" || " $bridge_test " == -* ]] ; then
continue
fi
2026-06-24 23:21:12 +02:00
if [[ " $bridge_test " == sdn:* ]] ; then
local vnet_test = " ${ bridge_test #sdn: } "
if validate_sdn_vnet " $vnet_test " ; then
_sdn_vnet = " $vnet_test "
_bridge = " ${ var_brg :- vmbr0 } "
(( STEP++))
else
whiptail --msgbox "SDN vnet ' $vnet_test ' is not configured on this cluster." 8 58
fi
elif validate_bridge " $bridge_test " ; then
2026-01-20 15:03:14 +01:00
_bridge = " $bridge_test "
2026-06-24 23:21:12 +02:00
_sdn_vnet = ""
2026-01-20 15:03:14 +01:00
(( STEP++))
else
whiptail --msgbox "Bridge ' $bridge_test ' is not available or not active." 8 58
fi
2025-12-04 07:52:18 +01:00
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
2026-01-20 15:03:14 +01:00
if validate_ip_address " $static_ip " ; then
2025-12-04 07:52:18 +01:00
# 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
2026-01-20 15:03:14 +01:00
if validate_gateway_ip " $gateway_ip " ; then
# Validate gateway is in same subnet
if validate_gateway_in_subnet " $static_ip " " $gateway_ip " ; then
_net = " $static_ip "
_gate = ",gw= $gateway_ip "
(( STEP++))
else
whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip \nGateway: $gateway_ip " 10 58
fi
2025-12-04 07:52:18 +01:00
else
2026-01-20 15:03:14 +01:00
whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
2025-12-04 07:52:18 +01:00
fi
fi
else
2026-01-20 15:03:14 +01:00
whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58
2025-12-04 07:52:18 +01:00
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
2026-01-20 15:03:14 +01:00
if validate_gateway_ip " $gateway_ip " ; then
# Validate gateway is in same subnet
if validate_gateway_in_subnet " $NET_RESOLVED " " $gateway_ip " ; then
_net = " $NET_RESOLVED "
_gate = ",gw= $gateway_ip "
(( STEP++))
else
whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED \nGateway: $gateway_ip " 10 58
fi
2025-12-16 16:52:37 +01:00
else
2026-01-20 15:03:14 +01:00
whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
2025-12-16 16:52:37 +01:00
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
2026-01-20 15:03:14 +01:00
if validate_ipv6_address " $ipv6_addr " ; then
2025-12-04 07:52:18 +01:00
_ipv6_addr = " $ipv6_addr "
2026-01-20 15:03:14 +01:00
# Optional gateway - loop until valid or empty
local ipv6_gw_valid = false
while [[ " $ipv6_gw_valid " == "false" ]] ; do
local ipv6_gw
ipv6_gw = $( 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
# Validate gateway if provided
if [[ -n " $ipv6_gw " ]] ; then
if validate_ipv6_address " $ipv6_gw " ; then
_ipv6_gate = " $ipv6_gw "
ipv6_gw_valid = true
(( STEP++))
else
whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58
fi
else
_ipv6_gate = ""
ipv6_gw_valid = true
(( STEP++))
fi
done
2025-12-04 07:52:18 +01:00
else
2026-01-20 15:03:14 +01:00
whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58
2025-12-04 07:52:18 +01:00
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" \
2026-01-20 15:03:14 +01:00
--inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \
2025-12-04 07:52:18 +01:00
3>& 1 1>& 2 2>& 3) ; then
2026-01-20 15:03:14 +01:00
if validate_mtu " $result " ; then
_mtu = " $result "
(( STEP++))
else
whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58
fi
2025-12-04 07:52:18 +01:00
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" \
2026-01-20 15:03:14 +01:00
--inputbox "\nSet MAC Address\n(leave blank for auto-generated, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \
2025-12-04 07:52:18 +01:00
3>& 1 1>& 2 2>& 3) ; then
2026-01-20 15:03:14 +01:00
if validate_mac_address " $result " ; then
_mac = " $result "
(( STEP++))
else
whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58
fi
2025-12-04 07:52:18 +01:00
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" \
2026-01-20 15:03:14 +01:00
--inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \
2025-12-04 07:52:18 +01:00
3>& 1 1>& 2 2>& 3) ; then
2026-01-20 15:03:14 +01:00
if validate_vlan_tag " $result " ; then
_vlan = " $result "
(( STEP++))
else
whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58
fi
2025-12-04 07:52:18 +01:00
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" \
2026-01-20 15:03:14 +01:00
--inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 " $_tags " \
2025-12-04 07:52:18 +01:00
3>& 1 1>& 2 2>& 3) ; then
2026-01-20 15:03:14 +01:00
local tags_test = " ${ result :- } "
tags_test = $( echo " $tags_test " | tr -d '[:space:]' )
if validate_tags " $tags_test " ; then
_tags = " $tags_test "
(( STEP++))
else
whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58
fi
2025-12-04 07:52:18 +01:00
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"
2026-01-26 22:21:09 +01:00
# Warn about potential issues with systemd-based OS when nesting is disabled
if [[ " $var_os " != "alpine" ]] ; then
whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "⚠️ NESTING WARNING" \
--msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68
fi
2025-12-09 16:01:31 +01:00
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)
2026-04-17 15:19:52 +02:00
# Keyctl is always required for unprivileged containers — skip dialog
if [[ " $_ct_type " == "1" ]] ; then
_enable_keyctl = "1"
(( STEP++))
continue
fi
2025-12-09 16:01:31 +01:00
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 \
2026-04-17 15:19:52 +02:00
--yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\n(App default: ${ var_keyctl :- 0 } )" 14 62; then
2025-12-09 16:01:31 +01:00
_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" \
2026-04-03 21:17:07 +02:00
--inputbox "\nEnter APT Cacher-NG IP or URL:\n(e.g. 192.168.1.10, http://host, https://host:443)" 12 62 " $_apt_cacher_ip " \
2025-12-09 16:01:31 +01:00
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++))
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-06-24 06:58:32 +02:00
# STEP 24: HTTP/HTTPS Proxy
2025-12-09 16:01:31 +01:00
# ═══════════════════════════════════════════════════════════════════════════
24)
2026-06-24 06:58:32 +02:00
local http_proxy_default_flag = "--defaultno"
[[ -n " $_http_proxy " ]] && http_proxy_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "HTTP/HTTPS PROXY" \
--ok-button "Next" --cancel-button "Back" \
$http_proxy_default_flag \
--yesno "\nUse HTTP/HTTPS proxy?\n\nRequired when internet access must go through a proxy server.\n(e.g. corporate networks)\n\n(App default: ${ var_http_proxy :- none } )" 14 62; then
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "HTTP PROXY URL" \
--inputbox "\nEnter HTTP proxy URL:\n(e.g. http://proxy.local:8080)" 12 62 " $_http_proxy " \
3>& 1 1>& 2 2>& 3) ; then
_http_proxy = " $result "
local _no_proxy_default = " ${ _http_no_proxy :- localhost ,127.0.0.1,.local } "
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "NO_PROXY EXCEPTIONS" \
--inputbox "\nEnter NO_PROXY exceptions (comma-separated):\nLeave empty for defaults." 12 62 " $_no_proxy_default " \
3>& 1 1>& 2 2>& 3) ; then
_http_no_proxy = " $result "
fi
fi
else
if [ $? -eq 1 ] ; then
_http_proxy = ""
_http_no_proxy = ""
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
# STEP 25: Host CA Inheritance
2026-06-24 06:58:32 +02:00
# ═══════════════════════════════════════════════════════════════════════════
25)
2026-07-30 10:31:09 +02:00
local host_ca_count = 0
local host_ca_dir = "/usr/local/share/ca-certificates"
local cert
shopt -s nullglob
for cert in " $host_ca_dir " /*.crt; do
host_ca_count = $(( host_ca_count + 1 ))
done
shopt -u nullglob
if [[ $host_ca_count -eq 0 ]] ; then
_inherit_host_ca = "no"
(( STEP++))
continue
fi
local host_ca_default_flag = "--defaultno"
[[ " $_inherit_host_ca " == "yes" ]] && host_ca_default_flag = ""
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "HOST CA INHERITANCE" \
--ok-button "Next" --cancel-button "Back" \
$host_ca_default_flag \
--yesno "\nInherit host CA certificates into this container?\n\nDetected on host: ${ host_ca_count } certificate(s) in:\n ${ host_ca_dir } \n\nRecommended for private PKI / TLS-inspection environments.\n\n(App default: ${ var_inherit_host_ca :- no } )" 16 72; then
_inherit_host_ca = "yes"
else
if [ $? -eq 1 ] ; then
_inherit_host_ca = "no"
else
(( STEP--))
continue
fi
fi
(( STEP++))
;;
# ═══════════════════════════════════════════════════════════════════════════
# STEP 26: Container Timezone
# ═══════════════════════════════════════════════════════════════════════════
26)
2025-12-09 16:01:31 +01:00
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
2026-01-20 15:03:14 +01:00
local tz_test = " $result "
[[ " ${ tz_test :- } " == Etc/* ]] && tz_test = "host" # pct doesn't accept Etc/* zones
if validate_timezone " $tz_test " ; then
_ct_timezone = " $tz_test "
(( STEP++))
else
whiptail --msgbox "Invalid timezone: ' $result '\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58
fi
2025-12-09 16:01:31 +01:00
else
(( STEP--))
fi
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
# STEP 27: Container Protection
2025-12-09 16:01:31 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
27)
2025-12-09 16:01:31 +01:00
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++))
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
# STEP 28: Device Node Creation (mknod)
2025-12-09 16:01:31 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
28)
2025-12-09 16:01:31 +01:00
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++))
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
# STEP 29: Mount Filesystems
2025-12-09 16:01:31 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
29)
2025-12-09 16:01:31 +01:00
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
2026-04-17 13:04:52 +02:00
# Normalize: strip spaces and trailing/leading commas
result = " ${ result // / } "
result = " ${ result %%, } "
result = " ${ result ##, } "
2025-12-09 16:01:31 +01:00
_mount_fs = " $result "
(( STEP++))
else
(( STEP--))
fi
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
# STEP 30: Optional host-side post-install hook (path on the Proxmox HOST)
2025-12-09 16:01:31 +01:00
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
30)
2026-05-11 15:54:38 +02:00
local _hook_prompt = "Optional: absolute path to a *.sh file ON THE PROXMOX HOST.
It runs as root on the HOST (NOT in the LXC) after the container
is fully provisioned and started.
Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG.
Leave empty to skip."
while true; do
if result = $( whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP / $MAX_STEP ]" \
--title "POST-INSTALL HOOK (HOST)" \
--ok-button "Next" --cancel-button "Back" \
--inputbox " $_hook_prompt " 16 70 " ${ _post_install } " \
3>& 1 1>& 2 2>& 3) ; then
# Normalize: strip surrounding whitespace
result = " $( printf '%s' " $result " | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) "
if [[ -z " $result " ]] ; then
_post_install = ""
(( STEP++))
break
fi
# Reject obvious shell-meta sneaking through
if [[ " $result " == *';' * || " $result " == *'$(' * || " $result " == *'`' * || " $result " == *'&&' * || " $result " == *'||' * ]] ; then
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \
--msgbox "Path contains shell metacharacters. Please provide a plain absolute file path." 10 70
continue
fi
if [[ " $result " != /* ]] ; then
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \
--msgbox "Path must be absolute (start with /).\n\nGot: $result " 10 70
continue
fi
if [[ ! -f " $result " ]] ; then
if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "FILE NOT FOUND" \
--yesno "File does not exist on host:\n\n $result \n\nKeep this path anyway?" 12 70; then
continue
fi
fi
_post_install = " $result "
(( STEP++))
break
else
(( STEP--))
break
fi
done
;;
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
# STEP 31: Verbose Mode & Confirmation
2026-05-11 15:54:38 +02:00
# ═══════════════════════════════════════════════════════════════════════════
2026-07-30 10:31:09 +02:00
31)
2026-04-23 15:30:38 +02:00
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
2025-12-09 16:01:31 +01:00
_verbose = "yes"
else
2026-04-23 15:30:38 +02:00
_verbose = "no"
2025-12-09 16:01:31 +01:00
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 "
2026-06-24 06:58:32 +02:00
local http_proxy_display = " ${ _http_proxy :- (none) } "
2026-07-30 10:31:09 +02:00
local inherit_ca_display = " ${ _inherit_host_ca :- no } "
2025-12-09 16:01:31 +01:00
2026-05-11 15:54:38 +02:00
local post_install_display = " ${ _post_install :- (none) } "
local post_install_warn = ""
[[ -n " $_post_install " ]] && post_install_warn = "
⚠ Hook runs as root on Proxmox HOST (not in LXC)"
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
2026-04-16 22:23:15 +02:00
Mknod: $( [ " $_enable_mknod " == "1" ] && echo Enabled || echo Disabled) | Mount FS: ${ _mount_fs :- (none) }
2025-12-09 16:01:31 +01:00
GPU: $_enable_gpu | Protection: $protect_desc
Advanced:
Timezone: $tz_display
APT Cacher: $apt_display
2026-06-24 06:58:32 +02:00
HTTP Proxy: $http_proxy_display
2026-07-30 10:31:09 +02:00
Inherit Host CAs: $inherit_ca_display
2026-05-11 15:54:38 +02:00
Verbose: $_verbose
Post-Install Script: ${ post_install_display }${ post_install_warn } "
2025-12-04 07:52:18 +01:00
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 "
2026-06-24 06:58:32 +02:00
HTTP_PROXY = " $_http_proxy "
HTTP_NO_PROXY = " $_http_no_proxy "
2026-07-30 10:31:09 +02:00
INHERIT_HOST_CA = " $_inherit_host_ca "
2025-12-04 07:52:18 +01:00
VERBOSE = " $_verbose "
2026-05-11 15:54:38 +02:00
var_post_install = " $_post_install "
2025-12-04 07:52:18 +01:00
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 "
2026-06-24 23:21:12 +02:00
var_sdn_vnet = " $_sdn_vnet "
2026-06-24 06:58:32 +02:00
var_http_proxy = " $_http_proxy "
var_http_no_proxy = " $_http_no_proxy "
2026-07-30 10:31:09 +02:00
var_inherit_host_ca = " $_inherit_host_ca "
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 = ""
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 } "
2026-04-16 22:23:15 +02:00
[[ " ${ ENABLE_MKNOD :- 0 } " == "1" ]] && echo -e " ${ CONTAINERTYPE }${ BOLD }${ DGN } Mknod: ${ BGN } Enabled ${ CL } "
[[ -n " ${ ALLOW_MOUNT_FS :- } " ]] && echo -e " ${ CONTAINERTYPE }${ BOLD }${ DGN } Mount FS: ${ BGN }${ ALLOW_MOUNT_FS }${ CL } "
2025-12-15 13:02:28 +01:00
[[ " ${ 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 } "
2026-06-24 06:58:32 +02:00
[[ -n " ${ HTTP_PROXY :- } " ]] && echo -e " ${ INFO }${ BOLD }${ DGN } HTTP Proxy: ${ BGN } $HTTP_PROXY ${ CL } "
2025-12-04 07:52:18 +01:00
echo -e " ${ SEARCH }${ BOLD }${ DGN } Verbose Mode: ${ BGN } $VERBOSE ${ CL } "
2026-02-06 14:55:35 -05:00
echo -e " ${ CREATING }${ BOLD }${ RD } Creating an LXC of ${ APP } using the above advanced settings ${ CL } "
2026-02-14 13:41:09 +01:00
# Log settings to file
log_section "CONTAINER SETTINGS (ADVANCED) - ${ APP } "
log_msg "Application: ${ APP } "
log_msg "PVE Version: ${ PVEVERSION } (Kernel: ${ KERNEL_VERSION } )"
log_msg "Operating System: $var_os ( $var_version )"
log_msg "Container Type: $( [ " $CT_TYPE " == "1" ] && echo "Unprivileged" || echo "Privileged" ) "
log_msg "Container ID: $CT_ID "
log_msg "Hostname: $HN "
log_msg "Disk Size: ${ DISK_SIZE } GB"
log_msg "CPU Cores: $CORE_COUNT "
log_msg "RAM Size: ${ RAM_SIZE } MiB"
log_msg "Bridge: $BRG "
log_msg "IPv4: $NET "
log_msg "IPv6: $IPV6_METHOD "
log_msg "FUSE Support: ${ ENABLE_FUSE :- no } "
log_msg "Nesting: $( [ " ${ ENABLE_NESTING :- 1 } " == "1" ] && echo "Enabled" || echo "Disabled" ) "
2026-04-16 22:23:15 +02:00
log_msg "Mknod: $( [ " ${ ENABLE_MKNOD :- 0 } " == "1" ] && echo "Enabled" || echo "Disabled" ) "
[[ -n " ${ ALLOW_MOUNT_FS :- } " ]] && log_msg "Mount FS: ${ ALLOW_MOUNT_FS } "
2026-02-14 13:41:09 +01:00
log_msg "GPU Passthrough: ${ ENABLE_GPU :- no } "
log_msg "Verbose Mode: $VERBOSE "
log_msg "Session ID: ${ SESSION_ID } "
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
2026-02-18 10:24:06 +01:00
# - Asks user whether to send anonymous diagnostic data (first run only)
2025-12-04 07:52:18 +01:00
# - Saves DIAGNOSTICS=yes/no in the config file
2026-02-18 10:24:06 +01:00
# - Reads current diagnostics setting from existing file
2025-12-04 07:52:18 +01:00
# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
# ------------------------------------------------------------------------------
2025-02-24 11:18:38 +01:00
diagnostics_check() {
2026-02-18 10:24:06 +01:00
local config_dir = "/usr/local/community-scripts"
local config_file = " ${ config_dir } /diagnostics"
2025-01-29 14:26:23 +01:00
2026-02-18 10:24:06 +01:00
mkdir -p " $config_dir "
2025-01-29 14:26:23 +01:00
2026-02-18 10:24:06 +01:00
if [[ -f " $config_file " ]] ; then
DIAGNOSTICS = $( awk -F '=' '/^DIAGNOSTICS/ {print $2}' " $config_file " ) || true
DIAGNOSTICS = " ${ DIAGNOSTICS :- no } "
return
2025-01-29 14:26:23 +01:00
fi
2026-02-18 10:24:06 +01:00
local result
result = $( whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "TELEMETRY & DIAGNOSTICS" \
--ok-button "Confirm" --cancel-button "Exit" \
--radiolist "\nHelp improve Community-Scripts by sharing anonymous data.\n\nWhat we collect:\n - Container resources (CPU, RAM, disk), OS & PVE version\n - Application name, install method and status\n\nWhat we DON'T collect:\n - No IP addresses, hostnames, or personal data\n\nYou can change this anytime in the Settings menu.\nPrivacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md\n\nUse SPACE to select, ENTER to confirm." 22 76 2 \
"yes" "Yes, share anonymous data" OFF \
"no" "No, opt out" OFF \
3>& 1 1>& 2 2>& 3) || result = "no"
DIAGNOSTICS = " ${ result :- no } "
cat <<EOF >"$config_file"
DIAGNOSTICS=${DIAGNOSTICS}
# Community-Scripts Telemetry Configuration
# https://telemetry.community-scripts.org
#
# This file stores your telemetry preference.
# Set DIAGNOSTICS=yes to share anonymous installation data.
# Set DIAGNOSTICS=no to disable telemetry.
#
# You can also change this via the Settings menu during installation.
#
# Data collected (when enabled):
# disk_size, core_count, ram_size, os_type, os_version,
# nsapp, method, pve_version, status, exit_code
#
# No personal data (IPs, hostnames, passwords) is ever collected.
# Privacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md
EOF
2025-12-04 07:52:18 +01:00
}
diagnostics_menu() {
2026-02-18 10:24:06 +01:00
local current = " ${ DIAGNOSTICS :- no } "
local status_text = "DISABLED"
[[ " $current " == "yes" ]] && status_text = "ENABLED"
local dialog_text =(
"Telemetry is currently: ${ status_text } \n\n"
"Anonymous data helps us improve scripts and track issues.\n"
"No personal data is ever collected.\n\n"
"More info: https://telemetry.community-scripts.org\n\n"
"Do you want to ${ current :+change this setting } ?"
)
if [[ " $current " == "yes" ]] ; then
2025-12-04 07:52:18 +01:00
if whiptail --backtitle "Proxmox VE Helper Scripts" \
2026-02-18 10:24:06 +01:00
--title "TELEMETRY SETTINGS" \
--yesno " ${ dialog_text [*] } " 14 64 \
--yes-button "Disable" --no-button "Keep enabled" ; then
2025-12-04 07:52:18 +01:00
DIAGNOSTICS = "no"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
2026-02-18 10:24:06 +01:00
whiptail --msgbox "Telemetry disabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
2025-12-04 07:52:18 +01:00
fi
else
if whiptail --backtitle "Proxmox VE Helper Scripts" \
2026-02-18 10:24:06 +01:00
--title "TELEMETRY SETTINGS" \
--yesno " ${ dialog_text [*] } " 14 64 \
--yes-button "Enable" --no-button "Keep disabled" ; then
2025-12-04 07:52:18 +01:00
DIAGNOSTICS = "yes"
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
2026-02-18 10:24:06 +01:00
whiptail --msgbox "Telemetry enabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
2025-12-04 07:52:18 +01:00
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
2026-02-14 13:41:09 +01:00
# - Also logs settings to log file for debugging
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
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 } "
2026-06-11 16:14:32 +10:00
if [[ " $( dpkg --print-architecture) " == "arm64" ]] ; then
echo -e " ${ INFO }${ BOLD }${ DGN } Architecture: ${ BGN } arm64 ${ CL } "
fi
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 " "
2026-02-14 13:41:09 +01:00
# Log settings to file
log_section "CONTAINER SETTINGS - ${ APP } "
log_msg "Application: ${ APP } "
log_msg "PVE Version: ${ PVEVERSION } (Kernel: ${ KERNEL_VERSION } )"
log_msg "Container ID: ${ CT_ID } "
log_msg "Operating System: $var_os ( $var_version )"
log_msg "Container Type: $CT_TYPE_DESC "
log_msg "Disk Size: ${ DISK_SIZE } GB"
log_msg "CPU Cores: ${ CORE_COUNT } "
log_msg "RAM Size: ${ RAM_SIZE } MiB"
[[ -n " ${ var_gpu :- } " && " ${ var_gpu } " == "yes" ]] && log_msg "GPU Passthrough: Enabled"
[[ " $VERBOSE " == "yes" ]] && log_msg "Verbose Mode: Enabled"
log_msg "Session ID: ${ SESSION_ID } "
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
2026-06-11 16:14:32 +10:00
ensure_whiptail
2024-02-21 19:51:36 -05:00
arch_check
2026-06-11 16:14:32 +10:00
arm64_notice
2024-02-21 19:51:36 -05:00
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 } "
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"
2026-03-02 10:55:20 +01:00
exit 110
2025-12-04 07:52:18 +01:00
}
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 } "
2026-03-02 10:55:20 +01:00
exit 111
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
;;
2026-03-12 12:21:28 +01:00
generated | GENERATED)
header_info
echo -e " ${ DEFAULT }${ BOLD }${ BL } Using Generated Settings on node $PVEHOST_NAME ${ CL } "
VERBOSE = "no"
METHOD = "generated"
base_settings " $VERBOSE "
echo_default
break
;;
2025-01-29 14:26:23 +01:00
*)
2026-02-27 13:59:02 +01:00
msg_error "Invalid option: $CHOICE "
2026-03-02 10:55:20 +01:00
exit 112
2025-12-04 07:52:18 +01:00
;;
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 ;;
2026-02-11 16:38:46 +01:00
2) ${ EDITOR :- nano } /usr/local/community-scripts/default.vars ;;
2025-12-04 07:52:18 +01:00
3)
if [ -f " $( get_app_defaults_path) " ] ; then
2026-02-11 16:38:46 +01:00
${ EDITOR :- nano } " $( get_app_defaults_path) "
2025-12-04 07:52:18 +01:00
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
2026-02-27 13:59:02 +01:00
msg_warn "Under-provisioned: Required ${ var_cpu } CPU/ ${ var_ram } MB RAM, Current ${ current_cpu } CPU/ ${ current_ram } MB RAM"
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"
2026-05-07 15:53:22 +02:00
if is_unattended; then
msg_error "Aborted: under-provisioned LXC in unattended mode ( ${ current_cpu } CPU/ ${ current_ram } MB RAM < ${ var_cpu } CPU/ ${ var_ram } MB RAM)"
exit 113
fi
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> "
2026-02-28 14:51:26 +01:00
read -r prompt </dev/tty
2025-12-04 07:52:18 +01:00
if [[ ! ${ prompt ,, } = ~ ^( yes) $ ]] ; then
2026-02-27 13:59:02 +01:00
msg_error "Aborted: under-provisioned LXC ( ${ current_cpu } CPU/ ${ current_ram } MB RAM < ${ var_cpu } CPU/ ${ var_ram } MB RAM)"
2026-03-02 10:55:20 +01:00
exit 113
2024-12-16 12:41:51 +01:00
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
2026-02-27 13:59:02 +01:00
msg_warn "Storage is dangerously low ( ${ usage } % used on /boot)"
2026-05-07 15:53:22 +02:00
if is_unattended; then
msg_error "Aborted: storage too low in unattended mode ( ${ usage } % used on /boot)"
exit 114
fi
2025-12-04 07:52:18 +01:00
echo -ne "Continue anyway? <y/N> "
2026-02-28 14:51:26 +01:00
read -r prompt </dev/tty
2025-12-04 07:52:18 +01:00
if [[ ! ${ prompt ,, } = ~ ^( y| yes) $ ]] ; then
2026-02-27 13:59:02 +01:00
msg_error "Aborted: storage too low ( ${ usage } % used)"
2026-03-02 10:55:20 +01:00
exit 114
2024-11-15 18:13:14 +01:00
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
2026-03-25 13:06:21 +01:00
line = $( grep -E "^ ${ tag } \|" " $MAPFILE " | head -n1 | cut -d'|' -f2- || true )
2025-12-04 07:52:18 +01:00
[[ -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
2026-07-19 19:01:41 +02:00
[[ -d " $glob_path " ]] && glob_path = " ${ glob_path %/ } /*"
local -a _scan_files
mapfile -t _scan_files < <( compgen -G " $glob_path " )
2025-12-04 07:52:18 +01:00
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
2026-03-25 13:06:21 +01:00
line = $( grep -E "^ ${ tag } \|" " $MAPFILE " | head -n1 | cut -d'|' -f2- || true )
2025-12-04 07:52:18 +01:00
[[ -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
}
2026-02-09 11:05:31 +01:00
# ------------------------------------------------------------------------------
# msg_menu()
#
# - Displays a numbered menu for update_script() functions
2026-04-23 15:30:38 +02:00
# - In silent mode (PHS_SILENT=1): auto-selects the default option
2026-02-09 11:05:31 +01:00
# - In interactive mode: shows menu via read with 10s timeout + default fallback
# - Usage: CHOICE=$(msg_menu "Title" "tag1" "Description 1" "tag2" "Desc 2" ...)
# - The first item is always the default
# - Returns the selected tag to stdout
# - If no valid selection or timeout, returns the default (first) tag
# ------------------------------------------------------------------------------
msg_menu() {
local title = " $1 "
shift
# Parse items into parallel arrays: tags[] and descriptions[]
local -a tags =()
local -a descs =()
while [[ $# -ge 2 ]] ; do
tags +=( " $1 " )
descs +=( " $2 " )
shift 2
done
local default_tag = " ${ tags [0] } "
local count = ${# tags [@] }
# Silent mode: return default immediately
2026-04-23 15:30:38 +02:00
if [[ -n " ${ PHS_SILENT +x } " ]] && [[ " ${ PHS_SILENT } " == "1" ]] ; then
2026-02-09 11:05:31 +01:00
echo " $default_tag "
return 0
fi
2026-02-09 17:12:56 +01:00
# Display menu to /dev/tty so it doesn't get captured by command substitution
{
echo ""
msg_custom "📋" " ${ BL } " " ${ title } "
echo ""
for i in " ${ !tags[@] } " ; do
local marker = " "
[[ $i -eq 0 ]] && marker = "* "
printf " ${ TAB3 }${ marker } %s) %s\n" " ${ tags [ $i ] } " " ${ descs [ $i ] } "
done
echo ""
} >/dev/tty
2026-02-09 11:05:31 +01:00
local selection = ""
2026-02-09 17:12:56 +01:00
read -r -t 10 -p " ${ TAB3 } Select [default= ${ default_tag } , timeout 10s]: " selection </dev/tty >/dev/tty || true
2026-02-09 11:05:31 +01:00
# Validate selection
if [[ -n " $selection " ]] ; then
for tag in " ${ tags [@] } " ; do
if [[ " $selection " == " $tag " ]] ; then
echo " $selection "
return 0
fi
done
msg_warn "Invalid selection ' ${ selection } ' - using default: ${ default_tag } "
fi
echo " $default_tag "
return 0
}
2026-05-02 12:06:16 +02:00
# ------------------------------------------------------------------------------
# run_addon_updates()
#
# - Scans /usr/local/bin/update_* for addon update scripts installed alongside
# the main application (e.g. by tools/addon/*.sh)
# - For each found addon, prompts the user (60s timeout, default no) whether
# it should be updated as well
# - Skipped entirely when PHS_SILENT=1 to keep unattended updates predictable
# ------------------------------------------------------------------------------
run_addon_updates() {
shopt -s nullglob
local addons =( /usr/local/bin/update_*)
shopt -u nullglob
(( ${# addons [@] } == 0)) && return 0
if [[ " ${ PHS_SILENT :- 0 } " == "1" ]] ; then
msg_info "Detected ${# addons [@] } addon update script(s) - skipping (PHS_SILENT)"
return 0
fi
echo
echo -e " ${ INFO }${ YW } Detected installed addon update script(s): ${ CL } "
local a name
for a in " ${ addons [@] } " ; do
echo -e " ${ TAB } - ${ a ##*/update_ } "
done
echo
local ans
for a in " ${ addons [@] } " ; do
name = " ${ a ##*/update_ } "
printf 'Do you also want to update addon "%s"? (y/N) [60s]: ' " $name "
ans = ""
2026-08-03 16:57:55 +02:00
if read -r -t 60 ans </dev/tty; then :; else echo; fi
2026-05-02 12:06:16 +02:00
case " ${ ans ,, } " in
y | yes)
bash " $a " || msg_warn "Addon update for $name failed (rc= $? )"
;;
*)
msg_info "Skipped addon: $name "
;;
esac
done
}
2026-06-16 10:56:29 +02:00
runtime_script_status_guard() {
2026-07-17 12:18:36 +02:00
local mode = " ${ 1 :- } "
2026-06-16 10:56:29 +02:00
local script_slug = " ${ SCRIPT_SLUG :- ${ NSAPP :- }} "
script_slug = " $( echo " $script_slug " | tr '[:upper:]' '[:lower:]' | tr ' ' '-' ) "
[[ -z " $script_slug " ]] && return 0
2026-07-17 12:18:36 +02:00
local api_url = "https://db.community-scripts.org/api/collections/script_scripts/records?filter=(slug=' ${ script_slug } ')&perPage=1&fields=slug,is_disabled,is_deleted,disable_message,deleted_message,pinned_version,pin_reason"
2026-06-16 10:56:29 +02:00
local response
if ! response = $( curl -fsSL --connect-timeout 2 --max-time 3 " $api_url " 2>/dev/null) ; then
msg_warn "Script status check is unavailable. Continuing without status verification."
return 0
fi
2026-07-17 12:18:36 +02:00
local is_deleted is_disabled deleted_message disable_message pinned_version pin_reason info_url
2026-07-13 11:10:49 +02:00
if printf '%s' " $response " | grep -qE '"items":[[:space:]]*\[[[:space:]]*\]' ; then
2026-06-16 10:56:29 +02:00
return 0
fi
2026-07-13 11:10:49 +02:00
# PocketBase returns a flat, fixed-field JSON blob; sed is enough here (no jq needed).
is_deleted = $( printf '%s' " $response " | sed -n 's/.*"is_deleted"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' | head -1)
is_disabled = $( printf '%s' " $response " | sed -n 's/.*"is_disabled"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' | head -1)
deleted_message = $( printf '%s' " $response " | sed -n 's/.*"deleted_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
disable_message = $( printf '%s' " $response " | sed -n 's/.*"disable_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
2026-07-17 12:18:36 +02:00
pinned_version = $( printf '%s' " $response " | sed -n 's/.*"pinned_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
pin_reason = $( printf '%s' " $response " | sed -n 's/.*"pin_reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
2026-07-13 11:10:49 +02:00
is_deleted = ${ is_deleted :- false }
is_disabled = ${ is_disabled :- false }
2026-06-16 10:56:29 +02:00
info_url = "https://community-scripts.org/scripts/ ${ script_slug } "
if [[ " $is_deleted " == "true" ]] ; then
msg_error "This script is no longer available in community-scripts."
[[ -n " $deleted_message " ]] && msg_error " $deleted_message "
[[ -z " $deleted_message " ]] && msg_error "This script was removed and cannot be installed or updated."
2026-06-16 11:07:23 +02:00
msg_warn "More info: ${ info_url } "
2026-06-16 10:56:29 +02:00
return 1
fi
if [[ " $is_disabled " == "true" ]] ; then
2026-07-02 13:53:39 +02:00
# Allow bypass via var_ignore_disable=true (still warn, but continue)
case " ${ var_ignore_disable :- } " in
1 | yes | true | on)
msg_warn "This script is currently disabled in community-scripts."
[[ -n " $disable_message " ]] && msg_warn " $disable_message "
msg_warn "Bypassing disable status via var_ignore_disable — continuing at your own risk."
msg_warn "More info: ${ info_url } "
return 0
;;
esac
2026-06-16 10:56:29 +02:00
msg_error "This script is currently disabled in community-scripts."
[[ -n " $disable_message " ]] && msg_error " $disable_message "
[[ -z " $disable_message " ]] && msg_error "Updates and installs are temporarily disabled for this script."
2026-06-16 11:07:23 +02:00
msg_warn "More info: ${ info_url } "
2026-06-16 10:56:29 +02:00
return 1
fi
2026-07-17 12:18:36 +02:00
if [[ " $mode " == "update" && -n " $pinned_version " && -n " ${ NSAPP :- } " ]] ; then
local current_file = " $HOME /. ${ NSAPP } "
if [[ -f " $current_file " ]] ; then
local installed pinned_clean installed_clean
installed = " $( <" $current_file " ) "
pinned_clean = " $pinned_version "
installed_clean = " $installed "
[[ " $pinned_clean " = ~ ^v[ 0-9] ]] && pinned_clean = " ${ pinned_clean : 1 } "
[[ " $installed_clean " = ~ ^v[ 0-9] ]] && installed_clean = " ${ installed_clean : 1 } "
if [[ " $installed_clean " == " $pinned_clean " ]] ; then
if [[ -n " $pin_reason " ]] ; then
msg_info "You are already on the pinned version ( ${ pinned_version } ). ${ pin_reason } "
else
msg_info "You are already on the pinned version ( ${ pinned_version } ). No newer update is offered intentionally — please do not open an issue unless you see an actual error."
fi
msg_info "More info: ${ info_url } "
fi
fi
fi
2026-06-16 10:56:29 +02:00
return 0
}
2026-07-22 08:55:32 +02:00
# ------------------------------------------------------------------------------
# check_container_os_guard()
#
# - Compares the container OS (/etc/os-release) with the script's recommended
# var_os/var_version before running update_script
# - On mismatch: interactive runs ask whether to continue (default: no);
# headless runs (PHS_SILENT=1 / no tty) abort instead of updating on an
# unsupported base and leaving the app broken mid-update
# - Bypass via var_ignore_os_mismatch=1|yes|true|on (still warns, but continues)
# ------------------------------------------------------------------------------
check_container_os_guard() {
local rec_os = " ${ var_os :- } " rec_ver = " ${ var_version :- } "
rec_os = " ${ rec_os ,, } "
[[ -z " $rec_os " || -z " $rec_ver " ]] && return 0
[[ -r /etc/os-release ]] || return 0
local cur_os cur_ver
cur_os = " $( . /etc/os-release 2>/dev/null; echo " ${ ID :- } " ) "
cur_ver = " $( . /etc/os-release 2>/dev/null; echo " ${ VERSION_ID :- } " ) "
cur_os = " ${ cur_os ,, } "
[[ -z " $cur_os " || -z " $cur_ver " ]] && return 0
# Exact version or prefix on a dot boundary (e.g. alpine 3.22 matches 3.22.1)
if [[ " $cur_os " == " $rec_os " ]] && [[ " $cur_ver " == " $rec_ver " || " $cur_ver " == " $rec_ver " .* ]] ; then
return 0
fi
2026-07-26 18:42:11 +02:00
local downgraded_target = 0
if [[ " $cur_os " == " $rec_os " ]] && [[ " $cur_ver " != " $rec_ver " ]] ; then
if [[ " $( printf '%s\n%s\n' " $cur_ver " " $rec_ver " | sort -V | tail -n1) " == " $cur_ver " ]] ; then
downgraded_target = 1
fi
fi
2026-07-22 08:55:32 +02:00
case " ${ var_ignore_os_mismatch :- } " in
1 | yes | true | on)
msg_warn "Container OS is ${ cur_os } ${ cur_ver } but the script recommends ${ rec_os } ${ rec_ver } — continuing via var_ignore_os_mismatch (may break, no support)."
return 0
;;
esac
# Persistent opt-out: stores the ignored target version, so the question
# comes back once the script targets a newer OS again
local ignore_file = "/usr/local/community-scripts/ignore-os-mismatch"
if [[ -f " $ignore_file " && " $( cat " $ignore_file " 2>/dev/null) " == " ${ rec_os } ${ rec_ver } " ]] ; then
msg_warn "Container OS is ${ cur_os } ${ cur_ver } but the script recommends ${ rec_os } ${ rec_ver } — continuing (previously ignored via ${ ignore_file } , may break, no support)."
return 0
fi
2026-07-26 18:42:11 +02:00
local mismatch_detail remediation
if [[ " $downgraded_target " -eq 1 ]] ; then
mismatch_detail = "Your container runs ${ cur_os } ${ cur_ver } , which is newer than what this script now targets ( ${ rec_os } ${ rec_ver } ).\n\nThe target was likely lowered on purpose, e.g. because ${ APP :- the app } had problems on newer OS versions - check the GitHub issues for ${ APP :- this app } for details.\n\nContinuing on ${ cur_ver } often still works, but isn't guaranteed. If you are unsure, ask in our Discord (https://discord.gg/3AnUqsXnmK) before continuing."
remediation = "This script now targets the older ${ rec_os } ${ rec_ver } . Check the GitHub issues for ${ APP :- this app } , or ask in Discord (https://discord.gg/3AnUqsXnmK) if you are unsure"
else
mismatch_detail = "This container runs ${ cur_os } ${ cur_ver } , but this script now targets ${ rec_os } ${ rec_ver } .\n\nUpdating on the older base OS may fail or leave ${ APP :- the application } broken (e.g. required runtime versions are not available).\n\nRecommended: upgrade the container OS to ${ rec_os } ${ rec_ver } first, then run this update again."
remediation = "Upgrade the container OS to ${ rec_os } ${ rec_ver } first, then run this update again"
fi
2026-07-22 08:55:32 +02:00
if [[ " ${ PHS_SILENT :- 0 } " != "1" ]] && command -v whiptail & >/dev/null && [ -t 0 ] && [[ " $TERM " != "dumb" ]] ; then
local choice
choice = $( whiptail --backtitle "Proxmox VE Helper Scripts" --title "OS VERSION MISMATCH" --menu \
2026-07-26 18:42:11 +02:00
" ${ mismatch_detail } \n\nIf you continue anyway, it may break — no support is provided in that case.\n\nContinue anyway?" \
2026-07-22 08:55:32 +02:00
20 70 3 \
"1" "No (cancel update)" \
"2" "Yes (continue this time)" \
"3" "Yes (continue and don't ask again)" \
--nocancel --default-item "1" 3>& 1 1>& 2 2>& 3)
case " $choice " in
2)
msg_warn "Continuing update on ${ cur_os } ${ cur_ver } despite recommended ${ rec_os } ${ rec_ver } — may break, no support."
return 0
;;
3)
mkdir -p " ${ ignore_file %/* } "
echo " ${ rec_os } ${ rec_ver } " >" $ignore_file "
msg_warn "Continuing update on ${ cur_os } ${ cur_ver } ; OS check for ${ rec_os } ${ rec_ver } disabled via ${ ignore_file } — may break, no support."
return 0
;;
esac
msg_error "Update cancelled: container OS ${ cur_os } ${ cur_ver } does not match the recommended ${ rec_os } ${ rec_ver } ."
return 1
fi
msg_error "Container OS ${ cur_os } ${ cur_ver } does not match the recommended ${ rec_os } ${ rec_ver } — skipping update."
2026-07-26 18:42:11 +02:00
msg_error " ${ remediation } — or bypass this check (may break, no support) with: echo \" ${ rec_os } ${ rec_ver } \" > ${ ignore_file } "
2026-07-22 08:55:32 +02:00
return 1
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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
# ------------------------------------------------------------------------------
2026-06-16 10:56:29 +02:00
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
2026-07-17 12:18:36 +02:00
runtime_script_status_guard install || return 0
2025-12-04 07:52:18 +01:00
install_script || return 0
return 0
2026-04-23 15:30:38 +02:00
elif [ ! -z ${ PHS_SILENT +x } ] && [[ " ${ PHS_SILENT } " == "1" ]] ; then
2025-12-04 07:52:18 +01:00
VERBOSE = "no"
set_std_mode
2026-04-23 15:30:38 +02:00
ensure_profile_loaded
get_lxc_ip
2026-07-17 12:18:36 +02:00
runtime_script_status_guard update || return 0
2026-07-22 08:55:32 +02:00
check_container_os_guard || return 0
2026-04-23 15:30:38 +02:00
update_script
2026-05-02 12:06:16 +02:00
run_addon_updates
2026-04-23 15:30:38 +02:00
update_motd_ip
cleanup_lxc
2026-05-22 09:08:13 +02:00
elif ! command -v whiptail & >/dev/null || ! [ -t 0 ] || [[ " $TERM " == "dumb" ]] ; then
2026-05-16 00:44:27 +02:00
msg_info "No interactive terminal detected – defaulting to silent update mode"
VERBOSE = "no"
set_std_mode
ensure_profile_loaded
get_lxc_ip
2026-07-17 12:18:36 +02:00
runtime_script_status_guard update || return 0
2026-07-22 08:55:32 +02:00
check_container_os_guard || return 0
2026-05-16 00:44:27 +02:00
update_script
run_addon_updates
update_motd_ip
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
2026-03-02 10:55:20 +01:00
exit 0
2025-02-24 11:18:38 +01:00
;;
esac
2026-04-23 15:30:38 +02:00
ensure_profile_loaded
get_lxc_ip
2026-07-17 12:18:36 +02:00
runtime_script_status_guard update || return 0
2026-07-22 08:55:32 +02:00
check_container_os_guard || return 0
2026-04-23 15:30:38 +02:00
update_script
2026-05-02 12:06:16 +02:00
run_addon_updates
2026-04-23 15:30:38 +02:00
update_motd_ip
cleanup_lxc
2023-03-22 20:48:20 -04:00
fi
}
2025-12-04 07:52:18 +01:00
# ==============================================================================
# SECTION 8: CONTAINER CREATION & DEPLOYMENT
# ==============================================================================
2026-06-24 06:58:32 +02:00
# ------------------------------------------------------------------------------
# _apply_http_proxy_in_container()
#
# - Writes persistent proxy settings into the LXC before base package install
# - Ensures apt/apk/curl work behind corporate proxies during early setup
# ------------------------------------------------------------------------------
_apply_http_proxy_in_container() {
local proxy = " ${ HTTP_PROXY :- } "
local noproxy = " ${ HTTP_NO_PROXY :- } "
[[ -z " $proxy " || -z " ${ CTID :- } " ]] && return 0
[[ -z " $noproxy " ]] && noproxy = "localhost,127.0.0.1,.local"
msg_info "Applying HTTP proxy in container"
local tmpf
tmpf = " $( mktemp) "
cat >" $tmpf " <<EOF
export http_proxy="$proxy"
export https_proxy="$proxy"
export HTTP_PROXY="$proxy"
export HTTPS_PROXY="$proxy"
export no_proxy="$noproxy"
export NO_PROXY="$noproxy"
EOF
pct push " $CTID " " $tmpf " /etc/profile.d/90-http-proxy.sh >/dev/null 2>& 1 || {
rm -f " $tmpf "
msg_warn "Failed to push HTTP proxy profile into container"
return 0
}
pct exec " $CTID " -- chmod 644 /etc/profile.d/90-http-proxy.sh >/dev/null 2>& 1 || true
if [[ " $var_os " != "alpine" && " ${ APT_CACHER :- } " != "yes" ]] ; then
cat >" $tmpf " <<EOF
Acquire::http::Proxy "$proxy";
Acquire::https::Proxy "$proxy";
EOF
pct push " $CTID " " $tmpf " /etc/apt/apt.conf.d/95http-proxy >/dev/null 2>& 1 || true
fi
rm -f " $tmpf "
msg_ok "Applied HTTP proxy in container"
}
2026-07-30 10:31:09 +02:00
# ------------------------------------------------------------------------------
# _apply_host_ca_certs_in_container()
#
# - Copies administrator-provided CA certificates from the Proxmox host into the
# container before base package bootstrap
# - Source: /usr/local/share/ca-certificates/*.crt (Debian convention)
# - Refreshes the container trust store when update-ca-certificates is available
# - No-op when no host certificates are present; failures are non-fatal
# ------------------------------------------------------------------------------
_apply_host_ca_certs_in_container() {
local host_ca_dir = "/usr/local/share/ca-certificates"
[[ -z " ${ CTID :- } " ]] && return 0
local inherit_host_ca = " ${ INHERIT_HOST_CA :- ${ var_inherit_host_ca :- no }} "
local -a host_certs =()
local cert
shopt -s nullglob
for cert in " $host_ca_dir " /*.crt; do
host_certs +=( " $cert " )
done
shopt -u nullglob
[[ ${# host_certs [@] } -eq 0 ]] && return 0
# Opt-in only: copy host CA certs solely when explicitly enabled.
# Any other value (no/auto/unset) is a silent no-op to preserve LXC isolation.
case " ${ inherit_host_ca ,, } " in
yes | true | 1 | on) ;;
*)
return 0
;;
esac
msg_info "Inheriting host CA certificates into container"
local found = ${# host_certs [@] }
local copied = 0
local skipped = 0
local cert_name
pct exec " $CTID " -- mkdir -p /usr/local/share/ca-certificates >/dev/null 2>& 1 || {
msg_warn "Failed to create CA certificate directory in container"
return 0
}
for cert in " ${ host_certs [@] } " ; do
cert_name = " $( basename " $cert " ) "
if [[ ! -r " $cert " || " $cert_name " != *.crt ]] ; then
msg_warn "Skipping invalid or unreadable host CA certificate: ${ cert_name } "
skipped = $(( skipped + 1 ))
continue
fi
if pct push " $CTID " " $cert " "/usr/local/share/ca-certificates/ ${ cert_name } " >/dev/null 2>& 1; then
pct exec " $CTID " -- chmod 644 "/usr/local/share/ca-certificates/ ${ cert_name } " >/dev/null 2>& 1 || true
copied = $(( copied + 1 ))
else
msg_warn "Failed to push host CA certificate: ${ cert_name } "
skipped = $(( skipped + 1 ))
fi
done
if [[ $copied -eq 0 ]] ; then
msg_warn "No host CA certificates were copied ( ${ found } found, ${ skipped } skipped)"
return 0
fi
local refresh_shell = "bash"
[[ " $var_os " == "alpine" ]] && refresh_shell = "ash"
if pct exec " $CTID " -- " $refresh_shell " -c 'command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates' >/dev/null 2>& 1; then
msg_ok "Inherited ${ copied } host CA certificate(s) and updated trust store ( ${ skipped } skipped)"
else
msg_warn "Copied ${ copied } host CA certificate(s), but trust store update failed or update-ca-certificates is unavailable ( ${ skipped } skipped)"
fi
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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 } "
2026-06-24 23:21:12 +02:00
if [[ -n " ${ var_sdn_vnet :- ${ SDN_VNET :- }} " ]] ; then
2026-07-01 22:36:07 +02:00
NET_STRING = "-net0 name=eth0,bridge= ${ var_sdn_vnet :- $SDN_VNET } "
2026-06-24 23:21:12 +02:00
fi
2025-12-04 07:52:18 +01:00
# 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
2026-03-31 23:57:25 +02:00
,gw=) ;;
2025-12-04 07:52:18 +01:00
,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)
2026-03-22 22:48:19 +01:00
if [[ -n " $IPV6_ADDR " ]] ; then
NET_STRING = " $NET_STRING ,ip6= $IPV6_ADDR "
[ -n " $IPV6_GATE " ] && NET_STRING = " $NET_STRING ,gw6= $IPV6_GATE "
fi
2025-06-30 12:11:44 +02:00
;;
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
2026-04-16 22:23:15 +02:00
# Mknod support (user configurable via advanced settings)
if [ " ${ ENABLE_MKNOD :- 0 } " == "1" ] ; then
[ -n " $FEATURES " ] && FEATURES = " $FEATURES ,"
FEATURES = " ${ FEATURES } mknod=1"
fi
# Mount filesystem types (user configurable via advanced settings)
if [ -n " ${ ALLOW_MOUNT_FS :- } " ] ; then
2026-04-17 13:04:52 +02:00
# Sanitize: strip spaces, trailing/leading commas, then convert commas to semicolons
local _mount_clean = " ${ ALLOW_MOUNT_FS // / } "
_mount_clean = " ${ _mount_clean %%, } "
_mount_clean = " ${ _mount_clean ##, } "
2026-04-20 20:22:10 +02:00
_mount_clean = " ${ _mount_clean %%; } "
2026-04-17 13:04:52 +02:00
_mount_clean = " ${ _mount_clean //,/; } "
if [ -n " $_mount_clean " ] ; then
[ -n " $FEATURES " ] && FEATURES = " $FEATURES ,"
FEATURES = " ${ FEATURES } mount= ${ _mount_clean } "
fi
2026-04-16 22:23:15 +02:00
fi
2025-12-04 07:52:18 +01:00
# Build PCT_OPTIONS as string for export
2026-02-27 13:59:02 +01:00
local _func_url
2023-03-23 12:51:53 -04:00
if [ " $var_os " == "alpine" ] ; then
2026-02-27 13:59:02 +01:00
_func_url = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func"
2023-03-23 12:51:53 -04:00
else
2026-02-27 13:59:02 +01:00
_func_url = "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func"
fi
export FUNCTIONS_FILE_PATH = " $( curl -fsSL " $_func_url " ) "
if [[ -z " $FUNCTIONS_FILE_PATH " || ${# FUNCTIONS_FILE_PATH } -lt 100 ]] ; then
msg_error "Failed to download install functions from: $_func_url "
2026-03-02 10:55:20 +01:00
exit 115
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 "
2026-02-18 10:24:06 +01:00
export EXECUTION_ID = " $EXECUTION_ID "
2025-12-04 07:52:18 +01:00
export SESSION_ID = " $SESSION_ID "
2026-07-20 20:32:36 +02:00
# Repo attribution + platform for container-side progress pings (the
# container inherits the host's detection instead of re-detecting)
export REPO_SOURCE = " ${ REPO_SOURCE :- } "
export REPO_SLUG = " ${ REPO_SLUG :- } "
export TELEMETRY_PLATFORM = "pve"
2024-01-12 15:00:42 -05:00
export CACHER = " $APT_CACHER "
export CACHER_IP = " $APT_CACHER_IP "
2026-06-24 06:58:32 +02:00
if [[ -n " ${ HTTP_PROXY :- } " ]] ; then
export HTTP_PROXY HTTPS_PROXY = " $HTTP_PROXY " http_proxy = " $HTTP_PROXY " https_proxy = " $HTTP_PROXY "
export NO_PROXY = " ${ HTTP_NO_PROXY :- } " no_proxy = " ${ HTTP_NO_PROXY :- } "
export var_http_proxy = " $HTTP_PROXY "
export var_http_no_proxy = " ${ HTTP_NO_PROXY :- } "
fi
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 "
2026-06-19 17:53:08 +10:00
export PCT_ARCH = " $( dpkg --print-architecture 2>/dev/null || uname -m) "
2023-03-22 20:48:20 -04:00
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"
2026-02-23 16:07:13 +01:00
# Keep host-side logging on BUILD_LOG (not exported — invisible to container)
# Without this, get_active_logfile() would return INSTALL_LOG (a container path)
# and all host msg_info/msg_ok/msg_error would write to /root/.install-SESSION.log
# on the HOST instead of BUILD_LOG, causing incomplete telemetry logs.
_HOST_LOGFILE = " $BUILD_LOG "
2025-12-04 07:52:18 +01:00
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
2026-01-21 12:01:34 +01:00
PCT_OPTIONS_STRING = " -hostname $HN "
# Only add -tags if TAGS is not empty
if [ -n " $TAGS " ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
2025-12-04 07:52:18 +01:00
-tags $TAGS "
2026-01-21 12:01:34 +01:00
fi
2025-12-04 07:52:18 +01:00
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
2026-03-05 08:53:48 +01:00
# Add searchdomain if specified
2025-12-04 07:52:18 +01:00
if [ -n " $SD " ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
2026-03-05 08:53:48 +01:00
$SD "
2025-12-04 07:52:18 +01:00
fi
# Add nameserver if specified
if [ -n " $NS " ] ; then
PCT_OPTIONS_STRING = " $PCT_OPTIONS_STRING
2026-03-05 08:53:48 +01:00
$NS "
2025-12-04 07:52:18 +01:00
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 :- } "
2026-01-12 18:44:46 +01:00
# Validate storage space only if CONTAINER_STORAGE is already set
# (Storage selection happens in create_lxc_container for some modes)
if [[ -n " $CONTAINER_STORAGE " ]] ; then
msg_info "Validating storage space"
if ! validate_storage_space " $CONTAINER_STORAGE " " $DISK_SIZE " "no" ; then
local free_space
free_space = $( pvesm status 2>/dev/null | awk -v s = " $CONTAINER_STORAGE " '$1 == s { print $6 }' )
local free_fmt
free_fmt = $( numfmt --to= iec --from-unit= 1024 --suffix= B --format %.1f " $free_space " 2>/dev/null || echo " ${ free_space } KB" )
msg_error "Not enough space on ' $CONTAINER_STORAGE '. Required: ${ DISK_SIZE } GB, Available: ${ free_fmt } "
exit 214
fi
msg_ok "Storage space validated"
2026-06-22 21:08:09 +02:00
check_storage_health " $CONTAINER_STORAGE " " $DISK_SIZE "
2026-01-12 18:44:46 +01:00
fi
2026-01-12 18:12:55 +01:00
2025-12-04 07:52:18 +01:00
create_lxc_container || exit $?
2023-03-23 04:22:01 -04:00
2026-02-23 17:11:14 +01:00
# Transition to 'configuring' — container created, now setting up OS/userland
post_progress_to_api "configuring"
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
2026-02-28 14:51:26 +01:00
# grep returns exit 1 when no match — use || true to prevent ERR trap
local pci_vga_info
pci_vga_info = $( lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true )
# No GPU-related PCI devices at all? Skip silently.
if [[ -z " $pci_vga_info " ]] ; then
msg_debug "No VGA/Display/3D PCI devices found"
return 0
fi
2025-12-04 07:52:18 +01:00
2026-07-20 11:46:33 +02:00
# Map each DRI render/card node to its owning GPU via the PCI vendor ID
# exposed in sysfs (/sys/class/drm/<node>/device/vendor). This is required
# on multi-GPU hosts: globbing every /dev/dri node onto the first detected
# vendor would pass through *both* GPUs and skip the selection prompt.
if [[ -d /dev/dri ]] ; then
for d in /dev/dri/renderD* /dev/dri/card*; do
[[ -e " $d " ]] || continue
local node vendor
node = $( basename " $d " )
vendor = $( cat "/sys/class/drm/ ${ node } /device/vendor" 2>/dev/null || true )
case " $vendor " in
0x8086) INTEL_DEVICES +=( " $d " ) ;; # Intel
0x1002 | 0x1022) AMD_DEVICES +=( " $d " ) ;; # AMD/ATI
0x10de) ;; # NVIDIA - handled via /dev/nvidia* below
*) msg_debug "Unmapped GPU vendor ' ${ vendor :- unknown } ' for $d " ;;
esac
done
2025-12-04 07:52:18 +01:00
fi
2026-07-20 11:46:33 +02:00
[[ ${# INTEL_DEVICES [@] } -gt 0 ]] && msg_custom "🎮" " ${ BL } " "Detected Intel GPU"
[[ ${# AMD_DEVICES [@] } -gt 0 ]] && msg_custom "🎮" " ${ RD } " "Detected AMD GPU"
# AMD compute (ROCm) additionally needs /dev/kfd alongside the render nodes
if [[ ${# AMD_DEVICES [@] } -gt 0 && -e /dev/kfd ]] ; then
AMD_DEVICES +=( "/dev/kfd" )
2025-12-04 07:52:18 +01:00
fi
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
2026-02-28 14:51:26 +01:00
if grep -q "\[10de:" <<< " $pci_vga_info " ; then
2025-12-04 07:52:18 +01:00
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
2026-02-28 14:51:26 +01:00
read -rp "Which GPU type to passthrough? ( ${ available_gpus [*] } ): " selected_gpu </dev/tty
2025-12-04 07:52:18 +01:00
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
2026-03-09 11:14:26 +01:00
# Increase disk size for AMD ROCm runtime (~4GB extra needed)
2026-03-09 10:18:08 +01:00
if [[ " ${ GPU_TYPE :- } " == "AMD" ]] ; then
2026-03-09 11:14:26 +01:00
local rocm_extra = 4
2026-03-09 10:18:08 +01:00
local new_disk_size = $(( PCT_DISK_SIZE + rocm_extra))
if pct resize " $CTID " rootfs " ${ new_disk_size } G" >/dev/null 2>& 1; then
msg_ok "Disk resized ${ PCT_DISK_SIZE } GB → ${ new_disk_size } GB for ROCm"
else
msg_warn "Failed to resize disk for ROCm — installation may fail if space is insufficient"
fi
fi
2025-12-04 07:52:18 +01:00
# ============================================================================
# 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
2026-02-27 13:59:02 +01:00
local ct_status
ct_status = $( pct status " $CTID " 2>/dev/null || echo "unknown" )
msg_error "LXC Container did not reach running state (status: ${ ct_status } )"
2026-03-02 10:55:20 +01:00
exit 117
2025-07-22 14:46:50 +02:00
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 = ""
2026-04-07 23:10:37 +02:00
for i in { 1..60} ; do
2025-12-04 07:52:18 +01:00
# 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
2026-04-07 23:10:37 +02:00
# Progressive backoff: 1s for first 20, 2s for next 20, 3s for last 20
if [ " $i " -le 20 ] ; then
sleep 1
elif [ " $i " -le 40 ] ; then
sleep 2
else
sleep 3
fi
2025-12-04 07:52:18 +01:00
done
if [ -z " $ip_in_lxc " ] ; then
2026-04-07 23:10:37 +02:00
msg_error "No IP assigned to CT $CTID after 60 attempts"
2026-02-27 13:59:02 +01:00
msg_custom "🔧" " ${ YW } " "Troubleshooting:"
2025-12-04 07:52:18 +01:00
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"
2026-03-02 10:55:20 +01:00
exit 118
2025-12-04 07:52:18 +01:00
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
2026-02-27 13:59:02 +01:00
msg_warn "Network configured (IP: $ip_in_lxc ) but connectivity test failed - installation will continue"
2025-12-04 07:52:18 +01:00
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
2026-01-28 14:44:00 +01:00
# Fix Debian 13 LXC template bug where / is owned by nobody:nogroup
2026-01-28 14:54:30 +01:00
# This must be done from the host as unprivileged containers cannot chown /
local rootfs
2026-03-25 13:06:21 +01:00
rootfs = $( pct config " $CTID " | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1 || true )
2026-01-28 14:54:30 +01:00
if [[ -n " $rootfs " ]] ; then
local mount_point = "/var/lib/lxc/ ${ CTID } /rootfs"
if [[ -d " $mount_point " ]] && [[ " $( stat -c '%U' " $mount_point " ) " != "root" ]] ; then
chown root:root " $mount_point " 2>/dev/null || true
2026-01-28 14:44:00 +01:00
fi
fi
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
2026-02-28 14:51:26 +01:00
# Disable error trap for entire customization & install phase.
# All errors are handled explicitly — recovery menu shown on failure.
# Without this, customization errors (e.g. container stopped during base package
# install) would trigger error_handler() with a simple "Remove broken container?"
# prompt instead of the full recovery menu with retry/repair options.
set +Eeuo pipefail
trap - ERR
local install_exit_code = 0
2026-06-24 06:58:32 +02:00
_apply_http_proxy_in_container
2026-07-30 10:31:09 +02:00
_apply_host_ca_certs_in_container
2026-06-24 06:58:32 +02:00
2025-12-04 07:52:18 +01:00
# 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
2026-03-23 22:22:23 +01:00
https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
2024-01-16 22:24:33 -05:00
EOF'
2026-06-24 06:58:32 +02:00
pct exec " $CTID " -- ash -c 'set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; apk add bash newt curl openssh nano mc ncurses jq' >>" $BUILD_LOG " 2>& 1 || {
2026-03-26 16:53:04 +01:00
msg_warn "apk install failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
local alpine_exit = 0
pct exec " $CTID " -- ash -c '
2026-06-24 06:58:32 +02:00
set -a
[ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh
set +a
2026-03-26 16:53:04 +01:00
ALPINE_MIRRORS="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
for m in $(printf "%s\n" $ALPINE_MIRRORS | shuf); do
if wget -q --spider --timeout=2 "http://$m/alpine/latest-stable/main/" 2>/dev/null; then
echo " Attempting mirror: $m"
cat <<EOF >/etc/apk/repositories
http://$m/alpine/latest-stable/main
http://$m/alpine/latest-stable/community
EOF
if apk update >/dev/null 2>&1 && apk add bash newt curl openssh nano mc ncurses jq >/dev/null 2>&1; then
echo " CDN set to $m: tests passed"
exit 0
else
echo " Mirror $m failed"
fi
fi
done
exit 2
' && alpine_exit = 0 || alpine_exit = $?
if [[ $alpine_exit -ne 0 ]] ; then
msg_error "Failed to install base packages in Alpine container"
install_exit_code = 1
fi
2026-02-27 13:59:02 +01:00
}
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 }
2026-03-23 22:22:23 +01:00
local LANG_ESC = " ${ LANG //./ \\ . } "
LANG_ESC = " ${ LANG_ESC //|/ \\ | } "
pct exec " $CTID " -- bash -c "sed -i \"/ $LANG_ESC / 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
2026-06-11 16:14:32 +10:00
local _base_pkgs = "sudo curl mc gnupg2 jq"
if [[ " ${ ARCH :- amd64 } " == "arm64" ]] ; then
_base_pkgs += " openssh-server wget gcc"
fi
2026-03-26 16:53:04 +01:00
# Detect broken DNS resolver (e.g. Tailscale MagicDNS) and inject public DNS
if ! pct exec " $CTID " -- bash -c "getent hosts deb.debian.org >/dev/null 2>&1 && getent hosts archive.ubuntu.com >/dev/null 2>&1" ; then
msg_warn "APT repository DNS resolution failed in container, injecting public DNS servers"
pct exec " $CTID " -- bash -c "echo -e 'nameserver 8.8.8.8\nnameserver 1.1.1.1' >/etc/resolv.conf"
fi
2026-06-24 06:58:32 +02:00
pct exec " $CTID " -- bash -c "set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; apt-get update 2>&1 && apt-get install -y ${ _base_pkgs } 2>&1" >>" $BUILD_LOG " 2>& 1 || {
2026-03-26 16:53:04 +01:00
local failed_mirror
failed_mirror = $( pct exec " $CTID " -- bash -c "grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null" 2>/dev/null || echo "unknown" )
2026-06-15 10:09:38 +02:00
msg_warn "apt-get update failed ( ${ failed_mirror } )."
msg_custom "ℹ ️" " ${ YW } " "Probing alternate mirrors (this can take 1-2 minutes on network issues)"
2026-03-26 16:53:04 +01:00
local mirror_exit = 0
2026-06-11 16:14:32 +10:00
pct exec " $CTID " -- env APT_BASE = " $_base_pkgs " bash -c '
2026-06-24 06:58:32 +02:00
set -a
[ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh
set +a
2026-03-26 16:53:04 +01:00
DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
2026-06-15 10:09:38 +02:00
echo " Mirror fallback for distro: $DISTRO"
2026-03-26 16:53:04 +01:00
if [ "$DISTRO" = "ubuntu" ]; then
EU_MIRRORS="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
US_MIRRORS="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
AP_MIRRORS="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
else
EU_MIRRORS="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
US_MIRRORS="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
AP_MIRRORS="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
fi
TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC")
case "$TZ" in
Europe/*|Arctic/*) REGIONAL="$EU_MIRRORS"; OTHERS="$US_MIRRORS $AP_MIRRORS" ;;
America/*) REGIONAL="$US_MIRRORS"; OTHERS="$EU_MIRRORS $AP_MIRRORS" ;;
Asia/*|Australia/*|Pacific/*) REGIONAL="$AP_MIRRORS"; OTHERS="$EU_MIRRORS $US_MIRRORS" ;;
*) REGIONAL=""; OTHERS="$EU_MIRRORS $US_MIRRORS $AP_MIRRORS" ;;
esac
echo "Acquire::By-Hash \"no\";" >/etc/apt/apt.conf.d/99no-by-hash
try_mirrors() {
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[ -f "$src" ] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${1}/|g; s|deb http[s]*://[^/]*/|deb http://${1}/|g" "$src"
done
rm -rf /var/lib/apt/lists/*
APT_OUT=$(apt-get update 2>&1)
APT_RC=$?
if echo "$APT_OUT" | grep -qi "hashsum\|hash sum"; then
echo " Mirror $1 failed (hash mismatch)"
return 1
elif echo "$APT_OUT" | grep -qi "SSL\|certificate"; then
echo " Mirror $1 failed (SSL/certificate error)"
return 1
elif [ $APT_RC -ne 0 ]; then
echo " Mirror $1 failed (apt-get update error)"
return 1
elif apt-get install -y $APT_BASE >/dev/null 2>&1; then
echo " CDN set to $1: tests passed"
return 0
else
echo " Mirror $1 failed (package install error)"
return 1
fi
}
scan_reachable() {
local result=""
for m in $1; do
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
result="$result $m"
fi
done
echo "$result" | xargs
}
# Phase 1: Scan global mirrors first (independent of local CDN issues)
2026-06-15 10:09:38 +02:00
echo " Phase 1/3: Scanning global mirrors for reachability..."
2026-03-26 16:53:04 +01:00
OTHERS_OK=$(scan_reachable "$OTHERS")
OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)
2026-06-15 10:09:38 +02:00
if [ -z "$OTHERS_PICK" ]; then
echo " No reachable global mirrors found"
fi
2026-03-26 16:53:04 +01:00
for mirror in $OTHERS_PICK; do
echo " Attempting mirror: $mirror"
try_mirrors "$mirror" && exit 0
done
# Phase 2: Try primary mirror
2026-06-15 10:09:38 +02:00
echo " Phase 2/3: Trying primary mirror..."
2026-03-26 16:53:04 +01:00
if [ "$DISTRO" = "ubuntu" ]; then
PRIMARY="archive.ubuntu.com"
else
PRIMARY="ftp.debian.org"
fi
if timeout 2 bash -c "echo >/dev/tcp/$PRIMARY/80" 2>/dev/null; then
echo " Attempting mirror: $PRIMARY"
try_mirrors "$PRIMARY" && exit 0
fi
# Phase 3: Fall back to regional mirrors
2026-06-15 10:09:38 +02:00
echo " Phase 3/3: Scanning regional mirrors..."
2026-03-26 16:53:04 +01:00
REGIONAL_OK=$(scan_reachable "$REGIONAL")
REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)
2026-06-15 10:09:38 +02:00
if [ -z "$REGIONAL_PICK" ]; then
echo " No reachable regional mirrors found"
fi
2026-03-26 16:53:04 +01:00
for mirror in $REGIONAL_PICK; do
echo " Attempting mirror: $mirror"
try_mirrors "$mirror" && exit 0
done
exit 2
' && mirror_exit = 0 || mirror_exit = $?
if [[ $mirror_exit -eq 2 ]] ; then
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
if [[ " $var_os " == "ubuntu" ]] ; then
msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
else
msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
fi
local custom_mirror = ""
while true; do
read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
[[ -z " $custom_mirror " ]] && continue
[[ " $custom_mirror " == "skip" ]] && break
[[ ! " $custom_mirror " = ~ ^[ a-zA-Z0-9._-] +$ ]] && {
msg_warn "Invalid hostname format."
continue
}
pct exec " $CTID " -- bash -c "
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[ -f \"\$src\" ] && sed -i \"s|URIs: http[s]*://[^/]*/|URIs: http:// ${ custom_mirror } /|g; s|deb http[s]*://[^/]*/|deb http:// ${ custom_mirror } /|g\" \"\$src\"
done
rm -rf /var/lib/apt/lists/*
2026-06-11 16:14:32 +10:00
apt-get update >/dev/null 2>&1 && apt-get install -y ${ _base_pkgs } >/dev/null 2>&1
2026-03-26 16:53:04 +01:00
" && break
msg_warn "Mirror ' ${ custom_mirror } ' also failed. Try another or type 'skip'."
done
if [[ " $custom_mirror " == "skip" ]] ; then
2026-06-15 10:09:38 +02:00
msg_warn "Mirror selection aborted by user ('skip')"
msg_warn "Aborting installation before base package bootstrap"
post_update_to_api "aborted" "130" "force"
install_exit_code = 130
2026-03-26 16:53:04 +01:00
fi
elif [[ $mirror_exit -ne 0 ]] ; then
msg_error "apt-get base packages installation failed"
2026-06-15 10:09:38 +02:00
install_exit_code = 100
2026-03-26 16:53:04 +01:00
fi
2025-12-04 07:52:18 +01:00
}
2025-06-20 13:28:06 +02:00
fi
2025-12-04 07:52:18 +01:00
2026-02-28 14:51:26 +01:00
# Only continue with installation if customization succeeded
if [[ $install_exit_code -eq 0 ]] ; then
msg_ok "Customized LXC Container"
2026-02-23 14:30:48 +01:00
2026-02-28 14:51:26 +01:00
# Optional DNS override for retry scenarios (inside LXC, never on host)
if [[ " ${ DNS_RETRY_OVERRIDE :- false } " == "true" ]] ; then
msg_info "Applying DNS retry override in LXC (8.8.8.8, 1.1.1.1)"
pct exec " $CTID " -- bash -c "printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >/etc/resolv.conf" >/dev/null 2>& 1 || true
msg_ok "DNS override applied in LXC"
fi
2025-12-04 07:52:18 +01:00
2026-02-28 14:51:26 +01:00
# Install SSH keys
install_ssh_keys_into_ct
# Start timer for duration tracking
start_install_timer
# Run application installer
# Error handling already disabled above (before customization phase)
# Signal handlers use this flag to stop the container on abort (SIGHUP/SIGINT/SIGTERM)
# Without this, SSH disconnects leave the container running as an orphan process
# that sends "configuring" status AFTER the host already reported "failed"
export CONTAINER_INSTALLING = true
2026-06-11 16:14:32 +10:00
local _install_script
_install_script = " $( curl -fsSL "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/ ${ var_install } .sh" ) "
lxc-attach -n " $CTID " -- bash -c " $_install_script "
2026-03-02 10:46:25 +01:00
local lxc_exit = $?
2026-03-02 09:29:55 +01:00
2026-02-28 14:51:26 +01:00
unset CONTAINER_INSTALLING
# Keep error handling DISABLED during failure detection and recovery
# Re-enabling it here would cause any pct exec/pull failure to trigger
# error_handler() on the host, bypassing the recovery menu entirely
# Check for error flag file in container (more reliable than lxc-attach exit code)
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
2025-12-04 07:52:18 +01:00
fi
2026-02-28 14:51:26 +01:00
# Fallback to lxc-attach exit code if no flag file
if [[ $install_exit_code -eq 0 && ${ lxc_exit :- 0 } -ne 0 ]] ; then
install_exit_code = ${ lxc_exit :- 0 }
fi
fi # end: if [[ $install_exit_code -eq 0 ]] (customization succeeded)
2025-12-04 07:52:18 +01:00
2026-02-28 14:51:26 +01:00
# Installation or customization failed?
2025-12-04 07:52:18 +01:00
if [[ $install_exit_code -ne 0 ]] ; then
2026-02-25 14:45:11 +01:00
# Prevent job-control signals from suspending the script during recovery.
2026-02-25 14:08:24 +01:00
# In non-interactive shells (bash -c), background processes (spinner) can
# trigger terminal-related signals that stop the entire process group.
2026-02-25 14:45:11 +01:00
# TSTP = Ctrl+Z, TTIN = bg read from tty, TTOU = bg write to tty (tostop)
trap '' TSTP TTIN TTOU
2026-02-25 14:08:24 +01:00
2026-08-05 14:57:15 +02:00
# lxc-attach left us in a background process group - claim the terminal back
# before we print anything. Note this does NOT hold: the log collection
# below uses pct pull/exec, which take it away again, so every interactive
# prompt has to reclaim it again right before its read.
reclaim_tty
2026-07-31 14:13:45 +02:00
2025-12-04 07:52:18 +01:00
msg_error "Installation failed in container ${ CTID } (exit code: ${ install_exit_code } )"
2026-02-13 12:29:03 +01:00
# Copy install log from container BEFORE API call so get_error_text() can read it
2025-12-04 07:52:18 +01:00
local build_log_copied = false
local install_log_copied = false
2026-02-14 13:41:09 +01:00
local combined_log = "/tmp/ ${ NSAPP :- lxc } - ${ CTID } - ${ SESSION_ID } .log"
2025-12-04 07:52:18 +01:00
if [[ -n " $CTID " && -n " ${ SESSION_ID :- } " ]] ; then
2026-02-14 13:41:09 +01:00
# Create combined log with header
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${ APP :- LXC } "
echo "Container ID: ${ CTID } "
echo "Session ID: ${ SESSION_ID } "
echo "Timestamp: $( date '+%Y-%m-%d %H:%M:%S' ) "
echo "================================================================================"
echo ""
} >" $combined_log "
# Append BUILD_LOG (host-side creation log) if it exists
2025-12-04 07:52:18 +01:00
if [[ -f " ${ BUILD_LOG } " ]] ; then
2026-02-14 13:41:09 +01:00
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat " ${ BUILD_LOG } "
echo ""
} >>" $combined_log "
build_log_copied = true
2025-12-04 07:52:18 +01:00
fi
2026-03-02 10:46:25 +01:00
# Copy and append INSTALL_LOG from container
2026-02-14 13:41:09 +01:00
local temp_install_log = "/tmp/.install-temp- ${ SESSION_ID } .log"
2026-03-02 10:46:25 +01:00
if pct pull " $CTID " "/root/.install- ${ SESSION_ID } .log" " $temp_install_log " 2>/dev/null; then
2026-02-14 13:41:09 +01:00
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat " $temp_install_log "
echo ""
} >>" $combined_log "
2026-03-02 10:46:25 +01:00
rm -f " $temp_install_log "
2025-12-04 07:52:18 +01:00
install_log_copied = true
2026-02-23 17:08:38 +01:00
# Point INSTALL_LOG to combined log so get_full_log() finds it
2026-02-14 13:41:09 +01:00
INSTALL_LOG = " $combined_log "
2025-12-04 07:52:18 +01:00
fi
2026-07-20 20:32:36 +02:00
# Pull the structured error capture (.errinfo) from the container.
# It contains EXACTLY the output of the command that failed (written by
# silent()/error_handler inside the container) and is the primary source
# for the telemetry error trace - instead of a generic log tail.
local host_errinfo = "/tmp/.errinfo- ${ SESSION_ID } "
if timeout 8 pct pull " $CTID " "/root/.install- ${ SESSION_ID } .log.errinfo" " $host_errinfo " 2>/dev/null && [[ -s " $host_errinfo " ]] ; then
TELEMETRY_ERRINFO = " $host_errinfo "
export TELEMETRY_ERRINFO
fi
2026-02-13 12:29:03 +01:00
fi
2025-12-04 07:52:18 +01:00
2026-03-24 09:57:43 +01:00
# Defense-in-depth: Ensure error handling stays disabled during recovery.
# Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
# and trap 'error_handler' ERR. If any code path above called such a function,
# the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
set +Eeuo pipefail
trap - ERR
# --- Exit code 1 subclassification: analyze logs BEFORE telemetry call ---
# Exit code 1 is generic ("General error"). Analyze logs to determine the
# real error category so telemetry gets a useful classification instead of "shell".
local is_oom = false
local is_network_issue = false
local is_apt_issue = false
local is_cmd_not_found = false
local is_disk_full = false
if [[ $install_exit_code -eq 1 && -f " $combined_log " ]] ; then
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' " $combined_log " ; then
is_apt_issue = true
fi
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' " $combined_log " ; then
is_oom = true
fi
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' " $combined_log " ; then
is_network_issue = true
fi
if grep -qiE ': command not found|No such file or directory.*/s?bin/' " $combined_log " ; then
is_cmd_not_found = true
fi
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' " $combined_log " ; then
is_disk_full = true
fi
fi
# Set override for categorize_error() so telemetry gets the real category
if [[ " $is_apt_issue " == true ]] ; then
export ERROR_CATEGORY_OVERRIDE = "dependency"
elif [[ " $is_oom " == true ]] ; then
export ERROR_CATEGORY_OVERRIDE = "resource"
elif [[ " $is_network_issue " == true ]] ; then
export ERROR_CATEGORY_OVERRIDE = "network"
elif [[ " $is_disk_full " == true ]] ; then
export ERROR_CATEGORY_OVERRIDE = "storage"
elif [[ " $is_cmd_not_found " == true ]] ; then
export ERROR_CATEGORY_OVERRIDE = "dependency"
fi
2026-02-13 12:29:03 +01:00
# Report failure to telemetry API (now with log available on host)
2026-02-25 14:08:24 +01:00
# NOTE: Do NOT use msg_info/spinner here — the background spinner process
# causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which
# stops the entire process group and prevents the recovery dialog from appearing.
$STD echo -e " ${ TAB } ⏳ Reporting failure to telemetry..."
2026-02-13 12:29:03 +01:00
post_update_to_api "failed" " $install_exit_code "
2026-02-25 14:08:24 +01:00
$STD echo -e " ${ TAB }${ CM :- ✔ } Failure reported"
2026-02-13 12:29:03 +01:00
2026-02-14 13:41:09 +01:00
# Show combined log location
2026-02-13 12:29:03 +01:00
if [[ -n " $CTID " && -n " ${ SESSION_ID :- } " ]] ; then
2026-02-14 15:28:30 +01:00
msg_custom "📋" " ${ YW } " "Installation log: ${ combined_log } "
2025-12-04 07:52:18 +01:00
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 } "
2026-08-05 14:57:15 +02:00
reclaim_tty
2026-02-28 14:51:26 +01:00
if read -r response </dev/tty && [[ " $response " = ~ ^[ Yy] $ ]] ; then
2025-12-04 07:52:18 +01:00
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
2026-02-14 15:28:30 +01:00
# Prompt user for cleanup with 60s timeout
2025-12-04 07:52:18 +01:00
echo ""
2026-02-17 12:14:46 +01:00
2026-03-24 09:57:43 +01:00
# Extend error detection for non-exit-1 codes (exit 1 was already analyzed above)
# The is_* flags were set above for exit code 1 log analysis; here we add
# exit-code-specific detections for other codes.
2026-02-17 12:14:46 +01:00
local error_explanation = ""
if declare -f explain_exit_code >/dev/null 2>& 1; then
error_explanation = " $( explain_exit_code " $install_exit_code " ) "
fi
# OOM detection: exit codes 134 (SIGABRT/heap), 137 (SIGKILL/OOM), 243 (Node.js heap)
if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]] ; then
is_oom = true
fi
# APT/DPKG detection: exit codes 100-102 (APT), 255 (DPKG with log evidence)
case " $install_exit_code " in
100 | 101 | 102) is_apt_issue = true ;;
255)
if [[ -f " $combined_log " ]] && grep -qiE 'dpkg|apt-get|apt\.conf|broken packages|unmet dependencies|E: Sub-process|E: Failed' " $combined_log " ; then
is_apt_issue = true
fi
;;
esac
2026-03-03 15:33:19 +01:00
# Disk full / ENOSPC detection: errno -28 (ENOSPC), exit 228 (custom handler), exit 23 (curl write error)
if [[ $install_exit_code -eq 228 || $install_exit_code -eq 23 ]] ; then
is_disk_full = true
fi
if [[ -f " $combined_log " ]] && grep -qiE 'ENOSPC|no space left on device|No space left on device|Disk quota exceeded|errno -28' " $combined_log " ; then
is_disk_full = true
fi
2026-02-17 12:14:46 +01:00
# Command not found detection
if [[ $install_exit_code -eq 127 ]] ; then
is_cmd_not_found = true
fi
# Network-related detection (curl/apt/git fetch failures and transient network issues)
case " $install_exit_code " in
6 | 7 | 22 | 28 | 35 | 52 | 56 | 57 | 75 | 78) is_network_issue = true ;;
100)
# APT can fail due to network (Failed to fetch)
if [[ -f " $combined_log " ]] && grep -qiE 'Failed to fetch|Could not resolve|Connection failed|Network is unreachable|Temporary failure resolving' " $combined_log " ; then
is_network_issue = true
fi
;;
128)
if [[ -f " $combined_log " ]] && grep -qiE 'RPC failed|early EOF|fetch-pack|HTTP/2 stream|Could not resolve host|Temporary failure resolving|Failed to fetch|Connection reset|Network is unreachable' " $combined_log " ; then
is_network_issue = true
fi
;;
esac
# Show error explanation if available
if [[ -n " $error_explanation " ]] ; then
echo -e " ${ TAB }${ RD } Error: ${ error_explanation }${ CL } "
echo ""
fi
# Show specific hints for known error types
if [[ $install_exit_code -eq 10 ]] ; then
echo -e " ${ TAB }${ INFO } This error usually means the container needs ${ GN } privileged ${ CL } mode or Docker/nesting support."
echo -e " ${ TAB }${ INFO } Recreate with: Advanced Install → Container Type: ${ GN } Privileged ${ CL } "
echo ""
fi
if [[ $install_exit_code -eq 125 || $install_exit_code -eq 126 ]] ; then
echo -e " ${ TAB }${ INFO } The command exists but cannot be executed. This may be a ${ GN } permission ${ CL } issue."
echo -e " ${ TAB }${ INFO } If using Docker, ensure the container is ${ GN } privileged ${ CL } or has correct permissions."
echo ""
fi
2026-03-03 15:33:19 +01:00
if [[ " $is_disk_full " == true ]] ; then
echo -e " ${ TAB }${ INFO } The container ran out of disk space during installation ( ${ GN } ENOSPC ${ CL } )."
echo -e " ${ TAB }${ INFO } Current disk size: ${ GN }${ DISK_SIZE } GB ${ CL } . A rebuild with doubled disk may resolve this."
echo ""
fi
2026-02-17 12:14:46 +01:00
if [[ " $is_cmd_not_found " == true ]] ; then
local missing_cmd = ""
if [[ -f " $combined_log " ]] ; then
2026-02-24 14:22:07 +01:00
missing_cmd = $( grep -oiE '[a-zA-Z0-9_.-]+: command not found' " $combined_log " 2>/dev/null | tail -1 | sed 's/: command not found//' ) || true
2026-02-17 12:14:46 +01:00
fi
if [[ -n " $missing_cmd " ]] ; then
echo -e " ${ TAB }${ INFO } Missing command: ${ GN }${ missing_cmd }${ CL } "
fi
echo ""
fi
# Build recovery menu based on error type
echo -e " ${ YW } What would you like to do? ${ CL } "
echo ""
echo -e " ${ GN } 1) ${ CL } Remove container and exit"
echo -e " ${ GN } 2) ${ CL } Keep container for debugging"
echo -e " ${ GN } 3) ${ CL } Retry with verbose mode (full rebuild)"
local next_option = 4
2026-03-03 15:33:19 +01:00
local APT_OPTION = "" OOM_OPTION = "" DNS_OPTION = "" DISK_OPTION = ""
2026-02-17 12:14:46 +01:00
if [[ " $is_apt_issue " == true ]] ; then
if [[ " $var_os " == "alpine" ]] ; then
echo -e " ${ GN }${ next_option } ) ${ CL } Repair APK state and re-run install (in-place)"
else
echo -e " ${ GN }${ next_option } ) ${ CL } Repair APT/DPKG state and re-run install (in-place)"
fi
APT_OPTION = $next_option
next_option = $(( next_option + 1 ))
fi
if [[ " $is_oom " == true ]] ; then
local recovery_attempt = " ${ RECOVERY_ATTEMPT :- 0 } "
if [[ $recovery_attempt -lt 2 ]] ; then
local new_ram = $(( RAM_SIZE * 2 ))
local new_cpu = $(( CORE_COUNT * 2 ))
echo -e " ${ GN }${ next_option } ) ${ CL } Retry with more resources (RAM: ${ RAM_SIZE } → ${ new_ram } MiB, CPU: ${ CORE_COUNT } → ${ new_cpu } cores)"
OOM_OPTION = $next_option
next_option = $(( next_option + 1 ))
else
echo -e " ${ DGN } -) ${ CL } ${ DGN } OOM retry exhausted (already retried ${ recovery_attempt } x) ${ CL } "
fi
fi
2026-03-03 15:33:19 +01:00
if [[ " $is_disk_full " == true ]] ; then
local disk_recovery_attempt = " ${ DISK_RECOVERY_ATTEMPT :- 0 } "
if [[ $disk_recovery_attempt -lt 2 ]] ; then
local new_disk = $(( DISK_SIZE * 2 ))
echo -e " ${ GN }${ next_option } ) ${ CL } Retry with more disk space (Disk: ${ DISK_SIZE } → ${ new_disk } GB)"
DISK_OPTION = $next_option
next_option = $(( next_option + 1 ))
else
echo -e " ${ DGN } -) ${ CL } ${ DGN } Disk resize retry exhausted (already retried ${ disk_recovery_attempt } x) ${ CL } "
fi
fi
2026-02-17 12:14:46 +01:00
if [[ " $is_network_issue " == true ]] ; then
echo -e " ${ GN }${ next_option } ) ${ CL } Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1)"
DNS_OPTION = $next_option
next_option = $(( next_option + 1 ))
fi
local max_option = $(( next_option - 1 ))
echo ""
echo -en " ${ YW } Select option [1- ${ max_option } ] (default: 1, auto-remove in 60s): ${ CL } "
2025-12-04 07:52:18 +01:00
2026-03-02 10:25:19 +01:00
local response = ""
2026-07-21 23:27:22 +02:00
local read_rc
2026-08-05 14:57:15 +02:00
reclaim_tty
2026-07-28 19:45:29 +02:00
read -t 60 -r response </dev/tty
2026-07-21 23:27:22 +02:00
read_rc = $?
if [[ $read_rc -eq 0 ]] ; then
2026-02-17 12:14:46 +01:00
case " ${ response :- 1 } " in
1)
2025-12-04 07:52:18 +01:00
# Remove container
2026-02-17 12:14:46 +01:00
echo -e "\n ${ TAB }${ HOLD }${ YW } Removing container ${ CTID }${ CL } "
2025-12-04 07:52:18 +01:00
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
2026-02-17 12:14:46 +01:00
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
;;
2)
echo -e "\n ${ TAB }${ YW } Container ${ CTID } kept for debugging ${ CL } "
2025-12-04 07:52:18 +01:00
# 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 "
2026-02-17 12:14:46 +01:00
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
2025-12-04 07:52:18 +01:00
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
2026-02-17 12:14:46 +01:00
exit $install_exit_code
;;
3)
# Retry with verbose mode (full rebuild)
echo -e "\n ${ TAB }${ HOLD }${ YW } Removing container ${ CTID } for rebuild... ${ CL } "
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
echo ""
# Get new container ID
local old_ctid = " $CTID "
export CTID = $( get_valid_container_id " $CTID " )
export VERBOSE = "yes"
export var_verbose = "yes"
# Show rebuild summary
echo -e " ${ YW } Rebuilding with preserved settings: ${ CL } "
echo -e " Container ID: ${ old_ctid } → ${ CTID } "
echo -e " RAM: ${ RAM_SIZE } MiB | CPU: ${ CORE_COUNT } cores | Disk: ${ DISK_SIZE } GB"
echo -e " Network: ${ NET :- dhcp } | Bridge: ${ BRG :- vmbr0 } "
echo -e " Verbose: ${ GN } enabled ${ CL } "
echo ""
msg_info "Restarting installation..."
2026-07-20 20:32:36 +02:00
# New telemetry execution for the retry (previous one keeps its "failed")
declare -f telemetry_new_attempt & >/dev/null && telemetry_new_attempt
2026-02-17 12:14:46 +01:00
# Re-run build_container
build_container
return $?
;;
*)
# Handle dynamic smart recovery options via named option variables
local handled = false
if [[ -n " ${ APT_OPTION } " && " ${ response } " == " ${ APT_OPTION } " ]] ; then
# Package manager in-place repair: fix broken state and re-run install script
handled = true
if [[ " $var_os " == "alpine" ]] ; then
echo -e "\n ${ TAB }${ HOLD }${ YW } Repairing APK state in container ${ CTID } ... ${ CL } "
pct exec " $CTID " -- ash -c "
apk fix 2>/dev/null || true
apk cache clean 2>/dev/null || true
apk update 2>/dev/null || true
" >/dev/null 2>& 1 || true
echo -e " ${ BFR }${ CM }${ GN } APK state repaired in container ${ CTID }${ CL } "
else
echo -e "\n ${ TAB }${ HOLD }${ YW } Repairing APT/DPKG state in container ${ CTID } ... ${ CL } "
pct exec " $CTID " -- bash -c "
DEBIAN_FRONTEND=noninteractive dpkg --configure -a 2>/dev/null || true
apt-get -f install -y 2>/dev/null || true
apt-get clean 2>/dev/null
apt-get update 2>/dev/null || true
" >/dev/null 2>& 1 || true
echo -e " ${ BFR }${ CM }${ GN } APT/DPKG state repaired in container ${ CTID }${ CL } "
fi
echo ""
export VERBOSE = "yes"
export var_verbose = "yes"
echo -e " ${ YW } Re-running installation in existing container ${ CTID } : ${ CL } "
echo -e " RAM: ${ RAM_SIZE } MiB | CPU: ${ CORE_COUNT } cores | Disk: ${ DISK_SIZE } GB"
echo -e " Verbose: ${ GN } enabled ${ CL } "
echo ""
msg_info "Re-running installation script..."
2026-07-20 20:32:36 +02:00
# New telemetry execution for the in-place retry
declare -f telemetry_new_attempt & >/dev/null && telemetry_new_attempt
declare -f post_to_api & >/dev/null && post_to_api 2>/dev/null || true
2026-02-17 12:14:46 +01:00
# Re-run install script in existing container (don't destroy/recreate)
set +Eeuo pipefail
trap - ERR
2026-06-11 16:14:32 +10:00
local _install_script
_install_script = " $( curl -fsSL "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/ ${ var_install } .sh" ) "
lxc-attach -n " $CTID " -- bash -c " $_install_script "
2026-03-02 10:46:25 +01:00
local apt_retry_exit = $?
2026-02-17 12:14:46 +01:00
set -Eeuo pipefail
trap 'error_handler' ERR
# Check for error flag from retry
local apt_retry_code = 0
if [[ -n " ${ SESSION_ID :- } " ]] ; then
local retry_error_flag = "/root/.install- ${ SESSION_ID } .failed"
if pct exec " $CTID " -- test -f " $retry_error_flag " 2>/dev/null; then
apt_retry_code = $( pct exec " $CTID " -- cat " $retry_error_flag " 2>/dev/null || echo "1" )
pct exec " $CTID " -- rm -f " $retry_error_flag " 2>/dev/null || true
fi
fi
if [[ $apt_retry_code -eq 0 && $apt_retry_exit -ne 0 ]] ; then
apt_retry_code = $apt_retry_exit
fi
if [[ $apt_retry_code -eq 0 ]] ; then
msg_ok "Installation completed successfully after APT repair!"
2026-03-24 09:57:43 +01:00
INSTALL_COMPLETE = true
2026-02-17 12:14:46 +01:00
post_update_to_api "done" "0" "force"
return 0
else
msg_error "Installation still failed after APT repair (exit code: ${ apt_retry_code } )"
install_exit_code = $apt_retry_code
fi
fi
if [[ -n " ${ OOM_OPTION } " && " ${ response } " == " ${ OOM_OPTION } " ]] ; then
# Retry with doubled resources
handled = true
echo -e "\n ${ TAB }${ HOLD }${ YW } Removing container ${ CTID } for rebuild with more resources... ${ CL } "
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
echo ""
local old_ctid = " $CTID "
local old_ram = " $RAM_SIZE "
local old_cpu = " $CORE_COUNT "
export CTID = $( get_valid_container_id " $CTID " )
export RAM_SIZE = $(( RAM_SIZE * 2 ))
export CORE_COUNT = $(( CORE_COUNT * 2 ))
export var_ram = " $RAM_SIZE "
export var_cpu = " $CORE_COUNT "
export VERBOSE = "yes"
export var_verbose = "yes"
2026-02-17 16:36:20 +01:00
export RECOVERY_ATTEMPT = $(( ${ RECOVERY_ATTEMPT :- 0 } + 1 ))
2026-02-17 12:14:46 +01:00
echo -e " ${ YW } Rebuilding with increased resources (attempt ${ RECOVERY_ATTEMPT } /2): ${ CL } "
echo -e " Container ID: ${ old_ctid } → ${ CTID } "
echo -e " RAM: ${ old_ram } → ${ GN }${ RAM_SIZE }${ CL } MiB (x2)"
echo -e " CPU: ${ old_cpu } → ${ GN }${ CORE_COUNT }${ CL } cores (x2)"
echo -e " Disk: ${ DISK_SIZE } GB | Network: ${ NET :- dhcp } | Bridge: ${ BRG :- vmbr0 } "
echo -e " Verbose: ${ GN } enabled ${ CL } "
echo ""
msg_info "Restarting installation..."
2026-07-20 20:32:36 +02:00
declare -f telemetry_new_attempt & >/dev/null && telemetry_new_attempt
2026-02-17 12:14:46 +01:00
build_container
return $?
fi
2026-03-03 15:33:19 +01:00
if [[ -n " ${ DISK_OPTION } " && " ${ response } " == " ${ DISK_OPTION } " ]] ; then
# Retry with doubled disk size
handled = true
echo -e "\n ${ TAB }${ HOLD }${ YW } Removing container ${ CTID } for rebuild with more disk space... ${ CL } "
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
echo ""
local old_ctid = " $CTID "
local old_disk = " $DISK_SIZE "
export CTID = $( get_valid_container_id " $CTID " )
export DISK_SIZE = $(( DISK_SIZE * 2 ))
export var_disk = " $DISK_SIZE "
export VERBOSE = "yes"
export var_verbose = "yes"
export DISK_RECOVERY_ATTEMPT = $(( ${ DISK_RECOVERY_ATTEMPT :- 0 } + 1 ))
echo -e " ${ YW } Rebuilding with increased disk space (attempt ${ DISK_RECOVERY_ATTEMPT } /2): ${ CL } "
echo -e " Container ID: ${ old_ctid } → ${ CTID } "
echo -e " Disk: ${ old_disk } → ${ GN }${ DISK_SIZE }${ CL } GB (x2)"
echo -e " RAM: ${ RAM_SIZE } MiB | CPU: ${ CORE_COUNT } cores"
echo -e " Network: ${ NET :- dhcp } | Bridge: ${ BRG :- vmbr0 } "
echo -e " Verbose: ${ GN } enabled ${ CL } "
echo ""
msg_info "Restarting installation..."
2026-07-20 20:32:36 +02:00
declare -f telemetry_new_attempt & >/dev/null && telemetry_new_attempt
2026-03-03 15:33:19 +01:00
build_container
return $?
fi
2026-02-17 12:14:46 +01:00
if [[ -n " ${ DNS_OPTION } " && " ${ response } " == " ${ DNS_OPTION } " ]] ; then
# Retry with DNS override in LXC
handled = true
echo -e "\n ${ TAB }${ HOLD }${ YW } Removing container ${ CTID } for rebuild with DNS override... ${ CL } "
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
echo -e " ${ BFR }${ CM }${ GN } Container ${ CTID } removed ${ CL } "
echo ""
local old_ctid = " $CTID "
export CTID = $( get_valid_container_id " $CTID " )
export DNS_RETRY_OVERRIDE = "true"
export VERBOSE = "yes"
export var_verbose = "yes"
echo -e " ${ YW } Rebuilding with DNS override in LXC: ${ CL } "
echo -e " Container ID: ${ old_ctid } → ${ CTID } "
echo -e " DNS: ${ GN } 8.8.8.8, 1.1.1.1 ${ CL } (inside LXC only)"
echo -e " Verbose: ${ GN } enabled ${ CL } "
echo ""
msg_info "Restarting installation..."
2026-07-20 20:32:36 +02:00
declare -f telemetry_new_attempt & >/dev/null && telemetry_new_attempt
2026-02-17 12:14:46 +01:00
build_container
return $?
fi
if [[ " $handled " == false ]] ; then
echo -e "\n ${ TAB }${ YW } Invalid option. Container ${ CTID } kept. ${ CL } "
exit $install_exit_code
fi
;;
esac
2026-07-21 23:27:22 +02:00
elif [[ $read_rc -gt 128 ]] ; then
2025-12-04 07:52:18 +01:00
# Timeout - auto-remove
2026-02-14 15:28:30 +01:00
echo ""
msg_info "No response - removing container ${ CTID } "
2025-12-04 07:52:18 +01:00
pct stop " $CTID " & >/dev/null || true
pct destroy " $CTID " & >/dev/null || true
2026-02-14 15:28:30 +01:00
msg_ok "Container ${ CTID } removed"
2026-07-21 23:27:22 +02:00
else
# read itself failed (e.g. broken/closed stdin, I/O error) rather than
# timing out - don't guess and destroy the container on a read we
# couldn't actually capture; keep it since that's the reversible choice.
2026-08-03 16:57:55 +02:00
while read -r -t 0.1 -n 1 _ </dev/tty 2>/dev/null; do :; done
2026-07-21 23:27:22 +02:00
echo ""
msg_error "Could not read your response (stdin error) - keeping container ${ CTID } for safety."
msg_error "Remove it manually if not needed: pct destroy ${ CTID } "
2025-12-04 07:52:18 +01:00
fi
2026-02-13 13:49:49 +01:00
# Force one final status update attempt after cleanup
# This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
2026-02-25 14:08:24 +01:00
$STD echo -e " ${ TAB } ⏳ Finalizing telemetry report..."
2026-02-13 13:49:49 +01:00
post_update_to_api "failed" " $install_exit_code " "force"
2026-02-25 14:08:24 +01:00
$STD echo -e " ${ TAB }${ CM :- ✔ } Telemetry finalized"
2026-02-13 13:49:49 +01:00
2026-02-25 14:45:11 +01:00
# Restore default job-control signal handling before exit
trap - TSTP TTIN TTOU
2025-12-04 07:52:18 +01:00
exit $install_exit_code
fi
2026-02-23 13:22:09 +01:00
# Re-enable error handling after successful install or recovery menu completion
set -Eeuo pipefail
trap 'error_handler' ERR
2025-12-04 07:52:18 +01:00
}
destroy_lxc() {
if [[ -z " $CT_ID " ]] ; then
msg_error "No CT_ID found. Nothing to remove."
2026-03-26 16:07:38 +01:00
return 65
2025-12-04 07:52:18 +01:00
fi
# Abort on Ctrl-C / Ctrl-D / ESC
trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
local prompt
2026-08-05 14:57:15 +02:00
reclaim_tty
2026-02-28 14:51:26 +01:00
if ! read -rp "Remove this Container? <y/N> " prompt </dev/tty; then
2025-12-04 07:52:18 +01:00
# 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" ;;
2026-03-26 16:07:38 +01:00
*) return 65 ;;
2025-12-04 07:52:18 +01:00
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)"
2026-03-26 16:07:38 +01:00
return 238
2025-12-04 07:52:18 +01:00
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
2026-01-12 18:12:55 +01:00
total_h = " $( numfmt --to= iec --from-unit= 1024 --suffix= B --format %.1f " $total " 2>/dev/null || echo " $total " ) "
used_h = " $( numfmt --to= iec --from-unit= 1024 --suffix= B --format %.1f " $used " 2>/dev/null || echo " $used " ) "
free_h = " $( numfmt --to= iec --from-unit= 1024 --suffix= B --format %.1f " $free " 2>/dev/null || echo " $free " ) "
2025-12-04 07:52:18 +01:00
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
2026-03-23 22:22:23 +01:00
# Validate GIDs are numeric before sed
[[ " $render_gid " = ~ ^[ 0-9] +$ ]] || render_gid = "104"
[[ " $video_gid " = ~ ^[ 0-9] +$ ]] || video_gid = "44"
2025-12-04 07:52:18 +01:00
# 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 "
2026-06-26 05:06:27 -07:00
sed -i -E "s|(dev[0-9]+: /dev/kfd),gid=[0-9]+|\1,gid= ${ render_gid } |g" " $LXC_CONFIG "
2025-12-04 07:52:18 +01:00
# 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
2026-06-26 05:06:27 -07:00
if [ -e /dev/kfd ]; then
chgrp ${ render_gid } /dev/kfd 2>/dev/null || true
chmod 660 /dev/kfd 2>/dev/null || true
fi
2025-12-04 07:52:18 +01:00
" >/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 '"
2026-03-26 16:07:38 +01:00
return 65
2025-12-04 07:52:18 +01:00
;;
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 } )"
2026-01-12 18:12:55 +01:00
local USED_FMT = $( numfmt --to= iec --from-unit= 1024 --format %.1f <<< " $USED " )
local FREE_FMT = $( numfmt --to= iec --from-unit= 1024 --format %.1f <<< " $FREE " )
2025-12-04 07:52:18 +01:00
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
2026-01-12 18:12:55 +01:00
# Validate storage space for container storage
if [[ " $CLASS " == "container" && -n " ${ DISK_SIZE :- } " ]] ; then
validate_storage_space " $STORAGE_RESULT " " $DISK_SIZE " "yes"
2026-06-22 21:08:09 +02:00
check_storage_health " $STORAGE_RESULT " " $DISK_SIZE "
2026-01-12 18:12:55 +01:00
# Continue even if validation fails - user was warned
fi
2025-12-04 07:52:18 +01:00
return 0
done
}
2026-01-12 18:12:55 +01:00
# ------------------------------------------------------------------------------
# validate_storage_space()
#
# - Validates if storage has enough free space for container
# - Takes storage name and required size in GB
# - Returns 0 if enough space, 1 if not enough, 2 if storage unavailable
# - Can optionally show whiptail warning
# - Handles all storage types: dir, lvm, lvmthin, zfs, nfs, cifs, etc.
# ------------------------------------------------------------------------------
validate_storage_space() {
local storage = " $1 "
local required_gb = " ${ 2 :- 8 } "
local show_dialog = " ${ 3 :- no } "
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
# Get full storage line from pvesm status
local storage_line
storage_line = $( pvesm status 2>/dev/null | awk -v s = " $storage " '$1 == s {print $0}' )
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
# Check if storage exists and is active
if [[ -z " $storage_line " ]] ; then
[[ " $show_dialog " == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage ' $storage ' not found!\n\nThe storage may be unavailable or disabled." 10 60
2026-03-26 16:07:38 +01:00
return 236
2026-01-12 18:12:55 +01:00
fi
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
# Check storage status (column 3)
local status
status = $( awk '{print $3}' <<< " $storage_line " )
if [[ " $status " == "disabled" ]] ; then
[[ " $show_dialog " == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage ' $storage ' is disabled!\n\nPlease enable the storage first." 10 60
2026-03-26 16:07:38 +01:00
return 236
2026-01-12 18:12:55 +01:00
fi
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
# Get storage type and free space (column 6)
local storage_type storage_free
storage_type = $( awk '{print $2}' <<< " $storage_line " )
storage_free = $( awk '{print $6}' <<< " $storage_line " )
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
# Some storage types (like PBS, iSCSI) don't report size info
# In these cases, skip space validation
if [[ -z " $storage_free " || " $storage_free " == "0" ]] ; then
# Silent pass for storages without size info
return 0
fi
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
local required_kb = $(( required_gb * 1024 * 1024 ))
local free_gb_fmt
free_gb_fmt = $( numfmt --to= iec --from-unit= 1024 --suffix= B --format %.1f " $storage_free " 2>/dev/null || echo " ${ storage_free } KB" )
2026-01-12 18:41:19 +01:00
2026-01-12 18:12:55 +01:00
if [[ " $storage_free " -lt " $required_kb " ]] ; then
if [[ " $show_dialog " == "yes" ]] ; then
whiptail --msgbox "⚠️ Warning: Storage ' $storage ' may not have enough space!\n\nStorage Type: ${ storage_type } \nRequired: ${ required_gb } GB\nAvailable: ${ free_gb_fmt } \n\nYou can continue, but creation might fail." 14 70
2026-01-12 18:41:19 +01:00
fi
2026-03-26 16:07:38 +01:00
return 236
2026-01-12 18:41:19 +01:00
fi
2026-01-12 18:12:55 +01:00
2026-01-12 18:41:19 +01:00
return 0
}
2026-06-22 21:08:09 +02:00
# ------------------------------------------------------------------------------
# check_storage_health()
#
# - Warns or blocks when target storage is nearly full
# - Complements validate_storage_space (free space vs. requested disk)
# - Uses pvesm status usage when available
# ------------------------------------------------------------------------------
check_storage_health() {
local storage = " $1 "
local required_gb = " ${ 2 :- 8 } "
[[ -z " $storage " ]] && return 0
local storage_line storage_type total_kb avail_kb used_pct free_gb_fmt
storage_line = $( pvesm status 2>/dev/null | awk -v s = " $storage " '$1 == s {print $0}' )
[[ -z " $storage_line " ]] && return 0
storage_type = $( awk '{print $2}' <<< " $storage_line " )
total_kb = $( awk '{print $4}' <<< " $storage_line " )
avail_kb = $( awk '{print $6}' <<< " $storage_line " )
[[ -z " $total_kb " || " $total_kb " == "0" || -z " $avail_kb " ]] && return 0
2026-06-24 23:44:26 +02:00
used_pct = $(( ( total_kb - avail_kb) * 100 / total_kb))
2026-06-22 21:08:09 +02:00
free_gb_fmt = $( numfmt --to= iec --from-unit= 1024 --suffix= B --format %.1f " $avail_kb " 2>/dev/null || echo " ${ avail_kb } KB" )
2026-06-24 23:44:26 +02:00
if (( used_pct >= 95)) ; then
2026-06-22 21:08:09 +02:00
msg_warn "Storage ' ${ storage } ' ( ${ storage_type } ) is ${ used_pct } % full ( ${ free_gb_fmt } free)"
if ! is_unattended && command -v whiptail >/dev/null 2>& 1 && [[ -t 0 ]] ; then
if ! whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "STORAGE ALMOST FULL" \
--yesno "Storage ' ${ storage } ' is ${ used_pct } % full.\n\nType: ${ storage_type } \nAvailable: ${ free_gb_fmt } \nRequired for CT: ${ required_gb } GB\n\nContinue anyway?" 14 70; then
msg_error "Installation cancelled – storage nearly full"
exit 214
fi
2026-06-24 23:44:26 +02:00
elif (( used_pct >= 98)) ; then
2026-06-22 21:08:09 +02:00
msg_error "Storage ' ${ storage } ' is ${ used_pct } % full – refusing install in unattended mode"
exit 214
fi
2026-06-24 23:44:26 +02:00
elif (( used_pct >= 85)) ; then
2026-06-22 21:08:09 +02:00
msg_warn "Storage ' ${ storage } ' ( ${ storage_type } ) is ${ used_pct } % full ( ${ free_gb_fmt } free)"
fi
return 0
}
2026-01-12 18:41:19 +01:00
# ==============================================================================
# SECTION 8: CONTAINER CREATION
# ==============================================================================
# ------------------------------------------------------------------------------
# create_lxc_container()
#
# - Main function for creating LXC containers
# - Handles all phases: validation, template discovery, container creation,
# network config, storage, etc.
# - Extensive error checking with detailed exit codes
# ------------------------------------------------------------------------------
2025-12-04 07:52:18 +01:00
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 " ; }
2026-04-20 20:22:10 +02:00
# Switch to the previous OS major version template and retry pct create
# Determines the fallback version automatically based on available templates.
# Returns: 0 = success, 1 = failed
fallback_to_previous_os_version() {
local old_template = " $TEMPLATE "
local os_type = " ${ PCT_OSTYPE :- } "
local current_ver = " ${ PCT_OSVERSION :- } "
# Determine template search pattern based on OS type
local tpl_pattern = ""
case " $os_type " in
debian | ubuntu) tpl_pattern = "-standard_" ;;
alpine | fedora | rocky | centos) tpl_pattern = "-default_" ;;
*) tpl_pattern = "" ;;
esac
msg_info "Searching for an older $os_type template (current: $os_type $current_ver )"
# Collect all available versions for this OS type (local + online)
local -a all_versions =()
# Local templates
mapfile -t _local_vers < <(
pveam list " $TEMPLATE_STORAGE " 2>/dev/null |
awk -v os = " $os_type " -v pat = " $tpl_pattern " '$1 ~ ("^"os"|/"os) && $1 ~ pat {print $1}' |
sed 's|.*/||' |
sed -E "s/^ ${ os_type } -([0-9]+(\.[0-9]+)?).*/\1/" |
sort -u -V
)
all_versions +=( " ${ _local_vers [@] } " )
# Online templates (only if needed)
if command -v timeout & >/dev/null; then
timeout 30 pveam update >/dev/null 2>& 1 || true
else
pveam update >/dev/null 2>& 1 || true
fi
mapfile -t _online_vers < <(
pveam available -section system 2>/dev/null |
awk '{print $2}' |
grep -E "^ ${ os_type } -[0-9]" |
{ [[ -n " $tpl_pattern " ]] && grep " $tpl_pattern " || cat; } |
sed -E "s/^ ${ os_type } -([0-9]+(\.[0-9]+)?).*/\1/" |
sort -u -V 2>/dev/null || true
)
all_versions +=( " ${ _online_vers [@] } " )
# Deduplicate and sort, find the highest version below current
local fallback_ver = ""
fallback_ver = $( printf '%s\n' " ${ all_versions [@] } " | sort -u -V | awk -v cur = " $current_ver " '{
# Compare major versions: extract major part
split($0, a, ".")
split(cur, b, ".")
if (a[1]+0 < b[1]+0) ver=$0
} END { if (ver) print ver }' )
if [[ -z " $fallback_ver " ]] ; then
msg_error "No older $os_type template version found."
return 1
fi
msg_ok "Fallback version: $os_type $fallback_ver "
# Find the actual template file for this version
local fallback_search = " ${ os_type } - ${ fallback_ver } "
local fallback_template = ""
# Check local first
mapfile -t _fb_local < <(
pveam list " $TEMPLATE_STORAGE " 2>/dev/null |
awk -v search = " $fallback_search " -v pat = " $tpl_pattern " '$1 ~ search && $1 ~ pat {print $1}' |
sed 's|.*/||' | sort -t - -k 2 -V
)
if [[ ${# _fb_local [@] } -gt 0 ]] ; then
fallback_template = " ${ _fb_local [-1] } "
else
# Check online
mapfile -t _fb_online < <(
pveam available -section system 2>/dev/null |
awk '{print $2}' |
grep -E "^ ${ fallback_search } .* ${ tpl_pattern } " |
sort -t - -k 2 -V 2>/dev/null || true
)
[[ ${# _fb_online [@] } -gt 0 ]] && fallback_template = " ${ _fb_online [-1] } "
fi
if [[ -z " $fallback_template " ]] ; then
msg_error "No template found for $os_type $fallback_ver ."
return 1
fi
msg_ok "Found template: $fallback_template "
# Download if needed
local fallback_path
fallback_path = " $( pvesm path " $TEMPLATE_STORAGE :vztmpl/ $fallback_template " 2>/dev/null || true ) "
[[ -z " $fallback_path " ]] && fallback_path = "/var/lib/vz/template/cache/ $fallback_template "
if [[ ! -f " $fallback_path " ]] ; then
msg_info "Downloading $os_type $fallback_ver template"
if ! pveam download " $TEMPLATE_STORAGE " " $fallback_template " >>" ${ BUILD_LOG :- /dev/null } " 2>& 1; then
msg_error "Failed to download $os_type $fallback_ver template."
return 1
fi
msg_ok "Template downloaded"
fi
# Update variables
TEMPLATE = " $fallback_template "
TEMPLATE_PATH = " $fallback_path "
PCT_OSVERSION = " $fallback_ver "
export PCT_OSVERSION
# Retry pct create
msg_info "Retrying container creation with $os_type $fallback_ver "
if pct create " $CTID " " ${ TEMPLATE_STORAGE } :vztmpl/ ${ TEMPLATE } " $PCT_OPTIONS >>" $LOGFILE " 2>& 1; then
msg_ok "Container created successfully with $os_type $fallback_ver (fallback from $old_template )."
return 0
else
msg_error "Container creation with $os_type $fallback_ver also failed. See $LOGFILE "
return 1
fi
}
2025-12-04 07:52:18 +01:00
# Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
# Returns:
2026-04-23 20:52:59 +02:00
# 0 = no upgrade needed / container created after upgrade or explicit fallback
2025-12-04 07:52:18 +01:00
# 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
2026-04-23 20:52:59 +02:00
# 2 = user chose ignore
2025-12-04 07:52:18 +01:00
# 3 = upgrade attempted but failed OR retry failed
2026-04-23 20:52:59 +02:00
# 4 = user cancelled
2025-12-04 07:52:18 +01:00
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
2026-04-23 20:52:59 +02:00
local _ans
has_previous_os_version_template() {
local os_type = " ${ PCT_OSTYPE :- } "
local current_ver = " ${ PCT_OSVERSION :- } "
local tpl_pattern = " ${ TEMPLATE_PATTERN :- ${ TEMPLATE :- }} "
local -a all_versions =()
[[ -n " $os_type " && -n " $current_ver " ]] || return 1
mapfile -t _local_vers < <(
pveam list " $TEMPLATE_STORAGE " 2>/dev/null |
awk '{print $1}' |
sed 's|.*/||' |
grep -E "^ ${ os_type } -[0-9]" |
{ [[ -n " $tpl_pattern " ]] && grep " $tpl_pattern " || cat; } |
sed -E "s/^ ${ os_type } -([0-9]+(\.[0-9]+)?).*/\1/" |
sort -u -V
)
all_versions +=( " ${ _local_vers [@] } " )
if command -v timeout & >/dev/null; then
timeout 30 pveam update >/dev/null 2>& 1 || true
else
pveam update >/dev/null 2>& 1 || true
fi
mapfile -t _online_vers < <(
pveam available -section system 2>/dev/null |
awk '{print $2}' |
grep -E "^ ${ os_type } -[0-9]" |
{ [[ -n " $tpl_pattern " ]] && grep " $tpl_pattern " || cat; } |
sed -E "s/^ ${ os_type } -([0-9]+(\.[0-9]+)?).*/\1/" |
sort -u -V 2>/dev/null || true
)
all_versions +=( " ${ _online_vers [@] } " )
printf '%s\n' " ${ all_versions [@] } " | sort -u -V | awk -v cur = " $current_ver " '
{
split($0, a, ".")
split(cur, b, ".")
if (a[1]+0 < b[1]+0) found=1
}
END { exit found ? 0 : 1 }
'
}
2025-12-04 07:52:18 +01:00
_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
2026-06-23 07:29:05 +02:00
local _disable_update = " ${ DISABLE_UPDATE :- no } "
2026-06-23 06:31:44 +02:00
if [[ " ${ PHS_SILENT :- 0 } " == "1" || " ${ _disable_update ,, } " == "yes" ]] ; then
2026-06-22 21:59:27 +02:00
msg_info "Skipping host LXC stack upgrade prompt (unattended mode)"
return 2
fi
2026-03-23 14:44:24 +01:00
msg_warn "An update for the Proxmox LXC stack is available"
2025-12-04 07:52:18 +01:00
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 } "
2026-04-23 20:52:59 +02:00
echo " note : option 1 runs host apt update + apt upgrade"
2025-12-04 07:52:18 +01:00
echo
2026-04-20 20:22:10 +02:00
# Offer older OS version fallback when template version might be too new for LXC stack
local _has_fallback_option = false
2026-04-23 20:52:59 +02:00
if [[ " $do_retry " == "yes" ]] && has_previous_os_version_template; then
2026-04-20 20:22:10 +02:00
_has_fallback_option = true
2026-04-27 17:00:29 +10:00
echo " [1] Run host upgrade now (recommended). WARNING: this runs apt upgrade and updates all Packages on your host!"
2026-04-20 20:22:10 +02:00
echo " [2] Use an older ${ PCT_OSTYPE } template instead (may not work with all scripts)"
2026-07-27 10:48:05 +02:00
echo " [3] Ignore (continue without upgrading)"
echo " [4] Cancel (abort the script)"
2026-04-23 20:52:59 +02:00
echo
read -rp "Select option [1/2/3/4]: " _ans </dev/tty
else
2026-04-27 17:00:29 +10:00
echo " [1] Run host upgrade now (recommended). WARNING: this runs apt upgrade and updates all Packages on your host!"
2026-07-27 10:48:05 +02:00
echo " [2] Ignore (continue without upgrading)"
echo " [3] Cancel (abort the script)"
2026-04-20 20:22:10 +02:00
echo
read -rp "Select option [1/2/3]: " _ans </dev/tty
fi
if [[ " $_has_fallback_option " == true ]] ; then
case " $_ans " in
1)
_ans = "y"
;;
2)
2026-04-23 20:52:59 +02:00
if fallback_to_previous_os_version; then
return 0
2026-04-20 20:22:10 +02:00
else
2026-04-23 20:52:59 +02:00
return 3
2026-04-20 20:22:10 +02:00
fi
;;
2026-04-23 20:52:59 +02:00
3)
return 2
;;
4)
return 4
;;
2026-04-20 20:22:10 +02:00
*)
2026-07-27 10:48:05 +02:00
# Unrecognized/empty input: treat as ignore, never as an implicit cancel
return 2
2026-04-23 20:52:59 +02:00
;;
esac
else
case " $_ans " in
1)
_ans = "y"
;;
2)
2026-04-20 20:22:10 +02:00
return 2
;;
2026-07-27 10:48:05 +02:00
3)
2026-04-23 20:52:59 +02:00
return 4
;;
2026-07-27 10:48:05 +02:00
*)
# Unrecognized/empty input: treat as ignore, never as an implicit cancel
return 2
;;
2026-04-20 20:22:10 +02:00
esac
fi
2025-12-04 07:52:18 +01:00
case " ${ _ans ,, } " in
y | yes)
2026-04-23 20:52:59 +02:00
msg_info "Running host upgrade for LXC stack compatibility"
2026-02-27 14:39:39 +01:00
apt_update_safe
2026-04-23 20:52:59 +02:00
if $STD apt-get upgrade -y; then
2025-12-04 07:52:18 +01:00
msg_ok "LXC stack upgraded."
2026-04-20 20:22:10 +02:00
# Verify pct binary still works after upgrade (partial upgrades can break Perl modules)
if ! pct list & >/dev/null; then
msg_error "LXC stack upgrade caused PVE tool breakage (likely Perl module incompatibility)."
msg_custom "⚠️" " ${ YW } " "A partial package upgrade has left the PVE stack in an inconsistent state."
msg_custom "🔧" " ${ YW } " "Please run the following on the Proxmox host, then retry:"
2026-04-23 20:52:59 +02:00
echo -e " ${ TAB } apt update && apt upgrade -y"
2026-04-20 20:22:10 +02:00
echo -e " ${ TAB } reboot"
return 3
fi
2025-12-04 07:52:18 +01:00
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
2026-06-23 07:39:36 +02:00
}
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
ARCH = " $( dpkg --print-architecture) "
# Maps OS type + version to the release variant name used by ARM64 template sources.
arm64_template_variant() {
case " $1 : $2 " in
2026-06-24 23:44:26 +02:00
debian:12) echo "bookworm" ;;
debian:13) echo "trixie" ;;
debian:) echo " $DEBIAN_DEFAULT_CODENAME " ;;
2026-06-11 16:14:32 +10:00
2026-06-24 23:44:26 +02:00
ubuntu:24.04) echo "noble" ;;
ubuntu:26.04) echo "questing" ;;
ubuntu:) echo " $UBUNTU_DEFAULT_CODENAME " ;;
2026-06-11 16:14:32 +10:00
2026-06-24 23:44:26 +02:00
alpine:*) echo " ${ 2 :- $ALPINE_DEFAULT_VERSION } " ;;
2026-06-11 16:14:32 +10:00
2026-06-24 23:44:26 +02:00
*) return 1 ;;
2026-06-11 16:14:32 +10:00
esac
}
2026-06-12 02:06:21 +10:00
# Downloads an ARM64 LXC rootfs template from jenkins.linuxcontainers.org.
2026-06-11 16:14:32 +10:00
download_arm64_template() {
local dest = " $1 " url
mkdir -p " $( dirname " $dest " ) " || {
msg_error "Cannot create template dir."
exit 207
}
2026-06-12 02:06:21 +10:00
url = "https://jenkins.linuxcontainers.org/job/image- ${ PCT_OSTYPE } /architecture=arm64,release= ${ CUSTOM_TEMPLATE_VARIANT } ,variant=default/lastStableBuild/artifact/rootfs.tar.xz"
2026-06-11 16:14:32 +10:00
msg_info "Downloading ${ PCT_OSTYPE ^ } ${ CUSTOM_TEMPLATE_VARIANT } ARM64 template"
2026-06-19 17:53:08 +10:00
local patched = " ${ dest %/* } /. ${ dest ##*/ } .patched"
(
flock -x 200
if [[ -f " $patched " ]] && [[ -s " $dest " ]] && xz -t " $dest " 2>/dev/null; then
exit 0
fi
if ! curl -fsSL -o " $dest " " $url " ; then
msg_error "Failed to download ARM64 template from: $url "
exit 208
fi
# Some linuxcontainers.org rootfs builds ship without /etc/network,
# which makes pve-container's post-create hook fail when it writes the
# interfaces file. This appends an empty /etc/network/interfaces if missing.
if ! tar -tJf " $dest " 2>/dev/null | grep -q '/etc/network/$' ; then
fixdir = $( mktemp -d)
tmptar = " ${ dest %.xz } "
mkdir -p " $fixdir /etc/network"
: >" $fixdir /etc/network/interfaces"
if xz -d -f " $dest " && tar -C " $fixdir " --append -f " $tmptar " ./etc/network && xz -1 -f " $tmptar " ; then
rm -rf " $fixdir "
else
rm -rf " $fixdir " " $tmptar "
msg_error "Failed to patch ARM64 template (missing /etc/network)"
exit 208
fi
fi
touch " $patched "
) 200>" ${ dest } .lock"
local dl_rc = $?
if [[ $dl_rc -ne 0 ]] ; then
exit " $dl_rc "
2026-06-11 16:14:32 +10:00
fi
msg_ok "Downloaded ARM64 LXC template"
}
download_template() {
local dest = " ${ 1 :- $TEMPLATE_PATH } "
if [[ " $ARCH " == "arm64" ]] ; then
download_arm64_template " $dest "
else
pveam download " $TEMPLATE_STORAGE " " $TEMPLATE " >>" ${ BUILD_LOG :- /dev/null } " 2>& 1 || {
msg_error "Failed to download template ' $TEMPLATE ' to storage ' $TEMPLATE_STORAGE '"
exit 222
}
fi
}
2025-12-04 07:52:18 +01:00
# ------------------------------------------------------------------------------
# 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
2026-04-07 23:10:37 +02:00
msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..."
CTID = $( get_valid_container_id " $(( CTID + 1 )) " )
export CTID
msg_ok "Reassigned to container ID $CTID "
2025-12-04 07:52:18 +01:00
fi
2026-02-18 16:19:19 +01:00
# Report installation start to API early - captures failures in storage/template/create
post_to_api
2026-02-23 17:01:18 +01:00
# Transition to 'validation' — Proxmox-internal checks (storage, template, cluster)
post_progress_to_api "validation"
2025-12-04 07:52:18 +01:00
# Storage capability check
check_storage_support "rootdir" || {
msg_error "No valid storage found for 'rootdir' [Container]"
2026-03-02 10:55:20 +01:00
exit 119
2025-12-04 07:52:18 +01:00
}
check_storage_support "vztmpl" || {
msg_error "No valid storage found for 'vztmpl' [Template]"
2026-03-02 10:55:20 +01:00
exit 120
2025-12-04 07:52:18 +01:00
}
# 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 '"
2026-04-20 20:22:10 +02:00
# Check if storage.cfg is accessible (pmxcfs must be mounted)
if [[ ! -f /etc/pve/storage.cfg ]] ; then
if ! mountpoint -q /etc/pve 2>/dev/null; then
msg_error "Proxmox cluster filesystem (pmxcfs) is not mounted at /etc/pve."
msg_custom "🔧" " ${ YW } " "Try: systemctl restart pve-cluster"
else
msg_error "/etc/pve/storage.cfg does not exist."
msg_custom "🔧" " ${ YW } " "Check Proxmox cluster filesystem integrity: pvecm status"
fi
exit 213
fi
STORAGE_TYPE = $( grep -E "^[^:]+:[[:space:]]* $CONTAINER_STORAGE [[:space:]]* $" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true )
# Fallback: use pvesm status to determine storage type
if [[ -z " $STORAGE_TYPE " ]] ; then
STORAGE_TYPE = $( pvesm status -storage " $CONTAINER_STORAGE " 2>/dev/null | awk 'NR>1{print $2}' )
fi
2025-12-04 19:41:01 +01:00
2026-02-27 13:59:02 +01:00
if [[ -z " $STORAGE_TYPE " ]] ; then
msg_error "Storage ' $CONTAINER_STORAGE ' not found in /etc/pve/storage.cfg"
2026-04-20 20:22:10 +02:00
msg_custom "📋" " ${ YW } " "Available storages: $( pvesm status 2>/dev/null | awk 'NR>1{printf "%s (%s) ", $1, $2}' || echo 'n/a' ) "
if [[ -r /etc/pve/storage.cfg ]] ; then
msg_custom "📋" " ${ YW } " "Storage definitions found in config:"
grep -E '^[a-z]+:' /etc/pve/storage.cfg 2>/dev/null | while IFS = read -r _line; do
echo " ${ TAB } $_line "
done
fi
msg_custom "📖" " ${ YW } " "See https://pve.proxmox.com/wiki/Storage for storage configuration details."
2026-02-27 13:59:02 +01:00
exit 213
fi
2025-12-04 19:41:01 +01:00
case " $STORAGE_TYPE " in
2026-02-27 13:59:02 +01:00
iscsidirect)
msg_error "Storage ' $CONTAINER_STORAGE ' uses iSCSI-direct which does not support container rootfs."
exit 212
;;
iscsi | zfs)
msg_error "Storage ' $CONTAINER_STORAGE ' ( $STORAGE_TYPE ) does not support container rootdir content."
exit 213
;;
cephfs)
msg_error "Storage ' $CONTAINER_STORAGE ' uses CephFS which is not supported for LXC rootfs."
exit 219
;;
pbs)
msg_error "Storage ' $CONTAINER_STORAGE ' is a Proxmox Backup Server — cannot be used for containers."
exit 224
;;
2025-12-04 19:41:01 +01:00
linstor | rbd | nfs | cifs)
2026-02-27 13:59:02 +01:00
if ! pvesm status -storage " $CONTAINER_STORAGE " & >/dev/null; then
msg_error "Storage ' $CONTAINER_STORAGE ' ( $STORAGE_TYPE ) is not accessible or inactive."
exit 217
fi
2025-12-04 19:41:01 +01:00
;;
esac
2025-12-04 07:52:18 +01:00
2026-02-27 13:59:02 +01:00
if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx " $CONTAINER_STORAGE " ; then
msg_error "Storage ' $CONTAINER_STORAGE ' ( $STORAGE_TYPE ) does not support 'rootdir' content."
2026-04-29 22:28:08 +02:00
msg_custom "💡" " ${ YW } " "Enable 'Disk image' (rootdir) for storage ' ${ CONTAINER_STORAGE } ' in:"
msg_custom " " " ${ YW } " "Datacenter → Storage → ${ CONTAINER_STORAGE } → Edit → Content"
msg_custom "📖" " ${ YW } " "See: https://pve.proxmox.com/wiki/Storage"
msg_custom "🔗" " ${ YW } " "Help: https://github.com/community-scripts/ProxmoxVE/discussions"
2026-02-27 13:59:02 +01:00
exit 213
fi
2025-12-04 19:41:01 +01:00
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 '"
2026-03-25 13:06:21 +01:00
TEMPLATE_TYPE = $( grep -E "^[^:]+: $TEMPLATE_STORAGE $" /etc/pve/storage.cfg | cut -d: -f1 || true )
2025-12-04 07:52:18 +01:00
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
# 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
# ------------------------------------------------------------------------------
2026-06-11 16:14:32 +10:00
CUSTOM_TEMPLATE_VARIANT = ""
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
if [[ " $ARCH " == "arm64" ]] ; then
# ARM64: use custom template download from linuxcontainers.org / GitHub
msg_info "Preparing ARM64 template"
2026-01-21 12:42:19 +01:00
2026-06-11 16:14:32 +10:00
CUSTOM_TEMPLATE_VARIANT = $( arm64_template_variant " $PCT_OSTYPE " " ${ PCT_OSVERSION :- } " ) || {
msg_error "No ARM64 template mapping for ${ PCT_OSTYPE } ${ PCT_OSVERSION :- latest } "
exit 207
}
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
TEMPLATE = " ${ PCT_OSTYPE } - ${ CUSTOM_TEMPLATE_VARIANT } -rootfs.tar.xz"
TEMPLATE_SOURCE = "custom-arm64"
# Resolve template path
TEMPLATE_PATH = " $( pvesm path " ${ TEMPLATE_STORAGE } :vztmpl/ ${ TEMPLATE } " 2>/dev/null || true ) "
if [[ -z " $TEMPLATE_PATH " ]] ; then
local _tpl_base
_tpl_base = $( awk -v s = " $TEMPLATE_STORAGE " '
$0 ~ "^[^:]+:[[:space:]]*" s "$" {f=1; next}
f && /^[^[:space:]]/ {f=0}
f && $1 == "path" {print $2; exit}
' /etc/pve/storage.cfg)
TEMPLATE_PATH = " ${ _tpl_base :- /var/lib/vz } /template/cache/ $TEMPLATE "
fi
2026-01-21 12:01:34 +01:00
2026-06-11 16:14:32 +10:00
# Download if missing, too small, or corrupt
if [[ ! -f " $TEMPLATE_PATH " ]] ; then
download_arm64_template " $TEMPLATE_PATH "
elif [[ " $( stat -c%s " $TEMPLATE_PATH " ) " -lt 1000000 ]] || ! tar -tf " $TEMPLATE_PATH " & >/dev/null; then
msg_warn "Local template invalid - re-downloading."
rm -f " $TEMPLATE_PATH "
download_arm64_template " $TEMPLATE_PATH "
2026-01-21 12:01:34 +01:00
else
2026-06-11 16:14:32 +10:00
msg_ok "Template ${ BL } $TEMPLATE ${ CL } found locally."
2026-01-21 12:01:34 +01:00
fi
2026-06-11 16:14:32 +10:00
else
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
2026-01-21 12:01:34 +01:00
2026-06-11 16:14:32 +10:00
msg_info "Searching for template ' $TEMPLATE_SEARCH '"
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
# Initialize variables
ONLINE_TEMPLATE = ""
ONLINE_TEMPLATES =()
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
# Step 1: Check local templates first (instant)
mapfile -t LOCAL_TEMPLATES < <(
pveam list " $TEMPLATE_STORAGE " 2>/dev/null |
2026-08-06 14:33:22 +02:00
awk -v search = " ${ TEMPLATE_SEARCH } " -v pattern = " ${ TEMPLATE_PATTERN } " -v arch = " ${ ARCH } " \
'$1 ~ search && $1 ~ pattern && $1 ~ ("_" arch "\\.(tar\\.zst|tar\\.xz|tar\\.gz)$") {print $1}' |
2026-06-11 16:14:32 +10:00
sed 's|.*/||' | sort -t - -k 2 -V
2025-12-04 07:52:18 +01:00
)
2026-06-11 16:14:32 +10:00
# Step 2: If local template found, use it immediately (skip pveam update)
if [[ ${# LOCAL_TEMPLATES [@] } -gt 0 ]] ; then
TEMPLATE = " ${ LOCAL_TEMPLATES [-1] } "
TEMPLATE_SOURCE = "local"
msg_ok "Template search completed"
else
# Step 3: No local template - need to check online (this may be slow)
msg_info "No local template found, checking online catalog..."
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
# Update catalog with timeout to prevent long hangs
if command -v timeout & >/dev/null; then
if ! timeout 30 pveam update >/dev/null 2>& 1; then
msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose."
2025-12-04 07:52:18 +01:00
fi
else
2026-06-11 16:14:32 +10:00
pveam update >/dev/null 2>& 1 || msg_warn "Could not update template catalog (pveam update failed)"
2025-12-04 07:52:18 +01:00
fi
2026-06-11 16:14:32 +10:00
ONLINE_TEMPLATES =()
2026-07-27 19:32:09 +02:00
mapfile -t ONLINE_TEMPLATES < <( _list_templates " $TEMPLATE_SEARCH " " $TEMPLATE_PATTERN " )
2026-06-11 16:14:32 +10:00
[[ ${# ONLINE_TEMPLATES [@] } -gt 0 ]] && ONLINE_TEMPLATE = " ${ ONLINE_TEMPLATES [-1] } "
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
TEMPLATE = " $ONLINE_TEMPLATE "
TEMPLATE_SOURCE = "online"
msg_ok "Template search completed"
fi
2025-12-04 07:52:18 +01:00
2026-07-27 19:32:09 +02:00
# resolve_os_version() already validated the version, so this means the
# variant (TEMPLATE_PATTERN) is missing - a catalog issue, don't auto-repair
2025-12-04 07:52:18 +01:00
if [[ -z " $TEMPLATE " ]] ; then
2026-07-27 19:32:09 +02:00
msg_error "No ${ TEMPLATE_PATTERN #- } template found for ${ PCT_OSTYPE } ${ PCT_OSVERSION } "
msg_custom "ℹ ️" " ${ YW } " "Refresh the catalog and check what this host offers:"
echo -e " ${ TAB } pveam update && pveam available -section system | grep ' ${ PCT_OSTYPE } -'"
exit 225
2025-12-04 07:52:18 +01:00
fi
2026-06-11 16:14:32 +10:00
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 "
2025-12-04 07:52:18 +01:00
fi
2026-06-11 16:14:32 +10:00
# 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 "
2025-12-04 07:52:18 +01:00
fi
2026-06-11 16:14:32 +10:00
[[ -n " $TEMPLATE_PATH " ]] || {
2026-07-27 19:32:09 +02:00
msg_error "Could not resolve a storage path for template ${ TEMPLATE } "
exit 220
2026-06-11 16:14:32 +10:00
}
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 it."
2025-12-04 07:52:18 +01:00
NEED_DOWNLOAD = 1
2026-06-11 16:14:32 +10:00
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
2025-12-04 07:52:18 +01:00
else
2026-06-11 16:14:32 +10:00
$STD msg_ok "Template $TEMPLATE is present and valid."
2025-12-04 07:52:18 +01:00
fi
2026-06-11 16:14:32 +10:00
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 "
2025-12-04 07:52:18 +01:00
fi
2026-06-11 16:14:32 +10:00
fi
2025-12-04 07:52:18 +01:00
2026-06-11 16:14:32 +10:00
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 " >>" ${ BUILD_LOG :- /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
2025-12-04 07:52:18 +01:00
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
2026-07-27 10:48:05 +02:00
local _lxc_stack_rc = 0
offer_lxc_stack_upgrade_and_maybe_retry "no" || _lxc_stack_rc = $?
# 4 = user chose "Cancel" -> honor it and abort before creating the container
if [[ $_lxc_stack_rc -eq 4 ]] ; then
exit_script
fi
2025-12-04 07:52:18 +01:00
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
2026-01-20 23:41:53 +01:00
# Lock by template file (avoid concurrent template downloads/validation)
2025-12-04 07:52:18 +01:00
lockfile = "/tmp/template. ${ TEMPLATE } .lock"
2026-01-20 23:41:53 +01:00
# Cleanup stale lock files (older than 1 hour - likely from crashed processes)
if [[ -f " $lockfile " ]] ; then
local lock_age = $(($( date +%s) - $( stat -c %Y " $lockfile " 2>/dev/null || echo 0)))
if [[ $lock_age -gt 3600 ]] ; then
msg_warn "Removing stale template lock file (age: ${ lock_age } s)"
rm -f " $lockfile "
fi
fi
2025-12-04 07:52:18 +01:00
exec 9>" $lockfile " || {
msg_error "Failed to create lock file ' $lockfile '."
exit 200
}
2026-01-20 23:41:53 +01:00
# Retry logic for template lock (another container creation may be running)
local lock_attempts = 0
local max_lock_attempts = 10
local lock_wait_time = 30
while ! flock -w " $lock_wait_time " 9; do
lock_attempts = $(( lock_attempts + 1 ))
if [[ $lock_attempts -ge $max_lock_attempts ]] ; then
msg_error "Timeout while waiting for template lock after ${ max_lock_attempts } attempts."
msg_custom "💡" " ${ YW } " "Another container creation may be stuck. Check running processes or remove: $lockfile "
exit 211
fi
msg_custom "⏳" " ${ YW } " "Another container is being created with this template. Waiting... (attempt ${ lock_attempts } / ${ max_lock_attempts } )"
done
2025-12-04 07:52:18 +01:00
LOGFILE = "/tmp/pct_create_ ${ CTID } _ $( date +%Y%m%d_%H%M%S) _ ${ SESSION_ID } .log"
2026-02-28 14:51:26 +01:00
# Helper: append pct_create log to BUILD_LOG before exit so combined log has full context
_flush_pct_log() {
if [[ -s " ${ LOGFILE :- } " && -n " ${ BUILD_LOG :- } " ]] ; then
{
echo ""
echo "--- pct create output ( ${ LOGFILE } ) ---"
cat " $LOGFILE "
echo "--- end pct create output ---"
} >>" $BUILD_LOG " 2>/dev/null || true
fi
}
2026-01-20 23:41:53 +01:00
# Validate template before pct create (while holding lock)
if [[ ! -s " $TEMPLATE_PATH " || " $( stat -c%s " $TEMPLATE_PATH " 2>/dev/null || echo 0) " -lt 1000000 ]] ; then
2026-06-11 16:14:32 +10:00
msg_info "Template file missing or too small - downloading"
2026-01-20 23:41:53 +01:00
rm -f " $TEMPLATE_PATH "
2026-06-11 16:14:32 +10:00
download_template
2026-01-20 23:41:53 +01:00
msg_ok "Template downloaded"
elif ! tar -tf " $TEMPLATE_PATH " & >/dev/null; then
2026-06-11 16:14:32 +10:00
if [[ " $ARCH " == "arm64" || -n " $ONLINE_TEMPLATE " ]] ; then
msg_info "Template appears corrupted - re-downloading"
2026-01-20 23:41:53 +01:00
rm -f " $TEMPLATE_PATH "
2026-06-11 16:14:32 +10:00
download_template
2026-01-20 23:41:53 +01:00
msg_ok "Template re-downloaded"
else
msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
fi
fi
# Release lock after template validation - pct create has its own internal locking
exec 9>& -
2025-12-04 07:52:18 +01:00
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)
2026-04-17 15:19:52 +02:00
# Disable globbing: unquoted $PCT_OPTIONS needs word-splitting but must not glob-expand
# (e.g. passwords containing * or ? would match filenames otherwise)
set -f
2025-12-04 07:52:18 +01:00
if ! pct create " $CTID " " ${ TEMPLATE_STORAGE } :vztmpl/ ${ TEMPLATE } " $PCT_OPTIONS >" $LOGFILE " 2>& 1; then
2026-01-20 23:41:53 +01:00
msg_debug "Container creation failed on ${ TEMPLATE_STORAGE } . Checking error..."
2025-12-04 07:52:18 +01:00
2026-04-20 20:22:10 +02:00
# Check for Perl module breakage (partial PVE upgrade)
if grep -qiE 'Compilation failed|Bareword.*not allowed' " $LOGFILE " ; then
msg_error "Container creation failed due to broken Perl modules on the PVE host."
msg_custom "⚠️" " ${ YW } " "This usually happens after a partial PVE package upgrade."
msg_custom "🔧" " ${ YW } " "Please run the following on the Proxmox host, then retry:"
echo -e " ${ TAB } apt update && apt dist-upgrade -y"
echo -e " ${ TAB } reboot"
_flush_pct_log
exit 232
fi
2026-04-07 23:10:37 +02:00
# Check if CTID collision (race condition: ID claimed between validation and creation)
if grep -qiE 'already exists|already in use' " $LOGFILE " ; then
local old_ctid = " $CTID "
CTID = $( get_valid_container_id " $(( CTID + 1 )) " )
export CTID
msg_warn "Container ID $old_ctid was claimed by another process. Retrying with ID $CTID "
LOGFILE = "/tmp/pct_create_ ${ CTID } _ $( date +%Y%m%d_%H%M%S) _ ${ SESSION_ID } .log"
if pct create " $CTID " " ${ TEMPLATE_STORAGE } :vztmpl/ ${ TEMPLATE } " $PCT_OPTIONS >" $LOGFILE " 2>& 1; then
msg_ok "Container successfully created with new ID $CTID "
else
msg_error "Container creation failed even with new ID $CTID . See $LOGFILE "
_flush_pct_log
exit 209
fi
else
# Not a CTID collision - check if template issue and retry with fresh download
if grep -qiE 'unable to open|corrupt|invalid' " $LOGFILE " ; then
2026-06-11 16:14:32 +10:00
msg_info "Template may be corrupted - re-downloading"
2026-04-07 23:10:37 +02:00
rm -f " $TEMPLATE_PATH "
2026-06-11 16:14:32 +10:00
download_template
2026-04-07 23:10:37 +02:00
msg_ok "Template re-downloaded"
fi
2025-12-04 07:52:18 +01:00
2026-04-07 23:10:37 +02:00
# 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_ok "Trying local storage fallback"
msg_info "Downloading template to local"
2026-06-11 16:14:32 +10:00
if [[ " $ARCH " == "arm64" ]] ; then
download_arm64_template " $LOCAL_TEMPLATE_PATH "
else
pveam download local " $TEMPLATE " >>" ${ BUILD_LOG :- /dev/null } " 2>& 1
fi
2026-04-07 23:10:37 +02:00
msg_ok "Template downloaded to local"
else
msg_ok "Trying local storage fallback"
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
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
offer_lxc_stack_upgrade_and_maybe_retry "yes"
rc = $?
case $rc in
0) : ;; # success - container created, continue
2026-04-23 20:52:59 +02:00
2)
msg_error "LXC stack upgrade ignored. Please inspect: $LOGFILE "
_flush_pct_log
exit 231
;;
3)
msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE "
_flush_pct_log
exit 231
;;
4)
msg_error "Cancelled by user."
_flush_pct_log
exit 231
2026-04-07 23:10:37 +02: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
_flush_pct_log
exit 209
fi
else
msg_ok "Container successfully created using local fallback."
fi
2026-01-20 23:41:53 +01:00
else
2026-04-07 23:10:37 +02:00
# Already on local storage and still failed - check LXC stack version
2025-12-04 07:52:18 +01:00
if grep -qiE 'unsupported .* version' " $LOGFILE " ; then
2026-02-27 13:59:02 +01:00
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
2025-12-04 07:52:18 +01:00
offer_lxc_stack_upgrade_and_maybe_retry "yes"
rc = $?
case $rc in
0) : ;; # success - container created, continue
2026-04-23 20:52:59 +02:00
2)
msg_error "LXC stack upgrade ignored. Please inspect: $LOGFILE "
_flush_pct_log
exit 231
;;
3)
msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE "
_flush_pct_log
exit 231
;;
4)
msg_error "Cancelled by user."
_flush_pct_log
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
2026-02-28 14:51:26 +01:00
_flush_pct_log
2025-12-04 07:52:18 +01:00
exit 209
fi
fi
else
2026-04-07 23:10:37 +02:00
msg_ok "Container successfully created after template repair."
2025-12-04 07:52:18 +01:00
fi
2026-04-07 23:10:37 +02:00
fi # close CTID collision else-branch
2025-12-04 07:52:18 +01:00
fi
2026-04-20 20:22:10 +02:00
set +f # re-enable globbing after pct create block
2025-12-04 07:52:18 +01:00
2026-04-07 23:10:37 +02:00
# Verify container exists (allow up to 10s for pmxcfs sync in clusters)
local _pct_visible = false
for _pct_check in { 1..10} ; do
if pct list | awk '{print $1}' | grep -qx " $CTID " ; then
_pct_visible = true
break
fi
sleep 1
done
if [[ " $_pct_visible " != true ]] ; then
msg_error "Container ID $CTID not listed in 'pct list' after 10s. See $LOGFILE "
msg_custom "🔧" " ${ YW } " "This can happen in clusters with pmxcfs sync delays."
2026-02-28 14:51:26 +01:00
_flush_pct_log
2025-12-04 07:52:18 +01:00
exit 215
2026-04-07 23:10:37 +02:00
fi
2025-12-04 07:52:18 +01:00
# Verify config rootfs
grep -q '^rootfs:' "/etc/pve/lxc/ $CTID .conf" || {
msg_error "RootFS entry missing in container config. See $LOGFILE "
2026-02-28 14:51:26 +01:00
_flush_pct_log
2025-12-04 07:52:18 +01:00
exit 216
}
msg_ok "LXC Container ${ BL } $CTID ${ CL } ${ GN } was successfully created."
2026-02-28 14:51:26 +01:00
# Append pct create log to BUILD_LOG for combined log visibility
if [[ -s " $LOGFILE " && -n " ${ BUILD_LOG :- } " ]] ; then
{
echo ""
echo "--- pct create output ---"
cat " $LOGFILE "
echo "--- end pct create output ---"
} >>" $BUILD_LOG " 2>/dev/null || true
fi
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)
2026-04-08 21:40:52 +02:00
local script_slug script_url donate_url
script_slug = " ${ SCRIPT_SLUG :- ${ NSAPP }} "
script_slug = " $( echo " $script_slug " | tr '[:upper:]' '[:lower:]' | tr ' ' '-' ) "
script_url = "https://community-scripts.org/scripts/ ${ script_slug } "
donate_url = "https://community-scripts.org/donate"
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'>
2026-03-25 17:48:03 +01:00
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
2024-11-12 16:54:39 +01:00
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
</a>
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
<p style='margin: 16px 0;'>
2026-04-08 21:40:52 +02:00
<a href='${donate_url}' target='_blank' rel='noopener noreferrer'>
<img src='https://img.shields.io/badge/❤️-Sponsoring%20%26%20Donations-FF5E5B' alt='Sponsoring and donations' />
</a>
</p>
<p style='margin: 12px 0;'>
<a href='${script_url}' target='_blank' rel='noopener noreferrer'>
<img src='https://img.shields.io/badge/📦-Open%20Script%20Page-00617f' alt='Open script page' />
2024-11-12 16:54:39 +01:00
</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
2026-05-11 15:54:38 +02:00
# Optional host-side post-install hook
# Path comes from var_post_install (default.vars / app.vars / advanced settings).
# Runs ON THE PROXMOX HOST after the container is up and configured.
# Exposed env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG.
# Output (stdout/stderr) is captured to /var/log/community-scripts/post-install-<CTID>.log
if [[ -n " ${ var_post_install :- } " ]] ; then
local _hook_log_dir = "/var/log/community-scripts"
local _hook_log = " ${ _hook_log_dir } /post-install- ${ CTID } .log"
mkdir -p " $_hook_log_dir " 2>/dev/null || true
if [[ ! -f " ${ var_post_install } " ]] ; then
msg_error "Post-install hook not found on host: ${ var_post_install } "
whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "POST-INSTALL HOOK FAILED" \
--msgbox "The configured post-install hook was not found on the Proxmox host:\n\n ${ var_post_install } \n\nThe LXC was created successfully, but the hook did NOT run." 14 72 || true
else
msg_info "Running post-install hook: ${ var_post_install } "
local _hook_rc = 0
APP = " $APP " NSAPP = " ${ NSAPP :- } " CTID = " $CTID " IP = " $IP " HN = " ${ HN :- } " \
STORAGE = " ${ STORAGE :- } " BRG = " ${ BRG :- } " \
bash " ${ var_post_install } " >" ${ _hook_log } " 2>& 1 || _hook_rc = $?
if [[ $_hook_rc -eq 0 ]] ; then
msg_ok "Post-install hook completed (log: ${ _hook_log } )"
else
msg_error "Post-install hook failed (rc= ${ _hook_rc } ) – see ${ _hook_log } "
local _hook_tail = ""
_hook_tail = " $( tail -n 15 " ${ _hook_log } " 2>/dev/null || true ) "
whiptail --backtitle "Proxmox VE Helper Scripts" \
--title "POST-INSTALL HOOK FAILED" \
--msgbox "Hook exited with code ${ _hook_rc } .\n\nScript: ${ var_post_install } \nLog: ${ _hook_log } \n\n--- Last log lines ---\n ${ _hook_tail } \n\nThe LXC itself was created successfully." 22 78 || true
fi
fi
fi
2026-03-24 09:57:43 +01:00
INSTALL_COMPLETE = true
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
# ==============================================================================
2026-02-14 13:41:09 +01:00
# ------------------------------------------------------------------------------
# ensure_log_on_host()
#
# - Ensures INSTALL_LOG points to a readable file on the host
# - If INSTALL_LOG points to a container path (e.g. /root/.install-*),
# tries to pull it from the container and create a combined log
# - This allows get_error_text() to find actual error output for telemetry
2026-02-18 13:14:59 +01:00
# - Uses timeout on pct pull to prevent hangs on dead/unresponsive containers
2026-02-14 13:41:09 +01:00
# ------------------------------------------------------------------------------
ensure_log_on_host() {
# Already readable on host? Nothing to do.
[[ -n " ${ INSTALL_LOG :- } " && -s " ${ INSTALL_LOG } " ]] && return 0
# Try pulling from container and creating combined log
if [[ -n " ${ CTID :- } " && -n " ${ SESSION_ID :- } " ]] && command -v pct & >/dev/null; then
local combined_log = "/tmp/ ${ NSAPP :- lxc } - ${ CTID } - ${ SESSION_ID } .log"
if [[ ! -s " $combined_log " ]] ; then
# Create combined log
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${ APP :- LXC } "
echo "Container ID: ${ CTID } "
echo "Session ID: ${ SESSION_ID } "
echo "Timestamp: $( date '+%Y-%m-%d %H:%M:%S' ) "
echo "================================================================================"
echo ""
} >" $combined_log " 2>/dev/null || return 0
# Append BUILD_LOG if it exists
if [[ -f " ${ BUILD_LOG :- } " ]] ; then
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat " ${ BUILD_LOG } "
echo ""
} >>" $combined_log "
fi
2026-02-18 13:14:59 +01:00
# Pull INSTALL_LOG from container (with timeout to prevent hangs on dead containers)
2026-02-14 13:41:09 +01:00
local temp_log = "/tmp/.install-temp- ${ SESSION_ID } .log"
2026-02-18 13:14:59 +01:00
if timeout 8 pct pull " $CTID " "/root/.install- ${ SESSION_ID } .log" " $temp_log " 2>/dev/null; then
2026-02-14 13:41:09 +01:00
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat " $temp_log "
echo ""
} >>" $combined_log "
rm -f " $temp_log "
fi
fi
2026-07-20 20:32:36 +02:00
# Also pull the structured error capture (.errinfo) so the telemetry error
# trace shows the failing command's exact output (signal-exit paths reach
# this via on_exit before/instead of the recovery flow)
if [[ -z " ${ TELEMETRY_ERRINFO :- } " || ! -s " ${ TELEMETRY_ERRINFO :- } " ]] ; then
local host_errinfo = "/tmp/.errinfo- ${ SESSION_ID } "
if timeout 8 pct pull " $CTID " "/root/.install- ${ SESSION_ID } .log.errinfo" " $host_errinfo " 2>/dev/null && [[ -s " $host_errinfo " ]] ; then
TELEMETRY_ERRINFO = " $host_errinfo "
export TELEMETRY_ERRINFO
fi
fi
2026-02-14 13:41:09 +01:00
if [[ -s " $combined_log " ]] ; then
INSTALL_LOG = " $combined_log "
fi
fi
}
2026-02-25 14:08:24 +01:00
# ==============================================================================
# TRAP MANAGEMENT
# ==============================================================================
# All traps (ERR, EXIT, INT, TERM, HUP) are set by catch_errors() in
# error_handler.func — called at the top of this file after sourcing.
2025-12-04 07:52:18 +01:00
#
2026-02-25 14:08:24 +01:00
# Do NOT set duplicate traps here. The handlers in error_handler.func
# (on_exit, on_interrupt, on_terminate, on_hangup, error_handler) already:
# - Send telemetry via post_update_to_api / _send_abort_telemetry
# - Stop orphaned containers via _stop_container_if_installing
# - Collect logs via ensure_log_on_host
# - Clean up lock files and spinner processes
#
# Previously, inline traps here overwrote catch_errors() traps, causing:
# - error_handler() never fired (no error output, no cleanup dialog)
# - on_hangup() never fired (SSH disconnect → stuck records)
# - Duplicated logic in two places (hard to debug)
# ==============================================================================