
Some checks failed
Bump build.func Revision / bump-revision (push) Has been cancelled
Eliminates the unnecessary 'uid=0' parameter from device configuration in GPU passthrough setup for privileged containers. Also updates the viseron install script to remove the target directory argument from the fetch_and_deploy_gh_release call, aligning with changes in its usage.
3342 lines
113 KiB
Bash
3342 lines
113 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"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_bridge_description()
|
||
#
|
||
# - Reads bridge descriptions from /usr/local/community-scripts/bridge_description
|
||
# - Returns description for given bridge name, or empty string if not found
|
||
# - File format: bridge_name:description (e.g., vmbr0:LAN, vmbr1:WAN)
|
||
# ------------------------------------------------------------------------------
|
||
get_bridge_description() {
|
||
local bridge_name="$1"
|
||
local desc_file="/usr/local/community-scripts/bridge_description"
|
||
|
||
if [[ -f "$desc_file" ]]; then
|
||
grep "^${bridge_name}:" "$desc_file" 2>/dev/null | cut -d':' -f2- | head -n1
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# 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
|
||
# Build bridge menu with descriptions
|
||
BRIDGE_MENU_OPTIONS=()
|
||
while IFS= read -r bridge; do
|
||
if [[ -n "$bridge" ]]; then
|
||
description=$(get_bridge_description "$bridge")
|
||
if [[ -n "$description" ]]; then
|
||
BRIDGE_MENU_OPTIONS+=("$bridge" "${description}")
|
||
else
|
||
BRIDGE_MENU_OPTIONS+=("$bridge" " ")
|
||
fi
|
||
fi
|
||
done <<< "$BRIDGES"
|
||
|
||
BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:\n To add a description, edit:\n /usr/local/community-scripts/bridge_description\n Example: vmbr0:LAN vmbr1:WAN" 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 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() {
|
||
local vf="$1"
|
||
|
||
# Read stored values (if any)
|
||
local tpl ct
|
||
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-)
|
||
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-)
|
||
|
||
# Wenn beide Werte schon existieren → übernehmen
|
||
if [[ -n "$tpl" && -n "$ct" ]]; then
|
||
TEMPLATE_STORAGE="$tpl"
|
||
CONTAINER_STORAGE="$ct"
|
||
echo_storage_summary_from_file "$vf"
|
||
return 0
|
||
fi
|
||
|
||
# --- Erstmalige Auswahl: beide Abfragen ---
|
||
select_storage template
|
||
local tpl_sel="$STORAGE_RESULT"
|
||
|
||
select_storage container
|
||
local ct_sel="$STORAGE_RESULT"
|
||
|
||
# --- Zusammenfassung + Nachfrage ---
|
||
if whiptail --backtitle "Community Scripts" --title "Default Storage" \
|
||
--yesno "Template-Storage --> $tpl_sel\nContainer-Storage --> $ct_sel\n\nSave as global defaults?" \
|
||
12 70; then
|
||
sed -i '/^[#[:space:]]*var_template_storage=/d' "$vf"
|
||
sed -i '/^[#[:space:]]*var_container_storage=/d' "$vf"
|
||
echo "var_template_storage=$tpl_sel" >>"$vf"
|
||
echo "var_container_storage=$ct_sel" >>"$vf"
|
||
else
|
||
sed -i '/^[#[:space:]]*var_template_storage=/d' "$vf"
|
||
sed -i '/^[#[:space:]]*var_container_storage=/d' "$vf"
|
||
echo "# var_template_storage=$tpl_sel" >>"$vf"
|
||
echo "# var_container_storage=$ct_sel" >>"$vf"
|
||
fi
|
||
|
||
TEMPLATE_STORAGE="$tpl_sel"
|
||
CONTAINER_STORAGE="$ct_sel"
|
||
msg_ok "Using Template-Storage → $tpl_sel"
|
||
msg_ok "Using Container-Storage → $ct_sel"
|
||
}
|
||
|
||
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
|
||
# still create
|
||
mkdir -p "$(dirname "$vf")"
|
||
touch "$vf"
|
||
|
||
if select_storage template; then
|
||
echo "var_template_storage=$STORAGE_RESULT" >>"$vf"
|
||
fi
|
||
|
||
if select_storage container; then
|
||
echo "var_container_storage=$STORAGE_RESULT" >>"$vf"
|
||
fi
|
||
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"
|
||
"viseron"
|
||
)
|
||
|
||
# Check if app needs GPU
|
||
is_gpu_app() {
|
||
local app="${1,,}"
|
||
for gpu_app in "${GPU_APPS[@]}"; do
|
||
[[ "$app" == "${gpu_app,,}" ]] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# Detect all available GPU devices
|
||
detect_gpu_devices() {
|
||
INTEL_DEVICES=()
|
||
AMD_DEVICES=()
|
||
NVIDIA_DEVICES=()
|
||
|
||
# Store PCI info to avoid multiple calls
|
||
local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D")
|
||
|
||
# Check for Intel GPU - look for Intel vendor ID [8086]
|
||
if echo "$pci_vga_info" | grep -q "\[8086:"; then
|
||
msg_info "Detected Intel GPU"
|
||
if [[ -d /dev/dri ]]; then
|
||
for d in /dev/dri/renderD* /dev/dri/card*; do
|
||
[[ -e "$d" ]] && INTEL_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
fi
|
||
|
||
# Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
|
||
if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then
|
||
msg_info "Detected AMD GPU"
|
||
if [[ -d /dev/dri ]]; then
|
||
# Only add if not already claimed by Intel
|
||
if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
|
||
for d in /dev/dri/renderD* /dev/dri/card*; do
|
||
[[ -e "$d" ]] && AMD_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
|
||
if echo "$pci_vga_info" | grep -q "\[10de:"; then
|
||
msg_info "Detected NVIDIA GPU"
|
||
for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do
|
||
[[ -e "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||
done
|
||
|
||
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
|
||
msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found"
|
||
msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver"
|
||
fi
|
||
fi
|
||
|
||
# Debug output
|
||
msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
|
||
msg_debug "AMD devices: ${AMD_DEVICES[*]}"
|
||
msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
|
||
}
|
||
|
||
|
||
# Configure USB passthrough for privileged containers
|
||
configure_usb_passthrough() {
|
||
if [[ "$CT_TYPE" != "0" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Configuring automatic USB passthrough (privileged container)"
|
||
cat <<EOF >>"$LXC_CONFIG"
|
||
# Automatic USB passthrough (privileged container)
|
||
lxc.cgroup2.devices.allow: a
|
||
lxc.cap.drop:
|
||
lxc.cgroup2.devices.allow: c 188:* rwm
|
||
lxc.cgroup2.devices.allow: c 189:* rwm
|
||
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
|
||
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
|
||
EOF
|
||
msg_ok "USB passthrough configured"
|
||
}
|
||
|
||
# Configure GPU passthrough
|
||
configure_gpu_passthrough() {
|
||
# Skip if not a GPU app and not privileged
|
||
if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
|
||
return 0
|
||
fi
|
||
|
||
detect_gpu_devices
|
||
|
||
# Count available GPU types
|
||
local gpu_count=0
|
||
local available_gpus=()
|
||
|
||
if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("INTEL")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("AMD")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("NVIDIA")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ $gpu_count -eq 0 ]]; then
|
||
msg_info "No GPU devices found for passthrough"
|
||
return 0
|
||
fi
|
||
|
||
local selected_gpu=""
|
||
|
||
if [[ $gpu_count -eq 1 ]]; then
|
||
# Automatic selection for single GPU
|
||
selected_gpu="${available_gpus[0]}"
|
||
msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
|
||
else
|
||
# Multiple GPUs - ask user
|
||
echo -e "\n${INFO} Multiple GPU types detected:"
|
||
for gpu in "${available_gpus[@]}"; do
|
||
echo " - $gpu"
|
||
done
|
||
read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
|
||
selected_gpu="${selected_gpu^^}"
|
||
|
||
# Validate selection
|
||
local valid=0
|
||
for gpu in "${available_gpus[@]}"; do
|
||
[[ "$selected_gpu" == "$gpu" ]] && valid=1
|
||
done
|
||
|
||
if [[ $valid -eq 0 ]]; then
|
||
msg_warn "Invalid selection. Skipping GPU passthrough."
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Apply passthrough configuration based on selection
|
||
local dev_idx=0
|
||
|
||
case "$selected_gpu" in
|
||
INTEL|AMD)
|
||
local devices=()
|
||
[[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
|
||
[[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
|
||
|
||
# For Proxmox WebUI visibility, add as dev0, dev1 etc.
|
||
for dev in "${devices[@]}"; do
|
||
if [[ "$CT_TYPE" == "0" ]]; then
|
||
# Privileged container - use dev entries for WebUI visibility
|
||
# Use initial GID 104 (render) for renderD*, 44 (video) for card*
|
||
if [[ "$dev" =~ renderD ]]; then
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
|
||
else
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
|
||
fi
|
||
dev_idx=$((dev_idx + 1))
|
||
|
||
# Also add cgroup allows for privileged containers
|
||
local major minor
|
||
major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
|
||
minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
|
||
|
||
if [[ "$major" != "0" && "$minor" != "0" ]]; then
|
||
echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
|
||
fi
|
||
else
|
||
# Unprivileged container
|
||
if [[ "$dev" =~ renderD ]]; then
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
|
||
else
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
|
||
fi
|
||
dev_idx=$((dev_idx + 1))
|
||
fi
|
||
done
|
||
|
||
export GPU_TYPE="$selected_gpu"
|
||
msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
|
||
;;
|
||
|
||
NVIDIA)
|
||
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
|
||
msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
|
||
return 1
|
||
fi
|
||
|
||
for dev in "${NVIDIA_DEVICES[@]}"; do
|
||
# NVIDIA devices typically need different handling
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
|
||
dev_idx=$((dev_idx + 1))
|
||
|
||
if [[ "$CT_TYPE" == "0" ]]; then
|
||
local major minor
|
||
major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
|
||
minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
|
||
|
||
if [[ "$major" != "0" && "$minor" != "0" ]]; then
|
||
echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
export GPU_TYPE="NVIDIA"
|
||
msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Additional device passthrough
|
||
configure_additional_devices() {
|
||
# TUN device passthrough
|
||
if [ "$ENABLE_TUN" == "yes" ]; then
|
||
cat <<EOF >>"$LXC_CONFIG"
|
||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||
EOF
|
||
fi
|
||
|
||
# Coral TPU passthrough
|
||
if [[ -e /dev/apex_0 ]]; then
|
||
msg_info "Detected Coral TPU - configuring passthrough"
|
||
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||
fi
|
||
}
|
||
|
||
# Execute pre-start configurations
|
||
configure_usb_passthrough
|
||
configure_gpu_passthrough
|
||
configure_additional_devices
|
||
|
||
# ============================================================================
|
||
# START CONTAINER AND INSTALL USERLAND
|
||
# ============================================================================
|
||
|
||
msg_info "Starting LXC Container"
|
||
pct start "$CTID"
|
||
|
||
# Wait for container to be running
|
||
for i in {1..10}; do
|
||
if pct status "$CTID" | grep -q "status: running"; then
|
||
msg_ok "Started LXC Container"
|
||
break
|
||
fi
|
||
sleep 1
|
||
if [ "$i" -eq 10 ]; then
|
||
msg_error "LXC Container did not reach running state"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
# Wait for network (skip for Alpine initially)
|
||
if [ "$var_os" != "alpine" ]; then
|
||
msg_info "Waiting for network in LXC container"
|
||
|
||
# Wait for IP
|
||
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
|
||
# Function to get correct GID inside container
|
||
get_container_gid() {
|
||
local group="$1"
|
||
local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
|
||
echo "${gid:-44}" # Default to 44 if not found
|
||
}
|
||
|
||
# Configure GPU passthrough
|
||
configure_gpu_passthrough() {
|
||
# Skip if not a GPU app and not privileged
|
||
if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then
|
||
return 0
|
||
fi
|
||
|
||
detect_gpu_devices
|
||
|
||
# Count available GPU types
|
||
local gpu_count=0
|
||
local available_gpus=()
|
||
|
||
if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("INTEL")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("AMD")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("NVIDIA")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ $gpu_count -eq 0 ]]; then
|
||
msg_info "No GPU devices found for passthrough"
|
||
return 0
|
||
fi
|
||
|
||
local selected_gpu=""
|
||
|
||
if [[ $gpu_count -eq 1 ]]; then
|
||
# Automatic selection for single GPU
|
||
selected_gpu="${available_gpus[0]}"
|
||
msg_info "Automatically configuring ${selected_gpu} GPU passthrough"
|
||
else
|
||
# Multiple GPUs - ask user
|
||
echo -e "\n${INFO} Multiple GPU types detected:"
|
||
for gpu in "${available_gpus[@]}"; do
|
||
echo " - $gpu"
|
||
done
|
||
read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu
|
||
selected_gpu="${selected_gpu^^}"
|
||
|
||
# Validate selection
|
||
local valid=0
|
||
for gpu in "${available_gpus[@]}"; do
|
||
[[ "$selected_gpu" == "$gpu" ]] && valid=1
|
||
done
|
||
|
||
if [[ $valid -eq 0 ]]; then
|
||
msg_warn "Invalid selection. Skipping GPU passthrough."
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Apply passthrough configuration based on selection
|
||
local dev_idx=0
|
||
|
||
case "$selected_gpu" in
|
||
INTEL|AMD)
|
||
local devices=()
|
||
[[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
|
||
[[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
|
||
|
||
# For Proxmox WebUI visibility, add as dev0, dev1 etc.
|
||
for dev in "${devices[@]}"; do
|
||
if [[ "$CT_TYPE" == "0" ]]; then
|
||
# Privileged container - use dev entries for WebUI visibility
|
||
# Use initial GID 104 (render) for renderD*, 44 (video) for card*
|
||
if [[ "$dev" =~ renderD ]]; then
|
||
echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG"
|
||
else
|
||
echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG"
|
||
fi
|
||
dev_idx=$((dev_idx + 1))
|
||
|
||
# Also add cgroup allows for privileged containers
|
||
local major minor
|
||
major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
|
||
minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
|
||
|
||
if [[ "$major" != "0" && "$minor" != "0" ]]; then
|
||
echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
|
||
fi
|
||
else
|
||
# Unprivileged container
|
||
if [[ "$dev" =~ renderD ]]; then
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG"
|
||
else
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
|
||
fi
|
||
dev_idx=$((dev_idx + 1))
|
||
fi
|
||
done
|
||
|
||
export GPU_TYPE="$selected_gpu"
|
||
msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)"
|
||
;;
|
||
|
||
NVIDIA)
|
||
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
|
||
msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver"
|
||
return 1
|
||
fi
|
||
|
||
for dev in "${NVIDIA_DEVICES[@]}"; do
|
||
# NVIDIA devices typically need different handling
|
||
echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG"
|
||
dev_idx=$((dev_idx + 1))
|
||
|
||
if [[ "$CT_TYPE" == "0" ]]; then
|
||
local major minor
|
||
major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0")
|
||
minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0")
|
||
|
||
if [[ "$major" != "0" && "$minor" != "0" ]]; then
|
||
echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
export GPU_TYPE="NVIDIA"
|
||
msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Continue with standard container setup
|
||
msg_info "Customizing LXC Container"
|
||
|
||
# # Install GPU userland if configured
|
||
# if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
|
||
# install_gpu_userland "VAAPI"
|
||
# fi
|
||
|
||
# if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
|
||
# install_gpu_userland "NVIDIA"
|
||
# fi
|
||
|
||
# Continue with standard container setup
|
||
if [ "$var_os" == "alpine" ]; then
|
||
sleep 3
|
||
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
|
||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
|
||
EOF'
|
||
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
|
||
else
|
||
sleep 3
|
||
pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
|
||
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
|
||
echo LANG=\$locale_line >/etc/default/locale && \
|
||
locale-gen >/dev/null && \
|
||
export LANG=\$locale_line"
|
||
|
||
if [[ -z "${tz:-}" ]]; then
|
||
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
|
||
fi
|
||
|
||
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
|
||
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
|