#!/usr/bin/env bash # Copyright (c) 2021-2025 community-scripts ORG # Author: tteck (tteckster) | MickLesk | michelroegl-brunner # License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE # Revision: 1 # ------------------------------------------------------------------------------ # variables() # # - Normalize application name (NSAPP = lowercase, no spaces) # - Build installer filename (var_install) # - Define regex for integer validation # - Fetch hostname of Proxmox node # - Set default values for diagnostics/method # - Generate random UUID for tracking # ------------------------------------------------------------------------------ variables() { NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern. PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. METHOD="default" # sets the METHOD variable to "default", used for the API call. RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. CTTYPE="${CTTYPE:-${CT_TYPE:-1}}" #CT_TYPE=${var_unprivileged:-$CT_TYPE} } # ----------------------------------------------------------------------------- # Community-Scripts bootstrap loader # - Always sources build.func from remote # - Updates local core files only if build.func changed # - Local cache: /usr/local/community-scripts/core # ----------------------------------------------------------------------------- # FUNC_DIR="/usr/local/community-scripts/core" # mkdir -p "$FUNC_DIR" # BUILD_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func" # BUILD_REV="$FUNC_DIR/build.rev" # DEVMODE="${DEVMODE:-no}" # # --- Step 1: fetch build.func content once, compute hash --- # build_content="$(curl -fsSL "$BUILD_URL")" || { # echo "❌ Failed to fetch build.func" # exit 1 # } # newhash=$(printf "%s" "$build_content" | sha256sum | awk '{print $1}') # oldhash=$(cat "$BUILD_REV" 2>/dev/null || echo "") # # --- Step 2: if build.func changed, offer update for core files --- # if [ "$newhash" != "$oldhash" ]; then # echo "⚠️ build.func changed!" # while true; do # read -rp "Refresh local core files? [y/N/diff]: " ans # case "$ans" in # [Yy]*) # echo "$newhash" >"$BUILD_REV" # update_func_file() { # local file="$1" # local url="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/$file" # local local_path="$FUNC_DIR/$file" # echo "⬇️ Downloading $file ..." # curl -fsSL "$url" -o "$local_path" || { # echo "❌ Failed to fetch $file" # exit 1 # } # echo "✔️ Updated $file" # } # update_func_file core.func # update_func_file error_handler.func # update_func_file tools.func # break # ;; # [Dd]*) # for file in core.func error_handler.func tools.func; do # local_path="$FUNC_DIR/$file" # url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/$file" # remote_tmp="$(mktemp)" # curl -fsSL "$url" -o "$remote_tmp" || continue # if [ -f "$local_path" ]; then # echo "🔍 Diff for $file:" # diff -u "$local_path" "$remote_tmp" || echo "(no differences)" # else # echo "📦 New file $file will be installed" # fi # rm -f "$remote_tmp" # done # ;; # *) # echo "❌ Skipped updating local core files" # break # ;; # esac # done # else # if [ "$DEVMODE" != "yes" ]; then # echo "✔️ build.func unchanged → using existing local core files" # fi # fi # if [ -n "${_COMMUNITY_SCRIPTS_LOADER:-}" ]; then # return 0 2>/dev/null || exit 0 # fi # _COMMUNITY_SCRIPTS_LOADER=1 # # --- Step 3: always source local versions of the core files --- # source "$FUNC_DIR/core.func" # source "$FUNC_DIR/error_handler.func" # source "$FUNC_DIR/tools.func" # # --- Step 4: finally, source build.func directly from memory --- # # (no tmp file needed) # source <(printf "%s" "$build_content") # ------------------------------------------------------------------------------ # Load core + error handler functions from community-scripts repo # # - Prefer curl if available, fallback to wget # - Load: core.func, error_handler.func, api.func # - Initialize error traps after loading # ------------------------------------------------------------------------------ source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/api.func) if command -v curl >/dev/null 2>&1; then source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) load_functions catch_errors #echo "(build.func) Loaded core.func via curl" elif command -v wget >/dev/null 2>&1; then source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) source <(wget -qO- https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) load_functions catch_errors #echo "(build.func) Loaded core.func via wget" fi # ------------------------------------------------------------------------------ # maxkeys_check() # # - Reads kernel keyring limits (maxkeys, maxbytes) # - Checks current usage for LXC user (UID 100000) # - Warns if usage is close to limits and suggests sysctl tuning # - Exits if thresholds are exceeded # - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html # ------------------------------------------------------------------------------ maxkeys_check() { # Read kernel parameters per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) # Exit if kernel parameters are unavailable if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}" exit 1 fi # Fetch key usage for user ID 100000 (typical for containers) used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) # Calculate thresholds and suggested new limits threshold_keys=$((per_user_maxkeys - 100)) threshold_bytes=$((per_user_maxbytes - 1000)) new_limit_keys=$((per_user_maxkeys * 2)) new_limit_bytes=$((per_user_maxbytes * 2)) # Check if key or byte usage is near limits failure=0 if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}" echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." failure=1 fi if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}" echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." failure=1 fi # Provide next steps if issues are detected if [[ "$failure" -eq 1 ]]; then echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}" exit 1 fi echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}" } # ------------------------------------------------------------------------------ # get_current_ip() # # - Returns current container IP depending on OS type # - Debian/Ubuntu: uses `hostname -I` # - Alpine: parses eth0 via `ip -4 addr` # ------------------------------------------------------------------------------ get_current_ip() { if [ -f /etc/os-release ]; then # Check for Debian/Ubuntu (uses hostname -I) if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then CURRENT_IP=$(hostname -I | awk '{print $1}') # Check for Alpine (uses ip command) elif grep -q 'ID=alpine' /etc/os-release; then CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) else CURRENT_IP="Unknown" fi fi echo "$CURRENT_IP" } # ------------------------------------------------------------------------------ # update_motd_ip() # # - Updates /etc/motd with current container IP # - Removes old IP entries to avoid duplicates # ------------------------------------------------------------------------------ update_motd_ip() { MOTD_FILE="/etc/motd" if [ -f "$MOTD_FILE" ]; then # Remove existing IP Address lines to prevent duplication sed -i '/IP Address:/d' "$MOTD_FILE" IP=$(get_current_ip) # Add the new IP address echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" fi } # ------------------------------------------------------------------------------ # install_ssh_keys_into_ct() # # - Installs SSH keys into container root account if SSH is enabled # - Uses pct push or direct input to authorized_keys # - Falls back to warning if no keys provided # ------------------------------------------------------------------------------ install_ssh_keys_into_ct() { [[ "$SSH" != "yes" ]] && return 0 if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then msg_info "Installing selected SSH keys into CT ${CTID}" pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { msg_error "prepare /root/.ssh failed" return 1 } pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 || pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || { msg_error "write authorized_keys failed" return 1 } pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true msg_ok "Installed SSH keys into CT ${CTID}" return 0 fi # Fallback: nichts ausgewählt msg_warn "No SSH keys to install (skipping)." return 0 } # ------------------------------------------------------------------------------ # base_settings() # # - Defines all base/default variables for container creation # - Reads from environment variables (var_*) # - Provides fallback defaults for OS type/version # ------------------------------------------------------------------------------ base_settings() { # Default Settings CT_TYPE=${var_unprivileged:-"1"} DISK_SIZE=${var_disk:-"4"} CORE_COUNT=${var_cpu:-"1"} RAM_SIZE=${var_ram:-"1024"} VERBOSE=${var_verbose:-"${1:-no}"} PW=${var_pw:-""} CT_ID=${var_ctid:-$NEXTID} HN=${var_hostname:-$NSAPP} BRG=${var_brg:-"vmbr0"} NET=${var_net:-"dhcp"} IPV6_METHOD=${var_ipv6_method:-"none"} IPV6_STATIC=${var_ipv6_static:-""} GATE=${var_gateway:-""} APT_CACHER=${var_apt_cacher:-""} APT_CACHER_IP=${var_apt_cacher_ip:-""} MTU=${var_mtu:-""} SD=${var_storage:-""} NS=${var_ns:-""} MAC=${var_mac:-""} VLAN=${var_vlan:-""} SSH=${var_ssh:-"no"} SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} UDHCPC_FIX=${var_udhcpc_fix:-""} TAGS="community-script,${var_tags:-}" ENABLE_FUSE=${var_fuse:-"${1:-no}"} ENABLE_TUN=${var_tun:-"${1:-no}"} # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts if [ -z "$var_os" ]; then var_os="debian" fi if [ -z "$var_version" ]; then var_version="12" fi } # ------------------------------------------------------------------------------ # echo_default() # # - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) # - Uses icons and formatting for readability # - Convert CT_TYPE to description # ------------------------------------------------------------------------------ echo_default() { CT_TYPE_DESC="Unprivileged" if [ "$CT_TYPE" -eq 0 ]; then CT_TYPE_DESC="Privileged" fi echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" if [ "$VERBOSE" == "yes" ]; then echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" fi echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" echo -e " " } # ------------------------------------------------------------------------------ # exit_script() # # - Called when user cancels an action # - Clears screen and exits gracefully # ------------------------------------------------------------------------------ exit_script() { clear echo -e "\n${CROSS}${RD}User exited script${CL}\n" exit } # ------------------------------------------------------------------------------ # find_host_ssh_keys() # # - Scans system for available SSH keys # - Supports defaults (~/.ssh, /etc/ssh/authorized_keys) # - Returns list of files containing valid SSH public keys # - Sets FOUND_HOST_KEY_COUNT to number of keys found # ------------------------------------------------------------------------------ find_host_ssh_keys() { local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))' local -a files=() cand=() local g="${var_ssh_import_glob:-}" local total=0 f base c shopt -s nullglob if [[ -n "$g" ]]; then for pat in $g; do cand+=($pat); done else cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) cand+=(/root/.ssh/*.pub) cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) fi shopt -u nullglob for f in "${cand[@]}"; do [[ -f "$f" && -r "$f" ]] || continue base="$(basename -- "$f")" case "$base" in known_hosts | known_hosts.* | config) continue ;; id_*) [[ "$f" != *.pub ]] && continue ;; esac # CRLF safe check for host keys c=$(tr -d '\r' <"$f" | awk ' /^[[:space:]]*#/ {next} /^[[:space:]]*$/ {next} {print} ' | grep -E -c '"$re"' || true) if ((c > 0)); then files+=("$f") total=$((total + c)) fi done # Fallback to /root/.ssh/authorized_keys if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then if grep -E -q "$re" /root/.ssh/authorized_keys; then files+=(/root/.ssh/authorized_keys) total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0))) fi fi FOUND_HOST_KEY_COUNT="$total" ( IFS=: echo "${files[*]}" ) } # ------------------------------------------------------------------------------ # advanced_settings() # # - Interactive whiptail menu for advanced configuration # - Lets user set container type, password, CT ID, hostname, disk, CPU, RAM # - Supports IPv4/IPv6, DNS, MAC, VLAN, tags, SSH keys, FUSE, verbose mode # - Ends with confirmation or re-entry if cancelled # ------------------------------------------------------------------------------ advanced_settings() { whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58 # Setting Default Tag for Advanced Settings TAGS="community-script;${var_tags:-}" CT_DEFAULT_TYPE="${CT_TYPE}" CT_TYPE="" while [ -z "$CT_TYPE" ]; do if [ "$CT_DEFAULT_TYPE" == "1" ]; then if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ "1" "Unprivileged" ON \ "0" "Privileged" OFF \ 3>&1 1>&2 2>&3); then if [ -n "$CT_TYPE" ]; then CT_TYPE_DESC="Unprivileged" if [ "$CT_TYPE" -eq 0 ]; then CT_TYPE_DESC="Privileged" fi echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os | ${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" fi else exit_script fi fi if [ "$CT_DEFAULT_TYPE" == "0" ]; then if CT_TYPE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ "1" "Unprivileged" OFF \ "0" "Privileged" ON \ 3>&1 1>&2 2>&3); then if [ -n "$CT_TYPE" ]; then CT_TYPE_DESC="Unprivileged" if [ "$CT_TYPE" -eq 0 ]; then CT_TYPE_DESC="Privileged" fi echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" fi else exit_script fi fi done while true; do if PW1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then # Empty = Autologin if [[ -z "$PW1" ]]; then PW="" PW1="Automatic Login" echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}" break fi # Invalid: contains spaces if [[ "$PW1" == *" "* ]]; then whiptail --msgbox "Password cannot contain spaces." 8 58 continue fi # Invalid: too short if ((${#PW1} < 5)); then whiptail --msgbox "Password must be at least 5 characters." 8 58 continue fi # Confirm password if PW2=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then if [[ "$PW1" == "$PW2" ]]; then PW="-password $PW1" echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" break else whiptail --msgbox "Passwords do not match. Please try again." 8 58 fi else exit_script fi else exit_script fi done if CT_ID=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then if [ -z "$CT_ID" ]; then CT_ID="$NEXTID" echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" else echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" fi else exit_script fi while true; do if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then if [ -z "$CT_NAME" ]; then HN="$NSAPP" else HN=$(echo "${CT_NAME,,}" | tr -d ' ') fi # Hostname validate (RFC 1123) if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" break else whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 fi else exit_script fi done while true; do DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script if [ -z "$DISK_SIZE" ]; then DISK_SIZE="$var_disk" fi if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" break else whiptail --msgbox "Disk size must be a positive integer!" 8 58 fi done while true; do CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script if [ -z "$CORE_COUNT" ]; then CORE_COUNT="$var_cpu" fi if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" break else whiptail --msgbox "CPU core count must be a positive integer!" 8 58 fi done while true; do RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script if [ -z "$RAM_SIZE" ]; then RAM_SIZE="$var_ram" fi if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" break else whiptail --msgbox "RAM size must be a positive integer!" 8 58 fi done IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) BRIDGES="" OLD_IFS=$IFS IFS=$'\n' for iface_filepath in ${IFACE_FILEPATH_LIST}; do iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true if [ -f "${iface_indexes_tmpfile}" ]; then while read -r pair; do start=$(echo "${pair}" | cut -d':' -f1) end=$(echo "${pair}" | cut -d':' -f2) if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') BRIDGES="${iface_name}"$'\n'"${BRIDGES}" fi done <"${iface_indexes_tmpfile}" rm -f "${iface_indexes_tmpfile}" fi done IFS=$OLD_IFS BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) if [[ -z "$BRIDGES" ]]; then BRG="vmbr0" echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" else BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3) if [[ -z "$BRG" ]]; then exit_script else echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" fi fi # IPv4 methods: dhcp, static, none while true; do IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --title "IPv4 Address Management" \ --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ "dhcp" "Automatic (DHCP, recommended)" \ "static" "Static (manual entry)" \ 3>&1 1>&2 2>&3) exit_status=$? if [ $exit_status -ne 0 ]; then exit_script fi case "$IPV4_METHOD" in dhcp) NET="dhcp" GATE="" echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" break ;; static) # Static: call and validate CIDR address while true; do NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) if [ -z "$NET" ]; then whiptail --msgbox "IPv4 address must not be empty." 8 58 continue elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" break else whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 fi done # call and validate Gateway while true; do GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ --title "Gateway IP" 3>&1 1>&2 2>&3) if [ -z "$GATE1" ]; then whiptail --msgbox "Gateway IP address cannot be empty." 8 58 elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then whiptail --msgbox "Invalid Gateway IP address format." 8 58 else GATE=",gw=$GATE1" echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" break fi done break ;; esac done # IPv6 Address Management selection while true; do IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ "Select IPv6 Address Management Type:" 15 58 4 \ "auto" "SLAAC/AUTO (recommended, default)" \ "dhcp" "DHCPv6" \ "static" "Static (manual entry)" \ "none" "Disabled" \ --default-item "auto" 3>&1 1>&2 2>&3) [ $? -ne 0 ] && exit_script case "$IPV6_METHOD" in auto) echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" IPV6_ADDR="" IPV6_GATE="" break ;; dhcp) echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" IPV6_ADDR="dhcp" IPV6_GATE="" break ;; static) # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) while true; do IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" break else whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 fi done # Optional: ask for IPv6 gateway for static config while true; do IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) if [ -z "$IPV6_GATE" ]; then IPV6_GATE="" break elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then break else whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ "Invalid IPv6 gateway format." 8 58 fi done break ;; none) echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" IPV6_ADDR="none" IPV6_GATE="" break ;; *) exit_script ;; esac done if [ "$var_os" == "alpine" ]; then APT_CACHER="" APT_CACHER_IP="" else if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then APT_CACHER="${APT_CACHER_IP:+yes}" echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" else exit_script fi fi # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then # DISABLEIP6="yes" # else # DISABLEIP6="no" # fi # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then if [ -z "$MTU1" ]; then MTU1="Default" MTU="" else MTU=",mtu=$MTU1" fi echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" else exit_script fi if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then if [ -z "$SD" ]; then SX=Host SD="" else SX=$SD SD="-searchdomain=$SD" fi echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" else exit_script fi if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then if [ -z "$NX" ]; then NX=Host NS="" else NS="-nameserver=$NX" fi echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" else exit_script fi if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then UDHCPC_FIX="yes" else UDHCPC_FIX="no" fi export UDHCPC_FIX if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then if [ -z "$MAC1" ]; then MAC1="Default" MAC="" else MAC=",hwaddr=$MAC1" echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" fi else exit_script fi if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then if [ -z "$VLAN1" ]; then VLAN1="Default" VLAN="" else VLAN=",tag=$VLAN1" fi echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" else exit_script fi if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then if [ -n "${ADV_TAGS}" ]; then ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') TAGS="${ADV_TAGS}" else TAGS=";" fi echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" else exit_script fi # --- SSH key provisioning (one dialog) --- SSH_KEYS_FILE="$(mktemp)" : >"$SSH_KEYS_FILE" IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') ssh_build_choices_from_files "${_def_files[@]}" DEF_KEYS_COUNT="$COUNT" if [[ "$DEF_KEYS_COUNT" -gt 0 ]]; then SSH_KEY_MODE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ "Provision SSH keys for root:" 14 72 4 \ "found" "Select from detected keys (${DEF_KEYS_COUNT})" \ "manual" "Paste a single public key" \ "folder" "Scan another folder (path or glob)" \ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script else SSH_KEY_MODE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ "No host keys detected; choose manual/none:" 12 72 2 \ "manual" "Paste a single public key" \ "none" "No keys" 3>&1 1>&2 2>&3) || exit_script fi case "$SSH_KEY_MODE" in found) SEL=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT HOST KEYS" \ --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script for tag in $SEL; do tag="${tag%\"}" tag="${tag#\"}" line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" done ;; manual) SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)" [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE" ;; folder) GLOB_PATH="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)" if [[ -n "$GLOB_PATH" ]]; then shopt -s nullglob read -r -a _scan_files <<<"$GLOB_PATH" shopt -u nullglob if [[ "${#_scan_files[@]}" -gt 0 ]]; then ssh_build_choices_from_files "${_scan_files[@]}" if [[ "$COUNT" -gt 0 ]]; then SEL=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SELECT FOLDER KEYS" \ --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script for tag in $SEL; do tag="${tag%\"}" tag="${tag#\"}" line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" done else whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "No keys found in: $GLOB_PATH" 8 60 fi else whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox "Path/glob returned no files." 8 60 fi fi ;; none) : ;; esac # Dedupe + clean EOF if [[ -s "$SSH_KEYS_FILE" ]]; then sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" printf '\n' >>"$SSH_KEYS_FILE" fi # SSH activate, if keys found or password set if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then SSH="yes" else SSH="no" fi else SSH="no" fi echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" export SSH_KEYS_FILE if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE Support" --yesno "Enable FUSE support?\nRequired for tools like rclone, mergerfs, AppImage, etc." 10 58); then ENABLE_FUSE="yes" else ENABLE_FUSE="no" fi echo -e "${FUSE}${BOLD}${DGN}Enable FUSE Support: ${BGN}$ENABLE_FUSE${CL}" if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then VERBOSE="yes" else VERBOSE="no" fi echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" else clear header_info echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" advanced_settings fi } # ------------------------------------------------------------------------------ # diagnostics_check() # # - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics # - Asks user whether to send anonymous diagnostic data # - Saves DIAGNOSTICS=yes/no in the config file # ------------------------------------------------------------------------------ diagnostics_check() { if ! [ -d "/usr/local/community-scripts" ]; then mkdir -p /usr/local/community-scripts fi if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then cat </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 </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/.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 .vars exists, offers to persist current advanced settings # into /usr/local/community-scripts/defaults/.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 .vars file from current advanced settings _build_current_app_vars_tmp() { tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" # NET/GW _net="${NET:-}" _gate="" case "${GATE:-}" in ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; esac # IPv6 _ipv6_method="${IPV6_METHOD:-auto}" _ipv6_static="" _ipv6_gateway="" if [ "$_ipv6_method" = "static" ]; then _ipv6_static="${IPV6_ADDR:-}" _ipv6_gateway="${IPV6_GATE:-}" fi # MTU/VLAN/MAC _mtu="" _vlan="" _mac="" case "${MTU:-}" in ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; esac case "${VLAN:-}" in ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; esac case "${MAC:-}" in ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; esac # DNS / Searchdomain _ns="" _searchdomain="" case "${NS:-}" in -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; esac case "${SD:-}" in -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; esac # SSH / APT / Features _ssh="${SSH:-no}" _ssh_auth="${SSH_AUTHORIZED_KEY:-}" _apt_cacher="${APT_CACHER:-}" _apt_cacher_ip="${APT_CACHER_IP:-}" _fuse="${ENABLE_FUSE:-no}" _tun="${ENABLE_TUN:-no}" _tags="${TAGS:-}" _verbose="${VERBOSE:-no}" # Type / Resources / Identity _unpriv="${CT_TYPE:-1}" _cpu="${CORE_COUNT:-1}" _ram="${RAM_SIZE:-1024}" _disk="${DISK_SIZE:-4}" _hostname="${HN:-$NSAPP}" # Storage _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" { echo "# App-specific defaults for ${APP} (${NSAPP})" echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" echo echo "var_unprivileged=$(_sanitize_value "$_unpriv")" echo "var_cpu=$(_sanitize_value "$_cpu")" echo "var_ram=$(_sanitize_value "$_ram")" echo "var_disk=$(_sanitize_value "$_disk")" [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")" [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" } >"$tmpf" echo "$tmpf" } # ------------------------------------------------------------------------------ # maybe_offer_save_app_defaults() # # - Called after advanced_settings() # - Offers to save current values as app defaults if not existing # - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel # ------------------------------------------------------------------------------ maybe_offer_save_app_defaults() { local app_vars_path app_vars_path="$(get_app_defaults_path)" # always build from current settings local new_tmp diff_tmp new_tmp="$(_build_current_app_vars_tmp)" diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" # 1) if no file → offer to create if [[ ! -f "$app_vars_path" ]]; then if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then mkdir -p "$(dirname "$app_vars_path")" install -m 0644 "$new_tmp" "$app_vars_path" msg_ok "Saved app defaults: ${app_vars_path}" fi rm -f "$new_tmp" "$diff_tmp" return 0 fi # 2) if file exists → build diff _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" # if no differences → do nothing if grep -q "^(No differences)$" "$diff_tmp"; then rm -f "$new_tmp" "$diff_tmp" return 0 fi # 3) if file exists → show menu with default selection "Update Defaults" local app_vars_file app_vars_file="$(basename "$app_vars_path")" while true; do local sel sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --title "APP DEFAULTS – ${APP}" \ --menu "Differences detected. What do you want to do?" 20 78 10 \ "Update Defaults" "Write new values to ${app_vars_file}" \ "Keep Current" "Keep existing defaults (no changes)" \ "View Diff" "Show a detailed diff" \ "Cancel" "Abort without changes" \ --default-item "Update Defaults" \ 3>&1 1>&2 2>&3)" || { sel="Cancel"; } case "$sel" in "Update Defaults") install -m 0644 "$new_tmp" "$app_vars_path" msg_ok "Updated app defaults: ${app_vars_path}" break ;; "Keep Current") msg_info "Keeping current app defaults: ${app_vars_path}" break ;; "View Diff") whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --title "Diff – ${APP}" \ --scrolltext --textbox "$diff_tmp" 25 100 ;; "Cancel" | *) msg_info "Canceled. No changes to app defaults." break ;; esac done rm -f "$new_tmp" "$diff_tmp" } ensure_storage_selection_for_vars_file() { vf="$1" # Read stored values (if any) tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) # Template storage if [ -n "$tpl" ]; then TEMPLATE_STORAGE="$tpl" else choose_and_set_storage_for_file "$vf" template fi # Container storage if [ -n "$ct" ]; then CONTAINER_STORAGE="$ct" else choose_and_set_storage_for_file "$vf" container fi echo_storage_summary_from_file "$vf" } diagnostics_menu() { if [ "${DIAGNOSTICS:-no}" = "yes" ]; then if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "No" --no-button "Back"; then DIAGNOSTICS="no" sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 fi else if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ --title "DIAGNOSTIC SETTINGS" \ --yesno "Send Diagnostics?\n\nCurrent: ${DIAGNOSTICS}" 10 58 \ --yes-button "Yes" --no-button "Back"; then DIAGNOSTICS="yes" sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics whiptail --msgbox "Diagnostics set to ${DIAGNOSTICS}." 8 58 fi fi } # ------------------------------------------------------------------------------ # install_script() # # - Main entrypoint for installation mode # - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) # - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) # - Applies chosen settings and triggers container build # ------------------------------------------------------------------------------ install_script() { pve_check shell_check root_check arch_check ssh_check maxkeys_check diagnostics_check if systemctl is-active -q ping-instances.service; then systemctl -q stop ping-instances.service fi NEXTID=$(pvesh get /cluster/nextid) timezone=$(cat /etc/timezone) header_info # --- Support CLI argument as direct preset (default, advanced, …) --- CHOICE="${mode:-${1:-}}" # If no CLI argument → show whiptail menu if [ -z "$CHOICE" ]; then local menu_items=( "1" "Default Install" "2" "Advanced Install" "3" "My Defaults" ) if [ -f "$(get_app_defaults_path)" ]; then menu_items+=("4" "App Defaults for ${APP}") menu_items+=("5" "Settings") else menu_items+=("4" "Settings") fi TMP_CHOICE=$(whiptail \ --backtitle "Proxmox VE Helper Scripts" \ --title "Community-Scripts Options" \ --ok-button "Select" --cancel-button "Exit Script" \ --notags \ --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ 20 60 9 \ "${menu_items[@]}" \ --default-item "1" \ 3>&1 1>&2 2>&3) || exit_script CHOICE="$TMP_CHOICE" fi # --- Main case --- case "$CHOICE" in 1 | default | DEFAULT) header_info echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" VERBOSE="no" METHOD="default" base_settings "$VERBOSE" echo_default [[ -f /usr/local/community-scripts/default.vars ]] || { mkdir -p /usr/local/community-scripts touch /usr/local/community-scripts/default.vars } ensure_storage_selection_for_vars_file "/usr/local/community-scripts/default.vars" ;; 2 | advanced | ADVANCED) header_info echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" METHOD="advanced" base_settings advanced_settings [[ -f /usr/local/community-scripts/default.vars ]] || { mkdir -p /usr/local/community-scripts touch /usr/local/community-scripts/default.vars } ensure_storage_selection_for_vars_file "/usr/local/community-scripts/default.vars" maybe_offer_save_app_defaults ;; 3 | mydefaults | MYDEFAULTS) default_var_settings || { msg_error "Failed to apply default.vars" exit 1 } ensure_storage_selection_for_vars_file "/usr/local/community-scripts/default.vars" ;; 4 | appdefaults | APPDEFAULTS) if [ -f "$(get_app_defaults_path)" ]; then header_info echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" METHOD="appdefaults" base_settings _load_vars_file "$(get_app_defaults_path)" echo_default ensure_storage_selection_for_vars_file "$(get_app_defaults_path)" else msg_error "No App Defaults available for ${APP}" exit 1 fi ;; 5 | settings | SETTINGS) settings_menu ;; *) echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" exit 1 ;; esac } edit_default_storage() { local vf="/usr/local/community-scripts/default.vars" # make sure file exists if [ ! -f "$vf" ]; then msg_info "No default.vars found, creating $vf" mkdir -p /usr/local/community-scripts touch "$vf" fi # reuse the same Whiptail selection we already have ensure_storage_selection_for_vars_file "$vf" } settings_menu() { while true; do local settings_items=( "1" "Manage API-Diagnostic Setting" "2" "Edit Default.vars" "3" "Edit Default Storage" ) if [ -f "$(get_app_defaults_path)" ]; then settings_items+=("4" "Edit App.vars for ${APP}") settings_items+=("5" "Exit") else settings_items+=("4" "Exit") fi local choice choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ --title "Community-Scripts SETTINGS Menu" \ --ok-button "OK" --cancel-button "Back" \ --menu "\n\nChoose a settings option:\n\nUse TAB or Arrow keys to navigate, ENTER to select." 20 60 9 \ "${settings_items[@]}" \ 3>&1 1>&2 2>&3) || break case "$choice" in 1) diagnostics_menu ;; 2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;; 3) edit_default_storage ;; 4) if [ -f "$(get_app_defaults_path)" ]; then ${EDITOR:-nano} "$(get_app_defaults_path)" else exit_script fi ;; 5) exit_script ;; esac done } # ===== Unified storage selection & writing to vars files ===== _write_storage_to_vars() { # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value local vf="$1" key="$2" val="$3" # remove uncommented and commented versions to avoid duplicates sed -i "/^[#[:space:]]*${key}=/d" "$vf" echo "${key}=${val}" >>"$vf" } choose_and_set_storage_for_file() { # $1 = vars_file, $2 = class ('container'|'template') local vf="$1" class="$2" key="" current="" case "$class" in container) key="var_container_storage" ;; template) key="var_template_storage" ;; *) msg_error "Unknown storage class: $class" return 1 ;; esac current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf") # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4). local content="rootdir" [[ "$class" == "template" ]] && content="vztmpl" local count count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l) if [[ "$count" -eq 1 ]]; then STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') STORAGE_INFO="" else # If the current value is preselectable, we could show it, but per your requirement we always offer selection select_storage "$class" || return 1 fi _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT" # Keep environment in sync for later steps (e.g. app-default save) if [[ "$class" == "container" ]]; then export var_container_storage="$STORAGE_RESULT" export CONTAINER_STORAGE="$STORAGE_RESULT" else export var_template_storage="$STORAGE_RESULT" export TEMPLATE_STORAGE="$STORAGE_RESULT" fi msg_ok "Updated ${key} → ${STORAGE_RESULT}" } echo_storage_summary_from_file() { local vars_file="$1" local ct_store tpl_store ct_store=$(awk -F= '/^var_container_storage=/ {print $2; exit}' "$vars_file") tpl_store=$(awk -F= '/^var_template_storage=/ {print $2; exit}' "$vars_file") if [ -n "$tpl" ]; then TEMPLATE_STORAGE="$tpl" else choose_and_set_storage_for_file "$vf" template fi if [ -n "$ct" ]; then CONTAINER_STORAGE="$ct" else choose_and_set_storage_for_file "$vf" container fi } # ------------------------------------------------------------------------------ # check_container_resources() # # - Compares host RAM/CPU with required values # - Warns if under-provisioned and asks user to continue or abort # ------------------------------------------------------------------------------ check_container_resources() { current_ram=$(free -m | awk 'NR==2{print $2}') current_cpu=$(nproc) if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " 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? " 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 -install.sh # ------------------------------------------------------------------------------ build_container() { # if [ "$VERBOSE" == "yes" ]; then set -x; fi NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" # MAC if [[ -n "$MAC" ]]; then case "$MAC" in ,hwaddr=*) NET_STRING+="$MAC" ;; *) NET_STRING+=",hwaddr=$MAC" ;; esac fi # IP (immer zwingend, Standard dhcp) NET_STRING+=",ip=${NET:-dhcp}" # Gateway if [[ -n "$GATE" ]]; then case "$GATE" in ,gw=*) NET_STRING+="$GATE" ;; *) NET_STRING+=",gw=$GATE" ;; esac fi # VLAN if [[ -n "$VLAN" ]]; then case "$VLAN" in ,tag=*) NET_STRING+="$VLAN" ;; *) NET_STRING+=",tag=$VLAN" ;; esac fi # MTU if [[ -n "$MTU" ]]; then case "$MTU" in ,mtu=*) NET_STRING+="$MTU" ;; *) NET_STRING+=",mtu=$MTU" ;; esac fi # IPv6 Handling case "$IPV6_METHOD" in auto) NET_STRING="$NET_STRING,ip6=auto" ;; dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;; static) NET_STRING="$NET_STRING,ip6=$IPV6_ADDR" [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE" ;; none) ;; esac if [ "$CT_TYPE" == "1" ]; then FEATURES="keyctl=1,nesting=1" else FEATURES="nesting=1" fi if [ "$ENABLE_FUSE" == "yes" ]; then FEATURES="$FEATURES,fuse=1" fi TEMP_DIR=$(mktemp -d) pushd "$TEMP_DIR" >/dev/null if [ "$var_os" == "alpine" ]; then export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/alpine-install.func)" else export FUNCTIONS_FILE_PATH="$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/install.func)" fi export DIAGNOSTICS="$DIAGNOSTICS" export RANDOM_UUID="$RANDOM_UUID" export CACHER="$APT_CACHER" export CACHER_IP="$APT_CACHER_IP" export tz="$timezone" export APPLICATION="$APP" export app="$NSAPP" export PASSWORD="$PW" export VERBOSE="$VERBOSE" export SSH_ROOT="${SSH}" export SSH_AUTHORIZED_KEY export CTID="$CT_ID" export CTTYPE="$CT_TYPE" export ENABLE_FUSE="$ENABLE_FUSE" export ENABLE_TUN="$ENABLE_TUN" export PCT_OSTYPE="$var_os" export PCT_OSVERSION="${var_version%%.*}" export PCT_DISK_SIZE="$DISK_SIZE" export PCT_OPTIONS=" -features $FEATURES -hostname $HN -tags $TAGS $SD $NS $NET_STRING -onboot 1 -cores $CORE_COUNT -memory $RAM_SIZE -unprivileged $CT_TYPE $PW " export TEMPLATE_STORAGE="${var_template_storage:-}" export CONTAINER_STORAGE="${var_container_storage:-}" create_lxc_container || exit $? LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" # ============================================================================ # GPU/USB PASSTHROUGH CONFIGURATION # ============================================================================ # List of applications that benefit from GPU acceleration GPU_APPS=( "immich" "channels" "emby" "ersatztv" "frigate" "jellyfin" "plex" "scrypted" "tdarr" "unmanic" "ollama" "fileflows" "open-webui" "tunarr" "debian" "handbrake" "sunshine" "moonlight" "kodi" "stremio" ) # ------------------------------------------------------------------------------ # Helper Functions for GPU/USB Configuration # ------------------------------------------------------------------------------ # Get device GID dynamically get_device_gid() { local group="$1" local gid gid=$(getent group "$group" 2>/dev/null | cut -d: -f3) if [[ -z "$gid" ]]; then case "$group" in video) gid=44 ;; render) gid=104 ;; *) gid=44 ;; esac fi echo "$gid" } # Check if app needs GPU is_gpu_app() { local app="${1,,}" for gpu_app in "${GPU_APPS[@]}"; do [[ "$app" == "${gpu_app,,}" ]] && return 0 done return 1 } # Detect available GPU devices detect_gpu_devices() { VAAPI_DEVICES=() NVIDIA_DEVICES=() for device in $(compgen -G "/dev/dri/renderD*" || true); do VAAPI_DEVICES+=("$device") done for device in $(compgen -G "/dev/dri/card*" || true); do VAAPI_DEVICES+=("$device") done for device in $(compgen -G "/dev/nvidia*" || true); do NVIDIA_DEVICES+=("$device") done } # Configure USB passthrough for privileged containers configure_usb_passthrough() { if [[ "$CT_TYPE" != "0" ]]; then return 0 fi msg_info "Configuring automatic USB passthrough (privileged container)" cat <>"$LXC_CONFIG" # USB passthrough (privileged container) lxc.cgroup2.devices.allow: a lxc.cap.drop: lxc.cgroup2.devices.allow: c 188:* rwm lxc.cgroup2.devices.allow: c 189:* rwm lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file EOF msg_ok "USB passthrough configured" } # Configure VAAPI device configure_vaapi_device() { local device="$1" local dev_index="$2" if [[ "$CT_TYPE" == "0" ]]; then # Privileged container local major minor major=$(stat -c '%t' "$device") minor=$(stat -c '%T' "$device") major=$((0x$major)) minor=$((0x$minor)) echo "lxc.cgroup2.devices.allow: c $major:$minor rwm" >>"$LXC_CONFIG" echo "lxc.mount.entry: $device dev/$(basename "$device") none bind,optional,create=file" >>"$LXC_CONFIG" else # Unprivileged container local gid if [[ "$device" =~ renderD ]]; then gid=$(get_device_gid "render") else gid=$(get_device_gid "video") fi echo "dev${dev_index}: $device,gid=$gid" >>"$LXC_CONFIG" fi } # Configure NVIDIA devices configure_nvidia_devices() { for device in "${NVIDIA_DEVICES[@]}"; do if [[ "$CT_TYPE" == "0" ]]; then local major minor major=$(stat -c '%t' "$device") minor=$(stat -c '%T' "$device") major=$((0x$major)) minor=$((0x$minor)) echo "lxc.cgroup2.devices.allow: c $major:$minor rwm" >>"$LXC_CONFIG" echo "lxc.mount.entry: $device dev/$(basename "$device") none bind,optional,create=file" >>"$LXC_CONFIG" else msg_warn "NVIDIA passthrough to unprivileged container may require additional configuration" fi done if [[ -d /dev/dri ]] && [[ "$CT_TYPE" == "0" ]]; then echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG" fi } # Main GPU configuration logic configure_gpu_passthrough() { detect_gpu_devices # Check if we should configure GPU local should_configure=false if [[ "$CT_TYPE" == "0" ]] || is_gpu_app "$APP"; then should_configure=true fi if [[ "$should_configure" == "false" ]]; then return 0 fi # No GPU devices available if [[ ${#VAAPI_DEVICES[@]} -eq 0 ]] && [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then msg_info "No GPU devices detected on host" return 0 fi # Build selection options local choices=() local SELECTED_GPUS=() if [[ ${#VAAPI_DEVICES[@]} -gt 0 ]]; then choices+=("VAAPI" "Intel/AMD GPU (${#VAAPI_DEVICES[@]} devices)" "OFF") fi if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then choices+=("NVIDIA" "NVIDIA GPU (${#NVIDIA_DEVICES[@]} devices)" "OFF") fi # Auto-select if only one type available if [[ ${#choices[@]} -eq 3 ]]; then SELECTED_GPUS=("${choices[0]}") msg_info "Auto-configuring ${choices[0]} GPU passthrough" elif [[ ${#choices[@]} -gt 3 ]]; then # Show selection dialog local selected selected=$(whiptail --title "GPU Passthrough Selection" \ --checklist "Multiple GPU types detected. Select which to pass through:" \ 12 60 $((${#choices[@]} / 3)) \ "${choices[@]}" 3>&1 1>&2 2>&3) || { msg_info "GPU passthrough skipped" return 0 } for item in $selected; do SELECTED_GPUS+=("${item//\"/}") done fi # Apply configuration for selected GPUs local dev_index=0 for gpu_type in "${SELECTED_GPUS[@]}"; do case "$gpu_type" in VAAPI) msg_info "Configuring VAAPI passthrough (${#VAAPI_DEVICES[@]} devices)" for device in "${VAAPI_DEVICES[@]}"; do configure_vaapi_device "$device" "$dev_index" : "${dev_index:=0}" dev_index=$((dev_index + 1)) done if [[ "$CT_TYPE" == "0" ]] && [[ -d /dev/dri ]]; then echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG" fi export ENABLE_VAAPI=1 ;; NVIDIA) msg_info "Configuring NVIDIA passthrough" configure_nvidia_devices export ENABLE_NVIDIA=1 ;; esac done [[ ${#SELECTED_GPUS[@]} -gt 0 ]] && msg_ok "GPU passthrough configured" } # ------------------------------------------------------------------------------ # Apply all hardware passthrough configurations # ------------------------------------------------------------------------------ # USB passthrough (automatic for privileged) configure_usb_passthrough # GPU passthrough (based on container type and app) configure_gpu_passthrough # TUN device passthrough if [ "$ENABLE_TUN" == "yes" ]; then cat <>"$LXC_CONFIG" lxc.cgroup2.devices.allow: c 10:200 rwm lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file EOF fi # Coral TPU passthrough (if available) if [[ -e /dev/apex_0 ]]; then msg_info "Detected Coral TPU - configuring passthrough" echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" fi # ============================================================================ # START CONTAINER AND INSTALL USERLAND # ============================================================================ msg_info "Starting LXC Container" pct start "$CTID" # Wait for container to be running for i in {1..10}; do if pct status "$CTID" | grep -q "status: running"; then msg_ok "Started LXC Container" break fi sleep 1 if [ "$i" -eq 10 ]; then msg_error "LXC Container did not reach running state" exit 1 fi done # Wait for network (skip for Alpine initially) if [ "$var_os" != "alpine" ]; then msg_info "Waiting for network in LXC container" # Wait for IP for i in {1..20}; do ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) [ -n "$ip_in_lxc" ] && break sleep 1 done if [ -z "$ip_in_lxc" ]; then msg_error "No IP assigned to CT $CTID after 20s" exit 1 fi # Try to reach gateway gw_ok=0 for i in {1..10}; do if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then gw_ok=1 break fi sleep 1 done if [ "$gw_ok" -eq 1 ]; then msg_ok "Network in LXC is reachable (IP $ip_in_lxc)" else msg_warn "Network reachable but gateway check failed" fi fi # Install GPU userland packages install_gpu_userland() { local gpu_type="$1" if [ "$var_os" == "alpine" ]; then case "$gpu_type" in VAAPI) msg_info "Installing VAAPI packages in Alpine container" pct exec "$CTID" -- ash -c ' apk add --no-cache \ mesa-dri-gallium \ mesa-va-gallium \ intel-media-driver \ libva-utils >/dev/null 2>&1 ' || msg_warn "Some VAAPI packages may not be available in Alpine" ;; NVIDIA) msg_warn "NVIDIA drivers are not readily available in Alpine Linux" ;; esac else case "$gpu_type" in VAAPI) msg_info "Installing VAAPI userland packages" pct exec "$CTID" -- bash -c ' . /etc/os-release || true if [[ "${VERSION_CODENAME:-}" == "trixie" ]]; then cat >/etc/apt/sources.list.d/non-free.sources </dev/null 2>&1 DEBIAN_FRONTEND=noninteractive apt-get install -y \ intel-media-va-driver-non-free \ mesa-va-drivers \ libvpl2 \ vainfo \ ocl-icd-libopencl1 \ mesa-opencl-icd \ intel-gpu-tools >/dev/null 2>&1 ' && msg_ok "VAAPI userland installed" || msg_warn "Some VAAPI packages failed to install" ;; NVIDIA) msg_info "Installing NVIDIA userland packages" pct exec "$CTID" -- bash -c ' apt-get update >/dev/null 2>&1 DEBIAN_FRONTEND=noninteractive apt-get install -y \ nvidia-driver \ nvidia-utils \ libnvidia-encode1 \ libcuda1 >/dev/null 2>&1 ' && msg_ok "NVIDIA userland installed" || msg_warn "Some NVIDIA packages failed to install" ;; esac fi } # Customize container msg_info "Customizing LXC Container" # Install GPU userland if configured if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then install_gpu_userland "VAAPI" fi if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then install_gpu_userland "NVIDIA" fi # Continue with standard container setup if [ "$var_os" == "alpine" ]; then sleep 3 pct exec "$CTID" -- /bin/sh -c 'cat </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? " 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 < Logo

${APP} LXC

spend Coffee

GitHub Discussions Issues 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