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