* Refactor Core Refactored misc/alpine-install.func to improve error handling, network checks, and MOTD setup. Added misc/alpine-tools.func and misc/error_handler.func for modular tool installation and error management. Enhanced misc/api.func with detailed exit code explanations and telemetry functions. Updated misc/core.func for better initialization, validation, and execution helpers. Removed misc/create_lxc.sh as part of cleanup. * Delete config-file.func * Update install.func * Refactor stop_all_services function and variable names Refactor service stopping logic and improve variable handling * Refactor installation script and update copyright Updated copyright information and adjusted package installation commands. Enhanced IPv6 disabling logic and improved container customization process. * Update install.func * Update license comment format in install.func * Refactor IPv6 handling and enhance MOTD and SSH Refactor IPv6 handling and update OS function. Enhance MOTD with additional details and configure SSH settings. * big core refactor * Enhance IPv6 configuration menu options Updated IPv6 Address Management menu options for clarity and added a new option for fully disabling IPv6. * Update default Node.js version to 24 LTS * Update misc/alpine-tools.func Co-authored-by: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com> * indention * remove debugf and duplicate codes * Update whiptail backtitles and error codes Removed '[dev]' from whiptail --backtitle strings for consistency. Refactored custom exit codes in build.func and error_handler.func: updated Proxmox error codes, shifted MySQL/MariaDB codes to 260-263, and removed unused MongoDB code. Updated error descriptions to match new codes. * comments * Refactor error handling and clean up debug comments Standardized bash variable checks, removed unnecessary debug and commented code, and clarified error handling logic in container build and setup scripts. These changes improve code readability and maintainability without altering functional behavior. * Update build.func * feat: Improve LXC network checks and LINSTOR storage handling Enhanced LXC container network setup to check for both IPv4 and IPv6 addresses, added connectivity (ping) tests, and provided troubleshooting tips on failure. Updated storage validation to support LINSTOR, including cluster connectivity checks and special handling for LINSTOR template storage. --------- Co-authored-by: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com>
3764 lines
138 KiB
Bash
3764 lines
138 KiB
Bash
#!/usr/bin/env bash
|
||
# Copyright (c) 2021-2025 community-scripts ORG
|
||
# 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)
|
||
# ------------------------------------------------------------------------------
|
||
variables() {
|
||
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
||
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
||
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
|
||
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
|
||
DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
|
||
METHOD="default" # sets the METHOD variable to "default", used for the API call.
|
||
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
|
||
SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
|
||
BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
|
||
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
|
||
|
||
# Parse dev_mode early
|
||
parse_dev_mode
|
||
|
||
# Setup persistent log directory if logs mode active
|
||
if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then
|
||
mkdir -p /var/log/community-scripts
|
||
BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log"
|
||
fi
|
||
|
||
# Get Proxmox VE version and kernel version
|
||
if command -v pveversion >/dev/null 2>&1; then
|
||
PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||
else
|
||
PVEVERSION="N/A"
|
||
fi
|
||
KERNEL_VERSION=$(uname -r)
|
||
|
||
# Capture app-declared defaults (for precedence logic)
|
||
# These values are set by the app script BEFORE default.vars is loaded
|
||
# If app declares higher values than default.vars, app values take precedence
|
||
if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then
|
||
export APP_DEFAULT_CPU="${var_cpu}"
|
||
fi
|
||
if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then
|
||
export APP_DEFAULT_RAM="${var_ram}"
|
||
fi
|
||
if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then
|
||
export APP_DEFAULT_DISK="${var_disk}"
|
||
fi
|
||
}
|
||
|
||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
|
||
|
||
if command -v curl >/dev/null 2>&1; then
|
||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
|
||
load_functions
|
||
catch_errors
|
||
elif command -v wget >/dev/null 2>&1; then
|
||
source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
||
source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
|
||
load_functions
|
||
catch_errors
|
||
fi
|
||
|
||
# ==============================================================================
|
||
# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maxkeys_check()
|
||
#
|
||
# - Reads kernel keyring limits (maxkeys, maxbytes)
|
||
# - Checks current usage for LXC user (UID 100000)
|
||
# - Warns if usage is close to limits and suggests sysctl tuning
|
||
# - Exits if thresholds are exceeded
|
||
# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
|
||
# ------------------------------------------------------------------------------
|
||
|
||
maxkeys_check() {
|
||
# Read kernel parameters
|
||
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
|
||
per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
|
||
|
||
# Exit if kernel parameters are unavailable
|
||
if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
|
||
echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
|
||
exit 1
|
||
fi
|
||
|
||
# Fetch key usage for user ID 100000 (typical for containers)
|
||
used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
|
||
used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
|
||
|
||
# Calculate thresholds and suggested new limits
|
||
threshold_keys=$((per_user_maxkeys - 100))
|
||
threshold_bytes=$((per_user_maxbytes - 1000))
|
||
new_limit_keys=$((per_user_maxkeys * 2))
|
||
new_limit_bytes=$((per_user_maxbytes * 2))
|
||
|
||
# Check if key or byte usage is near limits
|
||
failure=0
|
||
if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
|
||
echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
|
||
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||
failure=1
|
||
fi
|
||
if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
|
||
echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
|
||
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||
failure=1
|
||
fi
|
||
|
||
# Provide next steps if issues are detected
|
||
if [[ "$failure" -eq 1 ]]; then
|
||
echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
|
||
exit 1
|
||
fi
|
||
|
||
# Silent success - only show errors if they exist
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 3: CONTAINER SETUP UTILITIES
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_current_ip()
|
||
#
|
||
# - Returns current container IP depending on OS type
|
||
# - Debian/Ubuntu: uses `hostname -I`
|
||
# - Alpine: parses eth0 via `ip -4 addr`
|
||
# - Returns "Unknown" if OS type cannot be determined
|
||
# ------------------------------------------------------------------------------
|
||
get_current_ip() {
|
||
if [ -f /etc/os-release ]; then
|
||
# Check for Debian/Ubuntu (uses hostname -I)
|
||
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
|
||
CURRENT_IP=$(hostname -I | awk '{print $1}')
|
||
# Check for Alpine (uses ip command)
|
||
elif grep -q 'ID=alpine' /etc/os-release; then
|
||
CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
|
||
else
|
||
CURRENT_IP="Unknown"
|
||
fi
|
||
fi
|
||
echo "$CURRENT_IP"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# update_motd_ip()
|
||
#
|
||
# - Updates /etc/motd with current container IP
|
||
# - Removes old IP entries to avoid duplicates
|
||
# ------------------------------------------------------------------------------
|
||
update_motd_ip() {
|
||
MOTD_FILE="/etc/motd"
|
||
|
||
if [ -f "$MOTD_FILE" ]; then
|
||
# Remove existing IP Address lines to prevent duplication
|
||
sed -i '/IP Address:/d' "$MOTD_FILE"
|
||
|
||
IP=$(get_current_ip)
|
||
# Add the new IP address
|
||
echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# install_ssh_keys_into_ct()
|
||
#
|
||
# - Installs SSH keys into container root account if SSH is enabled
|
||
# - Uses pct push or direct input to authorized_keys
|
||
# - Falls back to warning if no keys provided
|
||
# ------------------------------------------------------------------------------
|
||
install_ssh_keys_into_ct() {
|
||
[[ "$SSH" != "yes" ]] && return 0
|
||
|
||
if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
|
||
msg_info "Installing selected SSH keys into CT ${CTID}"
|
||
pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || {
|
||
msg_error "prepare /root/.ssh failed"
|
||
return 1
|
||
}
|
||
pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
|
||
pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
|
||
msg_error "write authorized_keys failed"
|
||
return 1
|
||
}
|
||
pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
|
||
msg_ok "Installed SSH keys into CT ${CTID}"
|
||
return 0
|
||
fi
|
||
|
||
# Fallback
|
||
msg_warn "No SSH keys to install (skipping)."
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# find_host_ssh_keys()
|
||
#
|
||
# - Scans system for available SSH keys
|
||
# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
|
||
# - Returns list of files containing valid SSH public keys
|
||
# - Sets FOUND_HOST_KEY_COUNT to number of keys found
|
||
# ------------------------------------------------------------------------------
|
||
find_host_ssh_keys() {
|
||
local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
|
||
local -a files=() cand=()
|
||
local g="${var_ssh_import_glob:-}"
|
||
local total=0 f base c
|
||
|
||
shopt -s nullglob
|
||
if [[ -n "$g" ]]; then
|
||
for pat in $g; do cand+=($pat); done
|
||
else
|
||
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
|
||
cand+=(/root/.ssh/*.pub)
|
||
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
|
||
fi
|
||
shopt -u nullglob
|
||
|
||
for f in "${cand[@]}"; do
|
||
[[ -f "$f" && -r "$f" ]] || continue
|
||
base="$(basename -- "$f")"
|
||
case "$base" in
|
||
known_hosts | known_hosts.* | config) continue ;;
|
||
id_*) [[ "$f" != *.pub ]] && continue ;;
|
||
esac
|
||
|
||
# CRLF safe check for host keys
|
||
c=$(tr -d '\r' <"$f" | awk '
|
||
/^[[:space:]]*#/ {next}
|
||
/^[[:space:]]*$/ {next}
|
||
{print}
|
||
' | grep -E -c '"$re"' || true)
|
||
|
||
if ((c > 0)); then
|
||
files+=("$f")
|
||
total=$((total + c))
|
||
fi
|
||
done
|
||
|
||
# Fallback to /root/.ssh/authorized_keys
|
||
if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
|
||
if grep -E -q "$re" /root/.ssh/authorized_keys; then
|
||
files+=(/root/.ssh/authorized_keys)
|
||
total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
|
||
fi
|
||
fi
|
||
|
||
FOUND_HOST_KEY_COUNT="$total"
|
||
(
|
||
IFS=:
|
||
echo "${files[*]}"
|
||
)
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 4: STORAGE & RESOURCE MANAGEMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# _write_storage_to_vars()
|
||
#
|
||
# - Writes storage selection to vars file
|
||
# - Removes old entries (commented and uncommented) to avoid duplicates
|
||
# - Arguments: vars_file, key (var_container_storage/var_template_storage), value
|
||
# ------------------------------------------------------------------------------
|
||
_write_storage_to_vars() {
|
||
# $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
|
||
local vf="$1" key="$2" val="$3"
|
||
# remove uncommented and commented versions to avoid duplicates
|
||
sed -i "/^[#[:space:]]*${key}=/d" "$vf"
|
||
echo "${key}=${val}" >>"$vf"
|
||
}
|
||
|
||
choose_and_set_storage_for_file() {
|
||
# $1 = vars_file, $2 = class ('container'|'template')
|
||
local vf="$1" class="$2" key="" current=""
|
||
case "$class" in
|
||
container) key="var_container_storage" ;;
|
||
template) key="var_template_storage" ;;
|
||
*)
|
||
msg_error "Unknown storage class: $class"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
|
||
|
||
# If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
|
||
local content="rootdir"
|
||
[[ "$class" == "template" ]] && content="vztmpl"
|
||
local count
|
||
count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
|
||
|
||
if [[ "$count" -eq 1 ]]; then
|
||
STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
|
||
STORAGE_INFO=""
|
||
else
|
||
# If the current value is preselectable, we could show it, but per your requirement we always offer selection
|
||
select_storage "$class" || return 1
|
||
fi
|
||
|
||
_write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
|
||
|
||
# Keep environment in sync for later steps (e.g. app-default save)
|
||
if [[ "$class" == "container" ]]; then
|
||
export var_container_storage="$STORAGE_RESULT"
|
||
export CONTAINER_STORAGE="$STORAGE_RESULT"
|
||
else
|
||
export var_template_storage="$STORAGE_RESULT"
|
||
export TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||
fi
|
||
|
||
# Silent operation - no output message
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# base_settings()
|
||
#
|
||
# - Defines all base/default variables for container creation
|
||
# - Reads from environment variables (var_*)
|
||
# - Provides fallback defaults for OS type/version
|
||
# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK)
|
||
# - Sets up container type, resources, network, SSH, features, and tags
|
||
# ------------------------------------------------------------------------------
|
||
base_settings() {
|
||
# Default Settings
|
||
CT_TYPE=${var_unprivileged:-"1"}
|
||
|
||
# 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}"
|
||
VERBOSE=${var_verbose:-"${1:-no}"}
|
||
PW=${var_pw:-""}
|
||
CT_ID=${var_ctid:-$NEXTID}
|
||
HN=${var_hostname:-$NSAPP}
|
||
BRG=${var_brg:-"vmbr0"}
|
||
NET=${var_net:-"dhcp"}
|
||
IPV6_METHOD=${var_ipv6_method:-"none"}
|
||
IPV6_STATIC=${var_ipv6_static:-""}
|
||
GATE=${var_gateway:-""}
|
||
APT_CACHER=${var_apt_cacher:-""}
|
||
APT_CACHER_IP=${var_apt_cacher_ip:-""}
|
||
|
||
# Runtime check: Verify APT cacher is reachable if configured
|
||
if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
|
||
if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then
|
||
msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142"
|
||
msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation"
|
||
APT_CACHER=""
|
||
APT_CACHER_IP=""
|
||
else
|
||
msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142"
|
||
fi
|
||
fi
|
||
|
||
MTU=${var_mtu:-""}
|
||
SD=${var_storage:-""}
|
||
NS=${var_ns:-""}
|
||
MAC=${var_mac:-""}
|
||
VLAN=${var_vlan:-""}
|
||
SSH=${var_ssh:-"no"}
|
||
SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
|
||
UDHCPC_FIX=${var_udhcpc_fix:-""}
|
||
TAGS="community-script,${var_tags:-}"
|
||
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
|
||
ENABLE_TUN=${var_tun:-"${1:-no}"}
|
||
|
||
# Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
|
||
if [ -z "$var_os" ]; then
|
||
var_os="debian"
|
||
fi
|
||
if [ -z "$var_version" ]; then
|
||
var_version="12"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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
|
||
# ------------------------------------------------------------------------------
|
||
load_vars_file() {
|
||
local file="$1"
|
||
[ -f "$file" ] || return 0
|
||
msg_info "Loading defaults from ${file}"
|
||
|
||
# Allowed var_* keys
|
||
local VAR_WHITELIST=(
|
||
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl
|
||
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
|
||
var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
|
||
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
|
||
)
|
||
|
||
# Whitelist check helper
|
||
_is_whitelisted() {
|
||
local k="$1" w
|
||
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
|
||
return 1
|
||
}
|
||
|
||
local line key val
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
line="${line#"${line%%[![:space:]]*}"}"
|
||
line="${line%"${line##*[![:space:]]}"}"
|
||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
|
||
local var_key="${BASH_REMATCH[1]}"
|
||
local var_val="${BASH_REMATCH[2]}"
|
||
|
||
[[ "$var_key" != var_* ]] && continue
|
||
_is_whitelisted "$var_key" || continue
|
||
|
||
# Strip quotes
|
||
if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
|
||
var_val="${BASH_REMATCH[1]}"
|
||
elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
|
||
var_val="${BASH_REMATCH[1]}"
|
||
fi
|
||
|
||
# Set only if not already exported
|
||
[[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
|
||
fi
|
||
done <"$file"
|
||
msg_ok "Loaded ${file}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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=(
|
||
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl
|
||
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
|
||
var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
|
||
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
|
||
)
|
||
|
||
# Snapshot: environment variables (highest precedence)
|
||
declare -A _HARD_ENV=()
|
||
local _k
|
||
for _k in "${VAR_WHITELIST[@]}"; do
|
||
if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
|
||
done
|
||
|
||
# Find default.vars location
|
||
local _find_default_vars
|
||
_find_default_vars() {
|
||
local f
|
||
for f in \
|
||
/usr/local/community-scripts/default.vars \
|
||
"$HOME/.config/community-scripts/default.vars" \
|
||
"./default.vars"; do
|
||
[ -f "$f" ] && {
|
||
echo "$f"
|
||
return 0
|
||
}
|
||
done
|
||
return 1
|
||
}
|
||
# Allow override of storages via env (for non-interactive use cases)
|
||
[ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
|
||
[ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
|
||
|
||
# Create once, with storages already selected, no var_ctid/var_hostname lines
|
||
local _ensure_default_vars
|
||
_ensure_default_vars() {
|
||
_find_default_vars >/dev/null 2>&1 && return 0
|
||
|
||
local canonical="/usr/local/community-scripts/default.vars"
|
||
# Silent creation - no msg_info output
|
||
mkdir -p /usr/local/community-scripts
|
||
|
||
# Pick storages before writing the file (always ask unless only one)
|
||
# Create a minimal temp file to write into
|
||
: >"$canonical"
|
||
|
||
# Base content (no var_ctid / var_hostname here)
|
||
cat >"$canonical" <<'EOF'
|
||
# Community-Scripts defaults (var_* only). Lines starting with # are comments.
|
||
# Precedence: ENV var_* > default.vars > built-ins.
|
||
# Keep keys alphabetically sorted.
|
||
|
||
# Container type
|
||
var_unprivileged=1
|
||
|
||
# Resources
|
||
var_cpu=1
|
||
var_disk=4
|
||
var_ram=1024
|
||
|
||
# Network
|
||
var_brg=vmbr0
|
||
var_net=dhcp
|
||
var_ipv6_method=none
|
||
# var_gateway=
|
||
# var_vlan=
|
||
# var_mtu=
|
||
# var_mac=
|
||
# var_ns=
|
||
|
||
# SSH
|
||
var_ssh=no
|
||
# var_ssh_authorized_key=
|
||
|
||
# APT cacher (optional - with example)
|
||
# var_apt_cacher=yes
|
||
# var_apt_cacher_ip=192.168.1.10
|
||
|
||
# Features/Tags/verbosity
|
||
var_fuse=no
|
||
var_tun=no
|
||
|
||
# Advanced Settings (Proxmox-official features)
|
||
var_nesting=1 # Allow nesting (required for Docker/LXC in CT)
|
||
var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround)
|
||
var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental)
|
||
var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults)
|
||
var_protection=no # Prevent accidental deletion of container
|
||
var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone)
|
||
var_tags=community-script
|
||
var_verbose=no
|
||
|
||
# Security (root PW) – empty => autologin
|
||
# var_pw=
|
||
EOF
|
||
|
||
# Now choose storages (always prompt unless just one exists)
|
||
choose_and_set_storage_for_file "$canonical" template
|
||
choose_and_set_storage_for_file "$canonical" container
|
||
|
||
chmod 0644 "$canonical"
|
||
# Silent creation - no output message
|
||
}
|
||
|
||
# Whitelist check
|
||
local _is_whitelisted_key
|
||
_is_whitelisted_key() {
|
||
local k="$1"
|
||
local w
|
||
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
|
||
return 1
|
||
}
|
||
|
||
# 1) Ensure file exists
|
||
_ensure_default_vars
|
||
|
||
# 2) Load file
|
||
local dv
|
||
dv="$(_find_default_vars)" || {
|
||
msg_error "default.vars not found after ensure step"
|
||
return 1
|
||
}
|
||
load_vars_file "$dv"
|
||
|
||
# 3) Map var_verbose → VERBOSE
|
||
if [[ -n "${var_verbose:-}" ]]; then
|
||
case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
|
||
else
|
||
VERBOSE="no"
|
||
fi
|
||
|
||
# 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
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_app_defaults_path()
|
||
#
|
||
# - Returns full path for app-specific defaults file
|
||
# - Example: /usr/local/community-scripts/defaults/<app>.vars
|
||
# ------------------------------------------------------------------------------
|
||
|
||
get_app_defaults_path() {
|
||
local n="${NSAPP:-${APP,,}}"
|
||
echo "/usr/local/community-scripts/defaults/${n}.vars"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maybe_offer_save_app_defaults
|
||
#
|
||
# - Called after advanced_settings returned with fully chosen values.
|
||
# - If no <nsapp>.vars exists, offers to persist current advanced settings
|
||
# into /usr/local/community-scripts/defaults/<nsapp>.vars
|
||
# - Only writes whitelisted var_* keys.
|
||
# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
|
||
# ------------------------------------------------------------------------------
|
||
if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
|
||
# Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
|
||
declare -ag VAR_WHITELIST=(
|
||
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse
|
||
var_gateway var_hostname var_ipv6_method var_mac var_mtu
|
||
var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged
|
||
var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
|
||
)
|
||
fi
|
||
|
||
# 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
|
||
}
|
||
|
||
_sanitize_value() {
|
||
# Disallow Command-Substitution / Shell-Meta
|
||
case "$1" in
|
||
*'$('* | *'`'* | *';'* | *'&'* | *'<('*)
|
||
echo ""
|
||
return 0
|
||
;;
|
||
esac
|
||
echo "$1"
|
||
}
|
||
|
||
# Map-Parser: read var_* from file into _VARS_IN associative array
|
||
# 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"
|
||
fi
|
||
;;
|
||
esac
|
||
done <"$file"
|
||
}
|
||
|
||
# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
|
||
_build_vars_diff() {
|
||
local oldf="$1" newf="$2"
|
||
local k
|
||
local -A OLD=() NEW=()
|
||
_load_vars_file_to_map "$oldf"
|
||
for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
|
||
_load_vars_file_to_map "$newf"
|
||
for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
|
||
|
||
local out
|
||
out+="# Diff for ${APP} (${NSAPP})\n"
|
||
out+="# Old: ${oldf}\n# New: ${newf}\n\n"
|
||
|
||
local found_change=0
|
||
|
||
# Changed & Removed
|
||
for k in "${!OLD[@]}"; do
|
||
if [[ -v NEW["$k"] ]]; then
|
||
if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
|
||
out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
|
||
found_change=1
|
||
fi
|
||
else
|
||
out+="- ${k}\n - old: ${OLD[$k]}\n"
|
||
found_change=1
|
||
fi
|
||
done
|
||
|
||
# Added
|
||
for k in "${!NEW[@]}"; do
|
||
if [[ ! -v OLD["$k"] ]]; then
|
||
out+="+ ${k}\n + new: ${NEW[$k]}\n"
|
||
found_change=1
|
||
fi
|
||
done
|
||
|
||
if [[ $found_change -eq 0 ]]; then
|
||
out+="(No differences)\n"
|
||
fi
|
||
|
||
printf "%b" "$out"
|
||
}
|
||
|
||
# Build a temporary <app>.vars file from current advanced settings
|
||
_build_current_app_vars_tmp() {
|
||
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
|
||
|
||
# NET/GW
|
||
_net="${NET:-}"
|
||
_gate=""
|
||
case "${GATE:-}" in
|
||
,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
|
||
esac
|
||
|
||
# IPv6
|
||
_ipv6_method="${IPV6_METHOD:-auto}"
|
||
_ipv6_static=""
|
||
_ipv6_gateway=""
|
||
if [ "$_ipv6_method" = "static" ]; then
|
||
_ipv6_static="${IPV6_ADDR:-}"
|
||
_ipv6_gateway="${IPV6_GATE:-}"
|
||
fi
|
||
|
||
# MTU/VLAN/MAC
|
||
_mtu=""
|
||
_vlan=""
|
||
_mac=""
|
||
case "${MTU:-}" in
|
||
,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
|
||
esac
|
||
case "${VLAN:-}" in
|
||
,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
|
||
esac
|
||
case "${MAC:-}" in
|
||
,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
|
||
esac
|
||
|
||
# DNS / Searchdomain
|
||
_ns=""
|
||
_searchdomain=""
|
||
case "${NS:-}" in
|
||
-nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
|
||
esac
|
||
case "${SD:-}" in
|
||
-searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
|
||
esac
|
||
|
||
# SSH / APT / Features
|
||
_ssh="${SSH:-no}"
|
||
_ssh_auth="${SSH_AUTHORIZED_KEY:-}"
|
||
_apt_cacher="${APT_CACHER:-}"
|
||
_apt_cacher_ip="${APT_CACHER_IP:-}"
|
||
_fuse="${ENABLE_FUSE:-no}"
|
||
_tun="${ENABLE_TUN:-no}"
|
||
_nesting="${ENABLE_NESTING:-1}"
|
||
_keyctl="${ENABLE_KEYCTL:-0}"
|
||
_mknod="${ENABLE_MKNOD:-0}"
|
||
_mount_fs="${ALLOW_MOUNT_FS:-}"
|
||
_protect="${PROTECT_CT:-no}"
|
||
_timezone="${CT_TIMEZONE:-}"
|
||
_tags="${TAGS:-}"
|
||
_verbose="${VERBOSE:-no}"
|
||
|
||
# Type / Resources / Identity
|
||
_unpriv="${CT_TYPE:-1}"
|
||
_cpu="${CORE_COUNT:-1}"
|
||
_ram="${RAM_SIZE:-1024}"
|
||
_disk="${DISK_SIZE:-4}"
|
||
_hostname="${HN:-$NSAPP}"
|
||
|
||
# Storage
|
||
_tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
|
||
_ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
|
||
|
||
{
|
||
echo "# App-specific defaults for ${APP} (${NSAPP})"
|
||
echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||
echo
|
||
|
||
echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
|
||
echo "var_cpu=$(_sanitize_value "$_cpu")"
|
||
echo "var_ram=$(_sanitize_value "$_ram")"
|
||
echo "var_disk=$(_sanitize_value "$_disk")"
|
||
|
||
[ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
|
||
[ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
|
||
[ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
|
||
[ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
|
||
[ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
|
||
[ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
|
||
[ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
|
||
|
||
[ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
|
||
# var_ipv6_static removed - static IPs are unique, can't be default
|
||
|
||
[ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
|
||
[ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
|
||
|
||
[ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
|
||
[ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
|
||
|
||
[ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
|
||
[ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
|
||
[ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")"
|
||
[ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")"
|
||
[ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")"
|
||
[ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")"
|
||
[ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")"
|
||
[ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")"
|
||
[ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
|
||
[ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
|
||
|
||
[ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
|
||
[ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
|
||
|
||
[ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
|
||
[ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
|
||
} >"$tmpf"
|
||
|
||
echo "$tmpf"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maybe_offer_save_app_defaults()
|
||
#
|
||
# - Called after advanced_settings()
|
||
# - Offers to save current values as app defaults if not existing
|
||
# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
|
||
# ------------------------------------------------------------------------------
|
||
maybe_offer_save_app_defaults() {
|
||
local app_vars_path
|
||
app_vars_path="$(get_app_defaults_path)"
|
||
|
||
# always build from current settings
|
||
local new_tmp diff_tmp
|
||
new_tmp="$(_build_current_app_vars_tmp)"
|
||
diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
|
||
|
||
# 1) if no file → offer to create
|
||
if [[ ! -f "$app_vars_path" ]]; then
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then
|
||
mkdir -p "$(dirname "$app_vars_path")"
|
||
install -m 0644 "$new_tmp" "$app_vars_path"
|
||
msg_ok "Saved app defaults: ${app_vars_path}"
|
||
fi
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
return 0
|
||
fi
|
||
|
||
# 2) if file exists → build diff
|
||
_build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
|
||
|
||
# if no differences → do nothing
|
||
if grep -q "^(No differences)$" "$diff_tmp"; then
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
return 0
|
||
fi
|
||
|
||
# 3) if file exists → show menu with default selection "Update Defaults"
|
||
local app_vars_file
|
||
app_vars_file="$(basename "$app_vars_path")"
|
||
|
||
while true; do
|
||
local sel
|
||
sel="$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "APP DEFAULTS – ${APP}" \
|
||
--menu "Differences detected. What do you want to do?" 20 78 10 \
|
||
"Update Defaults" "Write new values to ${app_vars_file}" \
|
||
"Keep Current" "Keep existing defaults (no changes)" \
|
||
"View Diff" "Show a detailed diff" \
|
||
"Cancel" "Abort without changes" \
|
||
--default-item "Update Defaults" \
|
||
3>&1 1>&2 2>&3)" || { sel="Cancel"; }
|
||
|
||
case "$sel" in
|
||
"Update Defaults")
|
||
install -m 0644 "$new_tmp" "$app_vars_path"
|
||
msg_ok "Updated app defaults: ${app_vars_path}"
|
||
break
|
||
;;
|
||
"Keep Current")
|
||
msg_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
|
||
done
|
||
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
}
|
||
|
||
ensure_storage_selection_for_vars_file() {
|
||
local vf="$1"
|
||
|
||
# Read stored values (if any)
|
||
local tpl ct
|
||
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-)
|
||
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-)
|
||
|
||
if [[ -n "$tpl" && -n "$ct" ]]; then
|
||
TEMPLATE_STORAGE="$tpl"
|
||
CONTAINER_STORAGE="$ct"
|
||
return 0
|
||
fi
|
||
|
||
choose_and_set_storage_for_file "$vf" template
|
||
choose_and_set_storage_for_file "$vf" container
|
||
|
||
# Silent operation - no output message
|
||
}
|
||
|
||
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"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 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
|
||
|
||
# Initialize defaults
|
||
TAGS="community-script;${var_tags:-}"
|
||
local STEP=1
|
||
local MAX_STEP=19
|
||
|
||
# Store values for back navigation
|
||
local _ct_type="${CT_TYPE:-1}"
|
||
local _pw=""
|
||
local _pw_display="Automatic Login"
|
||
local _ct_id="$NEXTID"
|
||
local _hostname="$NSAPP"
|
||
local _disk_size="$var_disk"
|
||
local _core_count="$var_cpu"
|
||
local _ram_size="$var_ram"
|
||
local _bridge="vmbr0"
|
||
local _net="dhcp"
|
||
local _gate=""
|
||
local _ipv6_method="auto"
|
||
local _ipv6_addr=""
|
||
local _ipv6_gate=""
|
||
local _apt_cacher_ip=""
|
||
local _mtu=""
|
||
local _sd=""
|
||
local _ns=""
|
||
local _mac=""
|
||
local _vlan=""
|
||
local _tags="$TAGS"
|
||
local _enable_fuse="no"
|
||
local _verbose="no"
|
||
local _enable_keyctl="0"
|
||
local _enable_mknod="0"
|
||
local _mount_fs=""
|
||
local _protect_ct="no"
|
||
local _ct_timezone=""
|
||
|
||
# Helper to show current progress
|
||
show_progress() {
|
||
local current=$1
|
||
local total=$MAX_STEP
|
||
echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}"
|
||
}
|
||
|
||
# Detect available bridges (do this once)
|
||
local BRIDGES=""
|
||
local BRIDGE_MENU_OPTIONS=()
|
||
_detect_bridges() {
|
||
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null)
|
||
BRIDGES=""
|
||
local OLD_IFS=$IFS
|
||
IFS=$'\n'
|
||
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
|
||
local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
|
||
(grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true
|
||
if [ -f "${iface_indexes_tmpfile}" ]; then
|
||
while read -r pair; do
|
||
local start=$(echo "${pair}" | cut -d':' -f1)
|
||
local end=$(echo "${pair}" | cut -d':' -f2)
|
||
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
|
||
local iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
|
||
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
|
||
fi
|
||
done <"${iface_indexes_tmpfile}"
|
||
rm -f "${iface_indexes_tmpfile}"
|
||
fi
|
||
done
|
||
IFS=$OLD_IFS
|
||
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
|
||
|
||
# Build bridge menu
|
||
BRIDGE_MENU_OPTIONS=()
|
||
if [[ -n "$BRIDGES" ]]; then
|
||
while IFS= read -r bridge; do
|
||
if [[ -n "$bridge" ]]; then
|
||
local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//')
|
||
BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }")
|
||
fi
|
||
done <<<"$BRIDGES"
|
||
fi
|
||
}
|
||
_detect_bridges
|
||
|
||
# Main wizard loop
|
||
while [ $STEP -le $MAX_STEP ]; do
|
||
case $STEP in
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 2: Root Password
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
2)
|
||
if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "ROOT PASSWORD" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \
|
||
3>&1 1>&2 2>&3); then
|
||
|
||
if [[ -z "$PW1" ]]; then
|
||
_pw=""
|
||
_pw_display="Automatic Login"
|
||
((STEP++))
|
||
elif [[ "$PW1" == *" "* ]]; then
|
||
whiptail --msgbox "Password cannot contain spaces." 8 58
|
||
elif ((${#PW1} < 5)); then
|
||
whiptail --msgbox "Password must be at least 5 characters." 8 58
|
||
else
|
||
# Verify password
|
||
if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "PASSWORD VERIFICATION" \
|
||
--ok-button "Confirm" --cancel-button "Back" \
|
||
--passwordbox "\nVerify Root Password" 10 58 \
|
||
3>&1 1>&2 2>&3); then
|
||
if [[ "$PW1" == "$PW2" ]]; then
|
||
_pw="-password $PW1"
|
||
_pw_display="********"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Passwords do not match. Please try again." 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 3: Container ID
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
3)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER ID" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Container ID" 10 58 "$_ct_id" \
|
||
3>&1 1>&2 2>&3); then
|
||
_ct_id="${result:-$NEXTID}"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 4: Hostname
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
4)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "HOSTNAME" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \
|
||
3>&1 1>&2 2>&3); then
|
||
local hn_test="${result:-$NSAPP}"
|
||
hn_test=$(echo "${hn_test,,}" | tr -d ' ')
|
||
if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then
|
||
_hostname="$hn_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58
|
||
fi
|
||
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++))
|
||
else
|
||
whiptail --msgbox "Disk size must be a positive integer!" 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 8: Network Bridge
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
8)
|
||
if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then
|
||
_bridge="vmbr0"
|
||
((STEP++))
|
||
else
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "NETWORK BRIDGE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--menu "\nSelect network bridge:" 16 58 6 \
|
||
"${BRIDGE_MENU_OPTIONS[@]}" \
|
||
3>&1 1>&2 2>&3); then
|
||
_bridge="${result:-vmbr0}"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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" \
|
||
--menu "\nSelect IPv4 Address Assignment:" 14 60 2 \
|
||
"dhcp" "Automatic (DHCP, recommended)" \
|
||
"static" "Static (manual entry)" \
|
||
3>&1 1>&2 2>&3); then
|
||
|
||
if [[ "$result" == "static" ]]; then
|
||
# Get static IP
|
||
local static_ip
|
||
if static_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "STATIC IPv4 ADDRESS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
|
||
# Get gateway
|
||
local gateway_ip
|
||
if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "GATEWAY IP" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nEnter Gateway IP address" 10 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
_net="$static_ip"
|
||
_gate=",gw=$gateway_ip"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid Gateway IP format." 8 58
|
||
fi
|
||
fi
|
||
else
|
||
whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58
|
||
fi
|
||
fi
|
||
else
|
||
_net="dhcp"
|
||
_gate=""
|
||
((STEP++))
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
|
||
_ipv6_method="$result"
|
||
case "$result" in
|
||
static)
|
||
local ipv6_addr
|
||
if ipv6_addr=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "STATIC IPv6 ADDRESS" \
|
||
--inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then
|
||
_ipv6_addr="$ipv6_addr"
|
||
# Optional gateway
|
||
_ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "IPv6 GATEWAY" \
|
||
--inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \
|
||
3>&1 1>&2 2>&3) || true
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid IPv6 CIDR format." 8 58
|
||
fi
|
||
fi
|
||
;;
|
||
dhcp)
|
||
_ipv6_addr="dhcp"
|
||
_ipv6_gate=""
|
||
((STEP++))
|
||
;;
|
||
|
||
none)
|
||
_ipv6_addr="none"
|
||
_ipv6_gate=""
|
||
((STEP++))
|
||
;;
|
||
*)
|
||
_ipv6_addr=""
|
||
_ipv6_gate=""
|
||
((STEP++))
|
||
;;
|
||
esac
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 11: MTU Size
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
11)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "MTU SIZE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
_mtu="$result"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 14: MAC Address
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
14)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "MAC ADDRESS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
_mac="$result"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 15: VLAN Tag
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
15)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "VLAN TAG" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
_vlan="$result"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 16: Tags
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
16)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER TAGS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \
|
||
3>&1 1>&2 2>&3); then
|
||
_tags="${result:-;}"
|
||
_tags=$(echo "$_tags" | tr -d '[:space:]')
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 17: SSH Settings
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
17)
|
||
configure_ssh_settings
|
||
# configure_ssh_settings handles its own flow, always advance
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 18: FUSE & Verbose Mode
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
18)
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "FUSE SUPPORT" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--defaultno \
|
||
--yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc." 12 58; then
|
||
_enable_fuse="yes"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_fuse="no"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "VERBOSE MODE" \
|
||
--defaultno \
|
||
--yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then
|
||
_verbose="yes"
|
||
else
|
||
_verbose="no"
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 19: Confirmation
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
19)
|
||
# Build summary
|
||
local ct_type_desc="Unprivileged"
|
||
[[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged"
|
||
|
||
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
|
||
|
||
Options:
|
||
FUSE: $_enable_fuse
|
||
Verbose: $_verbose"
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONFIRM SETTINGS" \
|
||
--ok-button "Create LXC" --cancel-button "Back" \
|
||
--yesno "$summary\n\nCreate ${APP} LXC with these settings?" 26 58; then
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# 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"
|
||
VERBOSE="$_verbose"
|
||
|
||
# Format optional values
|
||
[[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU=""
|
||
[[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD=""
|
||
[[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS=""
|
||
[[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC=""
|
||
[[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN=""
|
||
|
||
# Alpine UDHCPC fix
|
||
if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then
|
||
UDHCPC_FIX="yes"
|
||
else
|
||
UDHCPC_FIX="no"
|
||
fi
|
||
export UDHCPC_FIX
|
||
export SSH_KEYS_FILE
|
||
|
||
# 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}"
|
||
echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}$ENABLE_FUSE${CL}"
|
||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
|
||
echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 7: USER INTERFACE & DIAGNOSTICS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# diagnostics_check()
|
||
#
|
||
# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
|
||
# - Asks user whether to send anonymous diagnostic data
|
||
# - Saves DIAGNOSTICS=yes/no in the config file
|
||
# - Creates file if missing with default DIAGNOSTICS=yes
|
||
# - Reads current diagnostics setting from file
|
||
# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
|
||
# ------------------------------------------------------------------------------
|
||
diagnostics_check() {
|
||
if ! [ -d "/usr/local/community-scripts" ]; then
|
||
mkdir -p /usr/local/community-scripts
|
||
fi
|
||
|
||
if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then
|
||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then
|
||
cat <<EOF >/usr/local/community-scripts/diagnostics
|
||
DIAGNOSTICS=yes
|
||
|
||
#This file is used to store the diagnostics settings for the Community-Scripts API.
|
||
#https://github.com/community-scripts/ProxmoxVE/discussions/1836
|
||
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
|
||
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
|
||
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||
#This will disable the diagnostics feature.
|
||
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||
#This will enable the diagnostics feature.
|
||
#The following information will be sent:
|
||
#"disk_size"
|
||
#"core_count"
|
||
#"ram_size"
|
||
#"os_type"
|
||
#"os_version"
|
||
#"nsapp"
|
||
#"method"
|
||
#"pve_version"
|
||
#"status"
|
||
#If you have any concerns, please review the source code at /misc/build.func
|
||
EOF
|
||
DIAGNOSTICS="yes"
|
||
else
|
||
cat <<EOF >/usr/local/community-scripts/diagnostics
|
||
DIAGNOSTICS=no
|
||
|
||
#This file is used to store the diagnostics settings for the Community-Scripts API.
|
||
#https://github.com/community-scripts/ProxmoxVE/discussions/1836
|
||
#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes.
|
||
#You can review the data at https://community-scripts.github.io/ProxmoxVE/data
|
||
#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||
#This will disable the diagnostics feature.
|
||
#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue.
|
||
#This will enable the diagnostics feature.
|
||
#The following information will be sent:
|
||
#"disk_size"
|
||
#"core_count"
|
||
#"ram_size"
|
||
#"os_type"
|
||
#"os_version"
|
||
#"nsapp"
|
||
#"method"
|
||
#"pve_version"
|
||
#"status"
|
||
#If you have any concerns, please review the source code at /misc/build.func
|
||
EOF
|
||
DIAGNOSTICS="no"
|
||
fi
|
||
else
|
||
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics)
|
||
|
||
fi
|
||
}
|
||
|
||
diagnostics_menu() {
|
||
if [ "${DIAGNOSTICS:-no}" = "yes" ]; then
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "DIAGNOSTIC SETTINGS" \
|
||
--yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
|
||
--yes-button "No" --no-button "Back"; then
|
||
DIAGNOSTICS="no"
|
||
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
|
||
whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
|
||
fi
|
||
else
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "DIAGNOSTIC SETTINGS" \
|
||
--yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \
|
||
--yes-button "Yes" --no-button "Back"; then
|
||
DIAGNOSTICS="yes"
|
||
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
|
||
whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# echo_default()
|
||
#
|
||
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
|
||
# - Uses icons and formatting for readability
|
||
# - Convert CT_TYPE to description
|
||
# ------------------------------------------------------------------------------
|
||
echo_default() {
|
||
CT_TYPE_DESC="Unprivileged"
|
||
if [ "$CT_TYPE" -eq 0 ]; then
|
||
CT_TYPE_DESC="Privileged"
|
||
fi
|
||
echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
|
||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
|
||
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
|
||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||
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 " "
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# install_script()
|
||
#
|
||
# - Main entrypoint for installation mode
|
||
# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
|
||
# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
|
||
# - Applies chosen settings and triggers container build
|
||
# ------------------------------------------------------------------------------
|
||
install_script() {
|
||
pve_check
|
||
shell_check
|
||
root_check
|
||
arch_check
|
||
ssh_check
|
||
maxkeys_check
|
||
diagnostics_check
|
||
|
||
if systemctl is-active -q ping-instances.service; then
|
||
systemctl -q stop ping-instances.service
|
||
fi
|
||
|
||
NEXTID=$(pvesh get /cluster/nextid)
|
||
|
||
# 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
|
||
timezone=$(cat /etc/timezone)
|
||
else
|
||
timezone="UTC"
|
||
fi
|
||
|
||
# Show APP Header
|
||
header_info
|
||
|
||
# --- 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
|
||
|
||
APPDEFAULTS_OPTION="$appdefaults_option"
|
||
SETTINGS_OPTION="$settings_option"
|
||
|
||
# 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)
|
||
header_info
|
||
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
|
||
VERBOSE="no"
|
||
METHOD="default"
|
||
base_settings "$VERBOSE"
|
||
echo_default
|
||
defaults_target="$(ensure_global_default_vars_file)"
|
||
break
|
||
;;
|
||
2 | advanced | ADVANCED)
|
||
header_info
|
||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
|
||
echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
|
||
METHOD="advanced"
|
||
base_settings
|
||
advanced_settings
|
||
defaults_target="$(ensure_global_default_vars_file)"
|
||
run_maybe_offer="yes"
|
||
break
|
||
;;
|
||
3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS)
|
||
default_var_settings || {
|
||
msg_error "Failed to apply default.vars"
|
||
exit 1
|
||
}
|
||
defaults_target="/usr/local/community-scripts/default.vars"
|
||
break
|
||
;;
|
||
"$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"
|
||
base_settings
|
||
load_vars_file "$(get_app_defaults_path)"
|
||
echo_default
|
||
defaults_target="$(get_app_defaults_path)"
|
||
break
|
||
else
|
||
msg_error "No App Defaults available for ${APP}"
|
||
exit 1
|
||
fi
|
||
;;
|
||
"$SETTINGS_OPTION" | settings | SETTINGS)
|
||
settings_menu
|
||
# After settings menu, show main menu again
|
||
header_info
|
||
CHOICE=""
|
||
;;
|
||
*)
|
||
echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if [[ -n "$defaults_target" ]]; then
|
||
ensure_storage_selection_for_vars_file "$defaults_target"
|
||
fi
|
||
|
||
if [[ "$run_maybe_offer" == "yes" ]]; then
|
||
maybe_offer_save_app_defaults
|
||
fi
|
||
}
|
||
|
||
edit_default_storage() {
|
||
local vf="/usr/local/community-scripts/default.vars"
|
||
|
||
# Ensure file exists
|
||
if [[ ! -f "$vf" ]]; then
|
||
mkdir -p "$(dirname "$vf")"
|
||
touch "$vf"
|
||
fi
|
||
|
||
# Let ensure_storage_selection_for_vars_file handle everything
|
||
ensure_storage_selection_for_vars_file "$vf"
|
||
}
|
||
|
||
settings_menu() {
|
||
while true; do
|
||
local settings_items=(
|
||
"1" "Manage API-Diagnostic Setting"
|
||
"2" "Edit Default.vars"
|
||
)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
settings_items+=("3" "Edit App.vars for ${APP}")
|
||
settings_items+=("4" "Back to Main Menu")
|
||
else
|
||
settings_items+=("3" "Back to Main Menu")
|
||
fi
|
||
|
||
local choice
|
||
choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "Community-Scripts SETTINGS Menu" \
|
||
--ok-button "Select" --cancel-button "Exit Script" \
|
||
--menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \
|
||
"${settings_items[@]}" \
|
||
3>&1 1>&2 2>&3) || exit_script
|
||
|
||
case "$choice" in
|
||
1) diagnostics_menu ;;
|
||
2) nano /usr/local/community-scripts/default.vars ;;
|
||
3)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
nano "$(get_app_defaults_path)"
|
||
else
|
||
# Back was selected (no app.vars available)
|
||
return
|
||
fi
|
||
;;
|
||
4)
|
||
# Back to main menu
|
||
return
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# check_container_resources()
|
||
#
|
||
# - Compares host RAM/CPU with required values
|
||
# - Warns if under-provisioned and asks user to continue or abort
|
||
# ------------------------------------------------------------------------------
|
||
check_container_resources() {
|
||
current_ram=$(free -m | awk 'NR==2{print $2}')
|
||
current_cpu=$(nproc)
|
||
|
||
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
|
||
echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
|
||
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
|
||
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? <yes/No> "
|
||
read -r prompt
|
||
if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
|
||
echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
|
||
exit 1
|
||
fi
|
||
else
|
||
echo -e ""
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# check_container_storage()
|
||
#
|
||
# - Checks /boot partition usage
|
||
# - Warns if usage >80% and asks user confirmation before proceeding
|
||
# ------------------------------------------------------------------------------
|
||
check_container_storage() {
|
||
total_size=$(df /boot --output=size | tail -n 1)
|
||
local used_size=$(df /boot --output=used | tail -n 1)
|
||
usage=$((100 * used_size / total_size))
|
||
if ((usage > 80)); then
|
||
echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
|
||
echo -ne "Continue anyway? <y/N> "
|
||
read -r prompt
|
||
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
|
||
echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
|
||
exit 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_extract_keys_from_file()
|
||
#
|
||
# - Extracts valid SSH public keys from given file
|
||
# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
|
||
# ------------------------------------------------------------------------------
|
||
ssh_extract_keys_from_file() {
|
||
local f="$1"
|
||
[[ -r "$f" ]] || return 0
|
||
tr -d '\r' <"$f" | awk '
|
||
/^[[:space:]]*#/ {next}
|
||
/^[[:space:]]*$/ {next}
|
||
# 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() {
|
||
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
|
||
ssh_key_mode=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
|
||
"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
|
||
else
|
||
ssh_key_mode=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \
|
||
"No host keys detected; choose manual/none:" 12 72 2 \
|
||
"manual" "Paste a single public key" \
|
||
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
|
||
fi
|
||
|
||
case "$ssh_key_mode" in
|
||
found)
|
||
local selection
|
||
selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \
|
||
--checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
|
||
for tag in $selection; do
|
||
tag="${tag%\"}"
|
||
tag="${tag#\"}"
|
||
local line
|
||
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
|
||
done
|
||
;;
|
||
manual)
|
||
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
|
||
[[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
|
||
;;
|
||
folder)
|
||
local glob_path
|
||
glob_path=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
|
||
if [[ -n "$glob_path" ]]; then
|
||
shopt -s nullglob
|
||
read -r -a _scan_files <<<"$glob_path"
|
||
shopt -u nullglob
|
||
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
|
||
ssh_build_choices_from_files "${_scan_files[@]}"
|
||
if [[ "$COUNT" -gt 0 ]]; then
|
||
local folder_selection
|
||
folder_selection=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \
|
||
--checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
|
||
for tag in $folder_selection; do
|
||
tag="${tag%\"}"
|
||
tag="${tag#\"}"
|
||
local line
|
||
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
|
||
done
|
||
else
|
||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "No keys found in: $glob_path" 8 60
|
||
fi
|
||
else
|
||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60
|
||
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
|
||
|
||
if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then
|
||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
|
||
SSH="yes"
|
||
else
|
||
SSH="no"
|
||
fi
|
||
else
|
||
SSH="no"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# start()
|
||
#
|
||
# - Entry point of script
|
||
# - On Proxmox host: calls install_script
|
||
# - In silent mode: runs update_script
|
||
# - Otherwise: shows update/setting menu
|
||
# ------------------------------------------------------------------------------
|
||
start() {
|
||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
||
if command -v pveversion >/dev/null 2>&1; then
|
||
install_script || return 0
|
||
return 0
|
||
elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
|
||
VERBOSE="no"
|
||
set_std_mode
|
||
update_script
|
||
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)
|
||
|
||
case "$CHOICE" in
|
||
1)
|
||
VERBOSE="no"
|
||
set_std_mode
|
||
;;
|
||
2)
|
||
VERBOSE="yes"
|
||
set_std_mode
|
||
;;
|
||
3)
|
||
clear
|
||
exit_script
|
||
exit
|
||
;;
|
||
esac
|
||
update_script
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 8: CONTAINER CREATION & DEPLOYMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# build_container()
|
||
#
|
||
# - Main function for creating and configuring LXC container
|
||
# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6)
|
||
# - Creates container via pct create with all specified settings
|
||
# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough
|
||
# - Starts container and waits for network connectivity
|
||
# - Installs base packages (curl, sudo, etc.)
|
||
# - Injects SSH keys if configured
|
||
# - Executes <app>-install.sh inside container
|
||
# - Posts installation telemetry to API if diagnostics enabled
|
||
# ------------------------------------------------------------------------------
|
||
build_container() {
|
||
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
||
|
||
NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
|
||
|
||
# MAC
|
||
if [[ -n "$MAC" ]]; then
|
||
case "$MAC" in
|
||
,hwaddr=*) NET_STRING+="$MAC" ;;
|
||
*) NET_STRING+=",hwaddr=$MAC" ;;
|
||
esac
|
||
fi
|
||
|
||
# IP (always required, default dhcp)
|
||
NET_STRING+=",ip=${NET:-dhcp}"
|
||
|
||
# Gateway
|
||
if [[ -n "$GATE" ]]; then
|
||
case "$GATE" in
|
||
,gw=*) NET_STRING+="$GATE" ;;
|
||
*) NET_STRING+=",gw=$GATE" ;;
|
||
esac
|
||
fi
|
||
|
||
# VLAN
|
||
if [[ -n "$VLAN" ]]; then
|
||
case "$VLAN" in
|
||
,tag=*) NET_STRING+="$VLAN" ;;
|
||
*) NET_STRING+=",tag=$VLAN" ;;
|
||
esac
|
||
fi
|
||
|
||
# MTU
|
||
if [[ -n "$MTU" ]]; then
|
||
case "$MTU" in
|
||
,mtu=*) NET_STRING+="$MTU" ;;
|
||
*) NET_STRING+=",mtu=$MTU" ;;
|
||
esac
|
||
fi
|
||
|
||
# IPv6 Handling
|
||
case "$IPV6_METHOD" in
|
||
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
|
||
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
|
||
static)
|
||
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
|
||
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
|
||
;;
|
||
none) ;;
|
||
esac
|
||
|
||
# Build FEATURES string
|
||
if [ "$CT_TYPE" == "1" ]; then
|
||
FEATURES="keyctl=1,nesting=1"
|
||
else
|
||
FEATURES="nesting=1"
|
||
fi
|
||
|
||
if [ "$ENABLE_FUSE" == "yes" ]; then
|
||
FEATURES="$FEATURES,fuse=1"
|
||
fi
|
||
|
||
# Build PCT_OPTIONS as string for export
|
||
TEMP_DIR=$(mktemp -d)
|
||
pushd "$TEMP_DIR" >/dev/null
|
||
if [ "$var_os" == "alpine" ]; then
|
||
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func)"
|
||
else
|
||
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)"
|
||
fi
|
||
|
||
# Core exports for install.func
|
||
export DIAGNOSTICS="$DIAGNOSTICS"
|
||
export RANDOM_UUID="$RANDOM_UUID"
|
||
export SESSION_ID="$SESSION_ID"
|
||
export CACHER="$APT_CACHER"
|
||
export CACHER_IP="$APT_CACHER_IP"
|
||
export tz="$timezone"
|
||
export APPLICATION="$APP"
|
||
export app="$NSAPP"
|
||
export PASSWORD="$PW"
|
||
export VERBOSE="$VERBOSE"
|
||
export SSH_ROOT="${SSH}"
|
||
export SSH_AUTHORIZED_KEY
|
||
export CTID="$CT_ID"
|
||
export CTTYPE="$CT_TYPE"
|
||
export ENABLE_FUSE="$ENABLE_FUSE"
|
||
export ENABLE_TUN="$ENABLE_TUN"
|
||
export PCT_OSTYPE="$var_os"
|
||
export PCT_OSVERSION="$var_version"
|
||
export PCT_DISK_SIZE="$DISK_SIZE"
|
||
|
||
# DEV_MODE exports (optional, for debugging)
|
||
export BUILD_LOG="$BUILD_LOG"
|
||
export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
|
||
export dev_mode="${dev_mode:-}"
|
||
export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
|
||
export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
|
||
export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}"
|
||
export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}"
|
||
export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}"
|
||
export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}"
|
||
export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}"
|
||
|
||
# Build PCT_OPTIONS as multi-line string
|
||
PCT_OPTIONS_STRING=" -features $FEATURES
|
||
-hostname $HN
|
||
-tags $TAGS"
|
||
|
||
# Add storage if specified
|
||
if [ -n "$SD" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$SD"
|
||
fi
|
||
|
||
# Add nameserver if specified
|
||
if [ -n "$NS" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$NS"
|
||
fi
|
||
|
||
# Network configuration
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$NET_STRING
|
||
-onboot 1
|
||
-cores $CORE_COUNT
|
||
-memory $RAM_SIZE
|
||
-unprivileged $CT_TYPE"
|
||
|
||
# Protection flag (if var_protection was set)
|
||
if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
-protection 1"
|
||
fi
|
||
|
||
# Timezone flag (if var_timezone was set)
|
||
if [ -n "${CT_TIMEZONE:-}" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
-timezone $CT_TIMEZONE"
|
||
fi
|
||
|
||
# Password (already formatted)
|
||
if [ -n "$PW" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$PW"
|
||
fi
|
||
|
||
# Export as string (this works, unlike arrays!)
|
||
export PCT_OPTIONS="$PCT_OPTIONS_STRING"
|
||
export TEMPLATE_STORAGE="${var_template_storage:-}"
|
||
export CONTAINER_STORAGE="${var_container_storage:-}"
|
||
|
||
create_lxc_container || exit $?
|
||
|
||
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
||
|
||
# ============================================================================
|
||
# GPU/USB PASSTHROUGH CONFIGURATION
|
||
# ============================================================================
|
||
|
||
# List of applications that benefit from GPU acceleration
|
||
GPU_APPS=(
|
||
"immich" "channels" "emby" "ersatztv" "frigate"
|
||
"jellyfin" "plex" "scrypted" "tdarr" "unmanic"
|
||
"ollama" "fileflows" "open-webui" "tunarr" "debian"
|
||
"handbrake" "sunshine" "moonlight" "kodi" "stremio"
|
||
"viseron"
|
||
)
|
||
|
||
# Check if app needs GPU
|
||
is_gpu_app() {
|
||
local app="${1,,}"
|
||
for gpu_app in "${GPU_APPS[@]}"; do
|
||
[[ "$app" == "${gpu_app,,}" ]] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# Detect all available GPU devices
|
||
detect_gpu_devices() {
|
||
INTEL_DEVICES=()
|
||
AMD_DEVICES=()
|
||
NVIDIA_DEVICES=()
|
||
|
||
# Store PCI info to avoid multiple calls
|
||
local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
|
||
|
||
# Check for Intel GPU - look for Intel vendor ID [8086]
|
||
if echo "$pci_vga_info" | grep -q "\[8086:"; then
|
||
msg_custom "🎮" "${BL}" "Detected Intel GPU"
|
||
if [[ -d /dev/dri ]]; then
|
||
for d in /dev/dri/renderD* /dev/dri/card*; do
|
||
[[ -e "$d" ]] && INTEL_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
fi
|
||
|
||
# Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
|
||
if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
|
||
msg_custom "🎮" "${RD}" "Detected AMD GPU"
|
||
if [[ -d /dev/dri ]]; then
|
||
# Only add if not already claimed by Intel
|
||
if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
|
||
for d in /dev/dri/renderD* /dev/dri/card*; do
|
||
[[ -e "$d" ]] && AMD_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
|
||
if echo "$pci_vga_info" | grep -q "\[10de:"; then
|
||
msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
|
||
|
||
# Simple passthrough - just bind /dev/nvidia* devices if they exist
|
||
for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do
|
||
[[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||
done
|
||
|
||
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)"
|
||
cat <<EOF >>"$LXC_CONFIG"
|
||
# Automatic USB passthrough (privileged container)
|
||
lxc.cgroup2.devices.allow: a
|
||
lxc.cap.drop:
|
||
lxc.cgroup2.devices.allow: c 188:* rwm
|
||
lxc.cgroup2.devices.allow: c 189:* rwm
|
||
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
|
||
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
|
||
EOF
|
||
msg_ok "USB passthrough configured"
|
||
}
|
||
|
||
# Configure GPU passthrough
|
||
configure_gpu_passthrough() {
|
||
# Skip if not a GPU app and not privileged
|
||
if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
|
||
return 0
|
||
fi
|
||
|
||
detect_gpu_devices
|
||
|
||
# Count available GPU types
|
||
local gpu_count=0
|
||
local available_gpus=()
|
||
|
||
if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("INTEL")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("AMD")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("NVIDIA")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ $gpu_count -eq 0 ]]; then
|
||
msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough"
|
||
return 0
|
||
fi
|
||
|
||
local selected_gpu=""
|
||
|
||
if [[ $gpu_count -eq 1 ]]; then
|
||
# Automatic selection for single GPU
|
||
selected_gpu="${available_gpus[0]}"
|
||
msg_ok "Automatically configuring ${selected_gpu} GPU passthrough"
|
||
else
|
||
# Multiple GPUs - ask user
|
||
echo -e "\n${INFO} Multiple GPU types detected:"
|
||
for gpu in "${available_gpus[@]}"; do
|
||
echo " - $gpu"
|
||
done
|
||
read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
|
||
selected_gpu="${selected_gpu^^}"
|
||
|
||
# Validate selection
|
||
local valid=0
|
||
for gpu in "${available_gpus[@]}"; do
|
||
[[ "$selected_gpu" == "$gpu" ]] && valid=1
|
||
done
|
||
|
||
if [[ $valid -eq 0 ]]; then
|
||
msg_warn "Invalid selection. Skipping GPU passthrough."
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# 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"
|
||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||
EOF
|
||
fi
|
||
|
||
# Coral TPU passthrough
|
||
if [[ -e /dev/apex_0 ]]; then
|
||
msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough"
|
||
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||
fi
|
||
}
|
||
|
||
# Execute pre-start configurations
|
||
configure_usb_passthrough
|
||
configure_gpu_passthrough
|
||
configure_additional_devices
|
||
|
||
# ============================================================================
|
||
# START CONTAINER AND INSTALL USERLAND
|
||
# ============================================================================
|
||
|
||
msg_info "Starting LXC Container"
|
||
pct start "$CTID"
|
||
|
||
# Wait for container to be running
|
||
for i in {1..10}; do
|
||
if pct status "$CTID" | grep -q "status: running"; then
|
||
msg_ok "Started LXC Container"
|
||
break
|
||
fi
|
||
sleep 1
|
||
if [ "$i" -eq 10 ]; then
|
||
msg_error "LXC Container did not reach running state"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
# Wait for network (skip for Alpine initially)
|
||
if [ "$var_os" != "alpine" ]; then
|
||
msg_info "Waiting for network in LXC container"
|
||
|
||
# Wait for IP assignment (IPv4 or IPv6)
|
||
local ip_in_lxc=""
|
||
for i in {1..20}; do
|
||
# Try IPv4 first
|
||
ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
|
||
# Fallback to IPv6 if IPv4 not available
|
||
if [ -z "$ip_in_lxc" ]; then
|
||
ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
|
||
fi
|
||
[ -n "$ip_in_lxc" ] && break
|
||
sleep 1
|
||
done
|
||
|
||
if [ -z "$ip_in_lxc" ]; then
|
||
msg_error "No IP assigned to CT $CTID after 20s"
|
||
echo -e "${YW}Troubleshooting:${CL}"
|
||
echo " • Verify bridge ${BRG} exists and has connectivity"
|
||
echo " • Check if DHCP server is reachable (if using DHCP)"
|
||
echo " • Verify static IP configuration (if using static IP)"
|
||
echo " • Check Proxmox firewall rules"
|
||
echo " • If using Tailscale: Disable MagicDNS temporarily"
|
||
exit 1
|
||
fi
|
||
|
||
# Verify basic connectivity (ping test)
|
||
local ping_success=false
|
||
for retry in {1..3}; do
|
||
if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null ||
|
||
pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null ||
|
||
pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then
|
||
ping_success=true
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
if [ "$ping_success" = false ]; then
|
||
msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed"
|
||
echo -e "${YW}Container may have limited internet access. Installation will continue...${CL}"
|
||
else
|
||
msg_ok "Network in LXC is reachable (ping)"
|
||
fi
|
||
fi
|
||
# 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
|
||
|
||
# Continue with standard container setup
|
||
msg_info "Customizing LXC Container"
|
||
|
||
# # Install GPU userland if configured
|
||
# if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
|
||
# install_gpu_userland "VAAPI"
|
||
# fi
|
||
|
||
# if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
|
||
# install_gpu_userland "NVIDIA"
|
||
# fi
|
||
|
||
# Continue with standard container setup
|
||
if [ "$var_os" == "alpine" ]; then
|
||
sleep 3
|
||
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
|
||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
|
||
EOF'
|
||
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
|
||
else
|
||
sleep 3
|
||
pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
|
||
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
|
||
echo LANG=\$locale_line >/etc/default/locale && \
|
||
locale-gen >/dev/null && \
|
||
export LANG=\$locale_line"
|
||
|
||
if [[ -z "${tz:-}" ]]; then
|
||
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
|
||
fi
|
||
|
||
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
|
||
# 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"
|
||
else
|
||
msg_warn "Skipping timezone setup – zone '$tz' not found in container"
|
||
fi
|
||
|
||
pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || {
|
||
msg_error "apt-get base packages installation failed"
|
||
exit 1
|
||
}
|
||
fi
|
||
|
||
msg_ok "Customized LXC Container"
|
||
|
||
# Install SSH keys
|
||
install_ssh_keys_into_ct
|
||
|
||
# Run application installer
|
||
# Disable error trap - container errors are handled internally via flag file
|
||
set +Eeuo pipefail # Disable ALL error handling temporarily
|
||
trap - ERR # Remove ERR trap completely
|
||
|
||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
|
||
local lxc_exit=$?
|
||
|
||
set -Eeuo pipefail # Re-enable error handling
|
||
trap 'error_handler' ERR # Restore ERR trap
|
||
|
||
# Check for error flag file in container (more reliable than lxc-attach exit code)
|
||
local install_exit_code=0
|
||
if [[ -n "${SESSION_ID:-}" ]]; then
|
||
local error_flag="/root/.install-${SESSION_ID}.failed"
|
||
if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then
|
||
install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1")
|
||
pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
# Fallback to lxc-attach exit code if no flag file
|
||
if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]]; then
|
||
install_exit_code=$lxc_exit
|
||
fi
|
||
|
||
# Installation failed?
|
||
if [[ $install_exit_code -ne 0 ]]; then
|
||
msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"
|
||
|
||
# Copy both logs from container before potential deletion
|
||
local build_log_copied=false
|
||
local install_log_copied=false
|
||
|
||
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
|
||
# Copy BUILD_LOG (creation log) if it exists
|
||
if [[ -f "${BUILD_LOG}" ]]; then
|
||
cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true
|
||
fi
|
||
|
||
# Copy INSTALL_LOG from container
|
||
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null; then
|
||
install_log_copied=true
|
||
fi
|
||
|
||
# Show available logs
|
||
echo ""
|
||
[[ "$build_log_copied" == true ]] && echo -e "${GN}✔${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}"
|
||
[[ "$install_log_copied" == true ]] && echo -e "${GN}✔${CL} Installation log: ${BL}/tmp/install-lxc-${CTID}-${SESSION_ID}.log${CL}"
|
||
fi
|
||
|
||
# Dev mode: Keep container or open breakpoint shell
|
||
if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then
|
||
msg_dev "Keep mode active - container ${CTID} preserved"
|
||
return 0
|
||
elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then
|
||
msg_dev "Breakpoint mode - opening shell in container ${CTID}"
|
||
echo -e "${YW}Type 'exit' to return to host${CL}"
|
||
pct enter "$CTID"
|
||
echo ""
|
||
echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}"
|
||
if read -r response && [[ "$response" =~ ^[Yy]$ ]]; then
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
msg_ok "Container ${CTID} removed"
|
||
else
|
||
msg_dev "Container ${CTID} kept for debugging"
|
||
fi
|
||
exit $install_exit_code
|
||
fi
|
||
|
||
# Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner)
|
||
echo ""
|
||
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||
|
||
if read -t 60 -r response; then
|
||
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
|
||
# Remove container
|
||
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
elif [[ "$response" =~ ^[Nn]$ ]]; then
|
||
echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
|
||
|
||
# Dev mode: Setup MOTD/SSH for debugging access to broken container
|
||
if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
|
||
echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}"
|
||
if pct exec "$CTID" -- bash -c "
|
||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)
|
||
declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true
|
||
" >/dev/null 2>&1; then
|
||
local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
|
||
echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
# Timeout - auto-remove
|
||
echo -e "\n${YW}No response - auto-removing container${CL}"
|
||
echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
fi
|
||
|
||
exit $install_exit_code
|
||
fi
|
||
}
|
||
|
||
destroy_lxc() {
|
||
if [[ -z "$CT_ID" ]]; then
|
||
msg_error "No CT_ID found. Nothing to remove."
|
||
return 1
|
||
fi
|
||
|
||
# Abort on Ctrl-C / Ctrl-D / ESC
|
||
trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
|
||
|
||
local prompt
|
||
if ! read -rp "Remove this Container? <y/N> " prompt; then
|
||
# read returns non-zero on Ctrl-D/ESC
|
||
msg_error "Aborted input (Ctrl-D/ESC)"
|
||
return 130
|
||
fi
|
||
|
||
case "${prompt,,}" in
|
||
y | yes)
|
||
if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
|
||
msg_ok "Removed Container $CT_ID"
|
||
else
|
||
msg_error "Failed to remove Container $CT_ID"
|
||
return 1
|
||
fi
|
||
;;
|
||
"" | n | no)
|
||
msg_custom "ℹ️" "${BL}" "Container was not removed."
|
||
;;
|
||
*)
|
||
msg_warn "Invalid response. Container was not removed."
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Storage discovery / selection helpers
|
||
# ------------------------------------------------------------------------------
|
||
resolve_storage_preselect() {
|
||
local class="$1" preselect="$2" required_content=""
|
||
case "$class" in
|
||
template) required_content="vztmpl" ;;
|
||
container) required_content="rootdir" ;;
|
||
*) return 1 ;;
|
||
esac
|
||
[[ -z "$preselect" ]] && return 1
|
||
if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then
|
||
msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)"
|
||
return 1
|
||
fi
|
||
|
||
local line total used free
|
||
line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
|
||
if [[ -z "$line" ]]; then
|
||
STORAGE_INFO="n/a"
|
||
else
|
||
total="$(awk '{print $4}' <<<"$line")"
|
||
used="$(awk '{print $5}' <<<"$line")"
|
||
free="$(awk '{print $6}' <<<"$line")"
|
||
local total_h used_h free_h
|
||
if command -v numfmt >/dev/null 2>&1; then
|
||
total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
|
||
used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
|
||
free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
|
||
STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
|
||
else
|
||
STORAGE_INFO="Free: ${free} Used: ${used}"
|
||
fi
|
||
fi
|
||
STORAGE_RESULT="$preselect"
|
||
return 0
|
||
}
|
||
|
||
fix_gpu_gids() {
|
||
if [[ -z "${GPU_TYPE:-}" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Detecting and setting correct GPU group IDs"
|
||
|
||
# Get actual GIDs from container
|
||
local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
|
||
local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
|
||
|
||
# Create groups if they don't exist
|
||
if [[ -z "$video_gid" ]]; then
|
||
pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1
|
||
video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
|
||
[[ -z "$video_gid" ]] && video_gid="44"
|
||
fi
|
||
|
||
if [[ -z "$render_gid" ]]; then
|
||
pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1
|
||
render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
|
||
[[ -z "$render_gid" ]] && render_gid="104"
|
||
fi
|
||
|
||
# Stop container to update config
|
||
pct stop "$CTID" >/dev/null 2>&1
|
||
sleep 1
|
||
|
||
# Update dev entries with correct GIDs
|
||
sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG"
|
||
sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG"
|
||
|
||
# Restart container
|
||
pct start "$CTID" >/dev/null 2>&1
|
||
sleep 2
|
||
|
||
msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})"
|
||
|
||
# For privileged containers: also fix permissions inside container
|
||
if [[ "$CT_TYPE" == "0" ]]; then
|
||
pct exec "$CTID" -- bash -c "
|
||
if [ -d /dev/dri ]; then
|
||
for dev in /dev/dri/*; do
|
||
if [ -e \"\$dev\" ]; then
|
||
if [[ \"\$dev\" =~ renderD ]]; then
|
||
chgrp ${render_gid} \"\$dev\" 2>/dev/null || true
|
||
else
|
||
chgrp ${video_gid} \"\$dev\" 2>/dev/null || true
|
||
fi
|
||
chmod 660 \"\$dev\" 2>/dev/null || true
|
||
fi
|
||
done
|
||
fi
|
||
" >/dev/null 2>&1
|
||
fi
|
||
}
|
||
|
||
check_storage_support() {
|
||
local CONTENT="$1" VALID=0
|
||
while IFS= read -r line; do
|
||
local STORAGE_NAME
|
||
STORAGE_NAME=$(awk '{print $1}' <<<"$line")
|
||
[[ -n "$STORAGE_NAME" ]] && VALID=1
|
||
done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
|
||
[[ $VALID -eq 1 ]]
|
||
}
|
||
|
||
select_storage() {
|
||
local CLASS=$1 CONTENT CONTENT_LABEL
|
||
case $CLASS in
|
||
container)
|
||
CONTENT='rootdir'
|
||
CONTENT_LABEL='Container'
|
||
;;
|
||
template)
|
||
CONTENT='vztmpl'
|
||
CONTENT_LABEL='Container template'
|
||
;;
|
||
iso)
|
||
CONTENT='iso'
|
||
CONTENT_LABEL='ISO image'
|
||
;;
|
||
images)
|
||
CONTENT='images'
|
||
CONTENT_LABEL='VM Disk image'
|
||
;;
|
||
backup)
|
||
CONTENT='backup'
|
||
CONTENT_LABEL='Backup'
|
||
;;
|
||
snippets)
|
||
CONTENT='snippets'
|
||
CONTENT_LABEL='Snippets'
|
||
;;
|
||
*)
|
||
msg_error "Invalid storage class '$CLASS'"
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
declare -A STORAGE_MAP
|
||
local -a MENU=()
|
||
local COL_WIDTH=0
|
||
|
||
while read -r TAG TYPE _ TOTAL USED FREE _; do
|
||
[[ -n "$TAG" && -n "$TYPE" ]] || continue
|
||
local DISPLAY="${TAG} (${TYPE})"
|
||
local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
|
||
local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
|
||
local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
|
||
STORAGE_MAP["$DISPLAY"]="$TAG"
|
||
MENU+=("$DISPLAY" "$INFO" "OFF")
|
||
((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
|
||
done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
|
||
|
||
if [[ ${#MENU[@]} -eq 0 ]]; then
|
||
msg_error "No storage found for content type '$CONTENT'."
|
||
return 2
|
||
fi
|
||
|
||
if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
|
||
STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
|
||
STORAGE_INFO="${MENU[1]}"
|
||
return 0
|
||
fi
|
||
|
||
local WIDTH=$((COL_WIDTH + 42))
|
||
while true; do
|
||
local DISPLAY_SELECTED
|
||
DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "Storage Pools" \
|
||
--radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
|
||
16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
|
||
|
||
DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
|
||
if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
|
||
whiptail --msgbox "No valid storage selected. Please try again." 8 58
|
||
continue
|
||
fi
|
||
STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
|
||
for ((i = 0; i < ${#MENU[@]}; i += 3)); do
|
||
if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
|
||
STORAGE_INFO="${MENU[$i + 1]}"
|
||
break
|
||
fi
|
||
done
|
||
return 0
|
||
done
|
||
}
|
||
|
||
create_lxc_container() {
|
||
# ------------------------------------------------------------------------------
|
||
# Optional verbose mode (debug tracing)
|
||
# ------------------------------------------------------------------------------
|
||
if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Helpers (dynamic versioning / template parsing)
|
||
# ------------------------------------------------------------------------------
|
||
pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
|
||
pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
|
||
|
||
ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
|
||
ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
|
||
ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
|
||
|
||
# Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
|
||
parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
|
||
|
||
# Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
|
||
# Returns:
|
||
# 0 = no upgrade needed
|
||
# 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
|
||
# 2 = user declined
|
||
# 3 = upgrade attempted but failed OR retry failed
|
||
offer_lxc_stack_upgrade_and_maybe_retry() {
|
||
local do_retry="${1:-no}" # yes|no
|
||
local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
|
||
|
||
_pvec_i="$(pkg_ver pve-container)"
|
||
_lxcp_i="$(pkg_ver lxc-pve)"
|
||
_pvec_c="$(pkg_cand pve-container)"
|
||
_lxcp_c="$(pkg_cand lxc-pve)"
|
||
|
||
if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
|
||
ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
|
||
fi
|
||
if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
|
||
ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
|
||
fi
|
||
if [[ $need -eq 0 ]]; then
|
||
msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
|
||
return 0
|
||
fi
|
||
|
||
echo
|
||
echo "An update for the Proxmox LXC stack is available:"
|
||
echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
|
||
echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
|
||
echo
|
||
read -rp "Do you want to upgrade now? [y/N] " _ans
|
||
case "${_ans,,}" in
|
||
y | yes)
|
||
msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
|
||
if $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then
|
||
msg_ok "LXC stack upgraded."
|
||
if [[ "$do_retry" == "yes" ]]; then
|
||
msg_info "Retrying container creation after upgrade"
|
||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||
msg_ok "Container created successfully after upgrade."
|
||
return 0
|
||
else
|
||
msg_error "pct create still failed after upgrade. See $LOGFILE"
|
||
return 3
|
||
fi
|
||
fi
|
||
return 1
|
||
else
|
||
msg_error "Upgrade failed. Please check APT output."
|
||
return 3
|
||
fi
|
||
;;
|
||
*) return 2 ;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Required input variables
|
||
# ------------------------------------------------------------------------------
|
||
[[ "${CTID:-}" ]] || {
|
||
msg_error "You need to set 'CTID' variable."
|
||
exit 203
|
||
}
|
||
[[ "${PCT_OSTYPE:-}" ]] || {
|
||
msg_error "You need to set 'PCT_OSTYPE' variable."
|
||
exit 204
|
||
}
|
||
|
||
msg_debug "CTID=$CTID"
|
||
msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
|
||
msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
|
||
|
||
# ID checks
|
||
[[ "$CTID" -ge 100 ]] || {
|
||
msg_error "ID cannot be less than 100."
|
||
exit 205
|
||
}
|
||
if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
|
||
echo -e "ID '$CTID' is already in use."
|
||
unset CTID
|
||
msg_error "Cannot use ID that is already in use."
|
||
exit 206
|
||
fi
|
||
|
||
# Storage capability check
|
||
check_storage_support "rootdir" || {
|
||
msg_error "No valid storage found for 'rootdir' [Container]"
|
||
exit 1
|
||
}
|
||
check_storage_support "vztmpl" || {
|
||
msg_error "No valid storage found for 'vztmpl' [Template]"
|
||
exit 1
|
||
}
|
||
|
||
# Template storage selection
|
||
if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
|
||
TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
|
||
else
|
||
while true; do
|
||
if [[ -z "${var_template_storage:-}" ]]; then
|
||
if select_storage template; then
|
||
TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
|
||
break
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Container storage selection
|
||
if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
|
||
CONTAINER_STORAGE="$STORAGE_RESULT"
|
||
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
|
||
else
|
||
if [[ -z "${var_container_storage:-}" ]]; then
|
||
if select_storage container; then
|
||
CONTAINER_STORAGE="$STORAGE_RESULT"
|
||
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Validate content types
|
||
msg_info "Validating content types of storage '$CONTAINER_STORAGE'"
|
||
STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm|linstor): $CONTAINER_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
|
||
msg_debug "Storage '$CONTAINER_STORAGE' has content types: $STORAGE_CONTENT"
|
||
|
||
# Check storage type for special handling
|
||
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1)
|
||
if [[ "$STORAGE_TYPE" == "linstor" ]]; then
|
||
msg_info "Detected LINSTOR storage - verifying cluster connectivity"
|
||
if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then
|
||
msg_error "LINSTOR storage '$CONTAINER_STORAGE' not accessible. Check LINSTOR cluster health."
|
||
exit 217
|
||
fi
|
||
fi
|
||
|
||
grep -qw "rootdir" <<<"$STORAGE_CONTENT" || {
|
||
msg_error "Storage '$CONTAINER_STORAGE' does not support 'rootdir'. Cannot create LXC."
|
||
exit 217
|
||
}
|
||
$STD msg_ok "Storage '$CONTAINER_STORAGE' supports 'rootdir'"
|
||
|
||
msg_info "Validating content types of template storage '$TEMPLATE_STORAGE'"
|
||
TEMPLATE_CONTENT=$(grep -A4 -E "^[^:]+: $TEMPLATE_STORAGE" /etc/pve/storage.cfg | grep content | awk '{$1=""; print $0}' | xargs)
|
||
msg_debug "Template storage '$TEMPLATE_STORAGE' has content types: $TEMPLATE_CONTENT"
|
||
|
||
# Check if template storage is LINSTOR (may need special handling)
|
||
TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1)
|
||
if [[ "$TEMPLATE_TYPE" == "linstor" ]]; then
|
||
msg_info "Template storage uses LINSTOR - ensuring resource availability"
|
||
fi
|
||
|
||
if ! grep -qw "vztmpl" <<<"$TEMPLATE_CONTENT"; then
|
||
msg_warn "Template storage '$TEMPLATE_STORAGE' does not declare 'vztmpl'. This may cause pct create to fail."
|
||
else
|
||
$STD msg_ok "Template storage '$TEMPLATE_STORAGE' supports 'vztmpl'"
|
||
fi
|
||
|
||
# Free space check
|
||
STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
|
||
REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
|
||
[[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || {
|
||
msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
|
||
exit 214
|
||
}
|
||
|
||
# Cluster quorum (if cluster)
|
||
if [[ -f /etc/pve/corosync.conf ]]; then
|
||
msg_info "Checking cluster quorum"
|
||
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
|
||
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
|
||
exit 201
|
||
fi
|
||
msg_ok "Cluster is quorate"
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Template discovery & validation
|
||
# ------------------------------------------------------------------------------
|
||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
|
||
case "$PCT_OSTYPE" in
|
||
debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
|
||
alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
|
||
*) TEMPLATE_PATTERN="" ;;
|
||
esac
|
||
|
||
msg_info "Searching for template '$TEMPLATE_SEARCH'"
|
||
|
||
# Build regex patterns outside awk/grep for clarity
|
||
SEARCH_PATTERN="^${TEMPLATE_SEARCH}"
|
||
|
||
mapfile -t LOCAL_TEMPLATES < <(
|
||
pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
|
||
awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
|
||
sed 's|.*/||' | sort -t - -k 2 -V
|
||
)
|
||
|
||
pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)."
|
||
|
||
msg_ok "Template search completed"
|
||
|
||
set +u
|
||
mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
|
||
set -u
|
||
|
||
ONLINE_TEMPLATE=""
|
||
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||
|
||
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
|
||
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
|
||
TEMPLATE_SOURCE="local"
|
||
else
|
||
TEMPLATE="$ONLINE_TEMPLATE"
|
||
TEMPLATE_SOURCE="online"
|
||
fi
|
||
|
||
# If still no template, try to find alternatives
|
||
if [[ -z "$TEMPLATE" ]]; then
|
||
echo ""
|
||
echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
|
||
|
||
# Get all available versions for this OS type
|
||
mapfile -t AVAILABLE_VERSIONS < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
|
||
awk -F'\t' '{print $1}' |
|
||
grep "^${PCT_OSTYPE}-" |
|
||
sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
|
||
sort -u -V 2>/dev/null
|
||
)
|
||
|
||
if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
|
||
echo ""
|
||
echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
|
||
for i in "${!AVAILABLE_VERSIONS[@]}"; do
|
||
echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
|
||
done
|
||
echo ""
|
||
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
|
||
PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
|
||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
|
||
SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
|
||
|
||
mapfile -t ONLINE_TEMPLATES < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
|
||
awk -F'\t' '{print $1}' |
|
||
grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
|
||
sort -t - -k 2 -V 2>/dev/null || true
|
||
)
|
||
|
||
if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
|
||
TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||
TEMPLATE_SOURCE="online"
|
||
else
|
||
msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
|
||
exit 225
|
||
fi
|
||
else
|
||
msg_custom "🚫" "${YW}" "Installation cancelled"
|
||
exit 0
|
||
fi
|
||
else
|
||
msg_error "No ${PCT_OSTYPE} templates available at all"
|
||
exit 225
|
||
fi
|
||
fi
|
||
|
||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
# If we still don't have a path but have a valid template name, construct it
|
||
if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
|
||
TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
[[ -n "$TEMPLATE_PATH" ]] || {
|
||
if [[ -z "$TEMPLATE" ]]; then
|
||
msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
|
||
|
||
# Get available versions
|
||
mapfile -t AVAILABLE_VERSIONS < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep "^${PCT_OSTYPE}-" |
|
||
sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
|
||
grep -E '^[0-9]+\.[0-9]+$' |
|
||
sort -u -V 2>/dev/null || sort -u
|
||
)
|
||
|
||
if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
|
||
echo -e "\n${BL}Available versions:${CL}"
|
||
for i in "${!AVAILABLE_VERSIONS[@]}"; do
|
||
echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
|
||
done
|
||
|
||
echo ""
|
||
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
|
||
export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
|
||
export PCT_OSVERSION="$var_version"
|
||
msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
|
||
|
||
# Retry template search with new version
|
||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
|
||
SEARCH_PATTERN="^${TEMPLATE_SEARCH}-"
|
||
|
||
mapfile -t LOCAL_TEMPLATES < <(
|
||
pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
|
||
awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
|
||
sed 's|.*/||' | sort -t - -k 2 -V
|
||
)
|
||
mapfile -t ONLINE_TEMPLATES < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
|
||
awk -F'\t' '{print $1}' |
|
||
grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" |
|
||
sort -t - -k 2 -V 2>/dev/null || true
|
||
)
|
||
ONLINE_TEMPLATE=""
|
||
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||
|
||
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
|
||
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
|
||
TEMPLATE_SOURCE="local"
|
||
else
|
||
TEMPLATE="$ONLINE_TEMPLATE"
|
||
TEMPLATE_SOURCE="online"
|
||
fi
|
||
|
||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
# If we still don't have a path but have a valid template name, construct it
|
||
if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
|
||
TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
[[ -n "$TEMPLATE_PATH" ]] || {
|
||
msg_error "Template still not found after version change"
|
||
exit 220
|
||
}
|
||
else
|
||
msg_custom "🚫" "${YW}" "Installation cancelled"
|
||
exit 1
|
||
fi
|
||
else
|
||
msg_error "No ${PCT_OSTYPE} templates available"
|
||
exit 220
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Validate that we found a template
|
||
if [[ -z "$TEMPLATE" ]]; then
|
||
msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
|
||
msg_custom "ℹ️" "${YW}" "Please check:"
|
||
msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)"
|
||
msg_custom " •" "${YW}" "Does the template exist for your OS version?"
|
||
exit 225
|
||
fi
|
||
|
||
msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
|
||
msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
|
||
|
||
NEED_DOWNLOAD=0
|
||
if [[ ! -f "$TEMPLATE_PATH" ]]; then
|
||
msg_info "Template not present locally – will download."
|
||
NEED_DOWNLOAD=1
|
||
elif [[ ! -r "$TEMPLATE_PATH" ]]; then
|
||
msg_error "Template file exists but is not readable – check permissions."
|
||
exit 221
|
||
elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
|
||
if [[ -n "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Template file too small (<1MB) – re-downloading."
|
||
NEED_DOWNLOAD=1
|
||
else
|
||
msg_warn "Template looks too small, but no online version exists. Keeping local file."
|
||
fi
|
||
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
|
||
if [[ -n "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Template appears corrupted – re-downloading."
|
||
NEED_DOWNLOAD=1
|
||
else
|
||
msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
|
||
fi
|
||
else
|
||
$STD msg_ok "Template $TEMPLATE is present and valid."
|
||
fi
|
||
|
||
if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
|
||
if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
|
||
TEMPLATE="$ONLINE_TEMPLATE"
|
||
NEED_DOWNLOAD=1
|
||
else
|
||
msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE"
|
||
fi
|
||
fi
|
||
|
||
if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
|
||
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
|
||
for attempt in {1..3}; do
|
||
msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
|
||
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
|
||
msg_ok "Template download successful."
|
||
break
|
||
fi
|
||
if [[ $attempt -eq 3 ]]; then
|
||
msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
|
||
exit 222
|
||
fi
|
||
sleep $((attempt * 5))
|
||
done
|
||
fi
|
||
|
||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
|
||
msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
|
||
exit 223
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
|
||
# ------------------------------------------------------------------------------
|
||
if [[ "$PCT_OSTYPE" == "debian" ]]; then
|
||
OSVER="$(parse_template_osver "$TEMPLATE")"
|
||
if [[ -n "$OSVER" ]]; then
|
||
# Proactive, but without abort – only offer
|
||
offer_lxc_stack_upgrade_and_maybe_retry "no" || true
|
||
fi
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Create LXC Container
|
||
# ------------------------------------------------------------------------------
|
||
msg_info "Creating LXC container"
|
||
|
||
# Ensure subuid/subgid entries exist
|
||
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
|
||
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
|
||
|
||
# PCT_OPTIONS is now a string (exported from build_container)
|
||
# Add rootfs if not already specified
|
||
if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then
|
||
PCT_OPTIONS="$PCT_OPTIONS
|
||
-rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}"
|
||
fi
|
||
|
||
# Lock by template file (avoid concurrent downloads/creates)
|
||
lockfile="/tmp/template.${TEMPLATE}.lock"
|
||
exec 9>"$lockfile" || {
|
||
msg_error "Failed to create lock file '$lockfile'."
|
||
exit 200
|
||
}
|
||
flock -w 60 9 || {
|
||
msg_error "Timeout while waiting for template lock."
|
||
exit 202
|
||
}
|
||
|
||
LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
|
||
|
||
msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS"
|
||
msg_debug "Logfile: $LOGFILE"
|
||
|
||
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
|
||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
|
||
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..."
|
||
|
||
# Validate template file
|
||
if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
|
||
msg_warn "Template file too small or missing – re-downloading."
|
||
rm -f "$TEMPLATE_PATH"
|
||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
|
||
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
|
||
if [[ -n "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Template appears corrupted – re-downloading."
|
||
rm -f "$TEMPLATE_PATH"
|
||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
|
||
else
|
||
msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
|
||
fi
|
||
fi
|
||
|
||
# Retry after repair
|
||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||
# Fallback to local storage if not already on local
|
||
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
|
||
msg_info "Retrying container creation with fallback to local storage..."
|
||
LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||
if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
|
||
msg_info "Downloading template to local..."
|
||
pveam download local "$TEMPLATE" >/dev/null 2>&1
|
||
fi
|
||
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||
# Local fallback also failed - check for LXC stack version issue
|
||
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
|
||
echo
|
||
echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
|
||
echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
|
||
offer_lxc_stack_upgrade_and_maybe_retry "yes"
|
||
rc=$?
|
||
case $rc in
|
||
0) : ;; # success - container created, continue
|
||
2)
|
||
echo "Upgrade was declined. Please update and re-run:
|
||
apt update && apt install --only-upgrade pve-container lxc-pve"
|
||
exit 213
|
||
;;
|
||
3)
|
||
echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
|
||
exit 213
|
||
;;
|
||
esac
|
||
else
|
||
msg_error "Container creation failed. See $LOGFILE"
|
||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||
set -x
|
||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||
set +x
|
||
fi
|
||
exit 209
|
||
fi
|
||
else
|
||
msg_ok "Container successfully created using local fallback."
|
||
fi
|
||
else
|
||
# Already on local storage and still failed - check LXC stack version
|
||
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
|
||
echo
|
||
echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template."
|
||
echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically."
|
||
offer_lxc_stack_upgrade_and_maybe_retry "yes"
|
||
rc=$?
|
||
case $rc in
|
||
0) : ;; # success - container created, continue
|
||
2)
|
||
echo "Upgrade was declined. Please update and re-run:
|
||
apt update && apt install --only-upgrade pve-container lxc-pve"
|
||
exit 213
|
||
;;
|
||
3)
|
||
echo "Upgrade and/or retry failed. Please inspect: $LOGFILE"
|
||
exit 213
|
||
;;
|
||
esac
|
||
else
|
||
msg_error "Container creation failed. See $LOGFILE"
|
||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||
set -x
|
||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||
set +x
|
||
fi
|
||
exit 209
|
||
fi
|
||
fi
|
||
else
|
||
msg_ok "Container successfully created after template repair."
|
||
fi
|
||
fi
|
||
|
||
# Verify container exists
|
||
pct list | awk '{print $1}' | grep -qx "$CTID" || {
|
||
msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE"
|
||
exit 215
|
||
}
|
||
|
||
# Verify config rootfs
|
||
grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
|
||
msg_error "RootFS entry missing in container config. See $LOGFILE"
|
||
exit 216
|
||
}
|
||
|
||
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
|
||
|
||
# Report container creation to API
|
||
post_to_api
|
||
}
|
||
|
||
# ==============================================================================
|
||
# 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
|
||
# ------------------------------------------------------------------------------
|
||
description() {
|
||
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
|
||
|
||
# Generate LXC Description
|
||
DESCRIPTION=$(
|
||
cat <<EOF
|
||
<div align='center'>
|
||
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
|
||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||
</a>
|
||
|
||
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
|
||
|
||
<p style='margin: 16px 0;'>
|
||
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
|
||
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
|
||
</a>
|
||
</p>
|
||
|
||
<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
|
||
)
|
||
pct set "$CTID" -description "$DESCRIPTION"
|
||
|
||
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
|
||
systemctl start ping-instances.service
|
||
fi
|
||
|
||
post_update_to_api "done" "none"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 10: ERROR HANDLING & EXIT TRAPS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# api_exit_script()
|
||
#
|
||
# - Exit trap handler for reporting to API telemetry
|
||
# - Captures exit code and reports to API using centralized error descriptions
|
||
# - Uses explain_exit_code() from error_handler.func for consistent error messages
|
||
# - Posts failure status with exit code to API (error description added automatically)
|
||
# - Only executes on non-zero exit codes
|
||
# ------------------------------------------------------------------------------
|
||
api_exit_script() {
|
||
exit_code=$?
|
||
if [ $exit_code -ne 0 ]; then
|
||
post_update_to_api "failed" "$exit_code"
|
||
fi
|
||
}
|
||
|
||
if command -v pveversion >/dev/null 2>&1; then
|
||
trap 'api_exit_script' EXIT
|
||
fi
|
||
trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR
|
||
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
|
||
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
|