diff --git a/bak/core_bak/build copy 2.func b/bak/core_bak/build copy 2.func deleted file mode 100644 index 45b304253..000000000 --- a/bak/core_bak/build copy 2.func +++ /dev/null @@ -1,3549 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE -# Revision: 1 - -# ============================================================================== -# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION -# ============================================================================== -# -# This file provides the main build functions for creating and configuring -# LXC containers in Proxmox VE. It handles: -# -# - Variable initialization and defaults -# - Container creation and resource allocation -# - Storage selection and management -# - Advanced configuration and customization -# - User interaction menus and prompts -# -# Usage: -# - Sourced automatically by CT creation scripts -# - Requires core.func and error_handler.func to be loaded first -# -# ============================================================================== - -# ============================================================================== -# SECTION 1: INITIALIZATION & CORE VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# variables() -# -# - Initializes core variables for container creation -# - Normalizes application name (NSAPP = lowercase, no spaces) -# - Builds installer filename (var_install) -# - Defines regex patterns for validation -# - Fetches Proxmox hostname and version -# - Generates unique session ID for tracking and logging -# - Captures app-declared resource defaults (CPU, RAM, Disk) -# ------------------------------------------------------------------------------ -variables() { - NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. - var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. - INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern. - PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase - DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. - METHOD="default" # sets the METHOD variable to "default", used for the API call. - RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. - SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files - CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"} - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) - - # Capture app-declared defaults (for precedence logic) - # These values are set by the app script BEFORE default.vars is loaded - # If app declares higher values than default.vars, app values take precedence - if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_CPU="${var_cpu}" - fi - if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_RAM="${var_ram}" - fi - if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_DISK="${var_disk}" - fi -} - -# ----------------------------------------------------------------------------- -# 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 - -# ============================================================================== -# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# maxkeys_check() -# -# - Reads kernel keyring limits (maxkeys, maxbytes) -# - Checks current usage for LXC user (UID 100000) -# - Warns if usage is close to limits and suggests sysctl tuning -# - Exits if thresholds are exceeded -# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html -# ------------------------------------------------------------------------------ - -maxkeys_check() { - # Read kernel parameters - per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) - per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) - - # Exit if kernel parameters are unavailable - if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then - echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}" - exit 1 - fi - - # Fetch key usage for user ID 100000 (typical for containers) - used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) - used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) - - # Calculate thresholds and suggested new limits - threshold_keys=$((per_user_maxkeys - 100)) - threshold_bytes=$((per_user_maxbytes - 1000)) - new_limit_keys=$((per_user_maxkeys * 2)) - new_limit_bytes=$((per_user_maxbytes * 2)) - - # Check if key or byte usage is near limits - failure=0 - if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then - echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}" - echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." - failure=1 - fi - if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then - echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}" - echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." - failure=1 - fi - - # Provide next steps if issues are detected - if [[ "$failure" -eq 1 ]]; then - echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}" - exit 1 - fi - - # Silent success - only show errors if they exist -} - -# ============================================================================== -# SECTION 3: CONTAINER SETUP UTILITIES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# get_current_ip() -# -# - Returns current container IP depending on OS type -# - Debian/Ubuntu: uses `hostname -I` -# - Alpine: parses eth0 via `ip -4 addr` -# - Returns "Unknown" if OS type cannot be determined -# ------------------------------------------------------------------------------ -get_current_ip() { - if [ -f /etc/os-release ]; then - # Check for Debian/Ubuntu (uses hostname -I) - if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then - CURRENT_IP=$(hostname -I | awk '{print $1}') - # Check for Alpine (uses ip command) - elif grep -q 'ID=alpine' /etc/os-release; then - CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) - else - CURRENT_IP="Unknown" - fi - fi - echo "$CURRENT_IP" -} - -# ------------------------------------------------------------------------------ -# update_motd_ip() -# -# - Updates /etc/motd with current container IP -# - Removes old IP entries to avoid duplicates -# ------------------------------------------------------------------------------ -update_motd_ip() { - MOTD_FILE="/etc/motd" - - if [ -f "$MOTD_FILE" ]; then - # Remove existing IP Address lines to prevent duplication - sed -i '/IP Address:/d' "$MOTD_FILE" - - IP=$(get_current_ip) - # Add the new IP address - echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" - fi -} - -# ------------------------------------------------------------------------------ -# install_ssh_keys_into_ct() -# -# - Installs SSH keys into container root account if SSH is enabled -# - Uses pct push or direct input to authorized_keys -# - Falls back to warning if no keys provided -# ------------------------------------------------------------------------------ -install_ssh_keys_into_ct() { - [[ "$SSH" != "yes" ]] && return 0 - - if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then - msg_info "Installing selected SSH keys into CT ${CTID}" - pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { - msg_error "prepare /root/.ssh failed" - return 1 - } - pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 || - pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || { - msg_error "write authorized_keys failed" - return 1 - } - pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true - msg_ok "Installed SSH keys into CT ${CTID}" - return 0 - fi - - # Fallback: nichts ausgewählt - msg_warn "No SSH keys to install (skipping)." - return 0 -} - -# ------------------------------------------------------------------------------ -# find_host_ssh_keys() -# -# - Scans system for available SSH keys -# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys) -# - Returns list of files containing valid SSH public keys -# - Sets FOUND_HOST_KEY_COUNT to number of keys found -# ------------------------------------------------------------------------------ -find_host_ssh_keys() { - local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))' - local -a files=() cand=() - local g="${var_ssh_import_glob:-}" - local total=0 f base c - - shopt -s nullglob - if [[ -n "$g" ]]; then - for pat in $g; do cand+=($pat); done - else - cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) - cand+=(/root/.ssh/*.pub) - cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) - fi - shopt -u nullglob - - for f in "${cand[@]}"; do - [[ -f "$f" && -r "$f" ]] || continue - base="$(basename -- "$f")" - case "$base" in - known_hosts | known_hosts.* | config) continue ;; - id_*) [[ "$f" != *.pub ]] && continue ;; - esac - - # CRLF safe check for host keys - c=$(tr -d '\r' <"$f" | awk ' - /^[[:space:]]*#/ {next} - /^[[:space:]]*$/ {next} - {print} - ' | grep -E -c '"$re"' || true) - - if ((c > 0)); then - files+=("$f") - total=$((total + c)) - fi - done - - # Fallback to /root/.ssh/authorized_keys - if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then - if grep -E -q "$re" /root/.ssh/authorized_keys; then - files+=(/root/.ssh/authorized_keys) - total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0))) - fi - fi - - FOUND_HOST_KEY_COUNT="$total" - ( - IFS=: - echo "${files[*]}" - ) -} - -# ============================================================================== -# SECTION 4: STORAGE & RESOURCE MANAGEMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# _write_storage_to_vars() -# -# - Writes storage selection to vars file -# - Removes old entries (commented and uncommented) to avoid duplicates -# - Arguments: vars_file, key (var_container_storage/var_template_storage), value -# ------------------------------------------------------------------------------ -_write_storage_to_vars() { - # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value - local vf="$1" key="$2" val="$3" - # remove uncommented and commented versions to avoid duplicates - sed -i "/^[#[:space:]]*${key}=/d" "$vf" - echo "${key}=${val}" >>"$vf" -} - -choose_and_set_storage_for_file() { - # $1 = vars_file, $2 = class ('container'|'template') - local vf="$1" class="$2" key="" current="" - case "$class" in - container) key="var_container_storage" ;; - template) key="var_template_storage" ;; - *) - msg_error "Unknown storage class: $class" - return 1 - ;; - esac - - current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf") - - # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4). - local content="rootdir" - [[ "$class" == "template" ]] && content="vztmpl" - local count - count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l) - - if [[ "$count" -eq 1 ]]; then - STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') - STORAGE_INFO="" - else - # If the current value is preselectable, we could show it, but per your requirement we always offer selection - select_storage "$class" || return 1 - fi - - _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT" - - # Keep environment in sync for later steps (e.g. app-default save) - if [[ "$class" == "container" ]]; then - export var_container_storage="$STORAGE_RESULT" - export CONTAINER_STORAGE="$STORAGE_RESULT" - else - export var_template_storage="$STORAGE_RESULT" - export TEMPLATE_STORAGE="$STORAGE_RESULT" - fi - - # Silent operation - no output message -} - -# ============================================================================== -# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# base_settings() -# -# - Defines all base/default variables for container creation -# - Reads from environment variables (var_*) -# - Provides fallback defaults for OS type/version -# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK) -# - Sets up container type, resources, network, SSH, features, and tags -# ------------------------------------------------------------------------------ -base_settings() { - # Default Settings - CT_TYPE=${var_unprivileged:-"1"} - - # Resource allocation: App defaults take precedence if HIGHER - # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values - local final_disk="${var_disk:-4}" - local final_cpu="${var_cpu:-1}" - local final_ram="${var_ram:-1024}" - - # If app declared higher values, use those instead - if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then - final_disk="${APP_DEFAULT_DISK}" - fi - fi - - if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then - final_cpu="${APP_DEFAULT_CPU}" - fi - fi - - if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then - final_ram="${APP_DEFAULT_RAM}" - fi - fi - - DISK_SIZE="${final_disk}" - CORE_COUNT="${final_cpu}" - RAM_SIZE="${final_ram}" - VERBOSE=${var_verbose:-"${1:-no}"} - PW=${var_pw:-""} - CT_ID=${var_ctid:-$NEXTID} - HN=${var_hostname:-$NSAPP} - BRG=${var_brg:-"vmbr0"} - NET=${var_net:-"dhcp"} - IPV6_METHOD=${var_ipv6_method:-"none"} - IPV6_STATIC=${var_ipv6_static:-""} - GATE=${var_gateway:-""} - APT_CACHER=${var_apt_cacher:-""} - APT_CACHER_IP=${var_apt_cacher_ip:-""} - - # Runtime check: Verify APT cacher is reachable if configured - if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then - if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then - msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142" - msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation" - APT_CACHER="" - APT_CACHER_IP="" - else - msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142" - fi - fi - - MTU=${var_mtu:-""} - SD=${var_storage:-""} - NS=${var_ns:-""} - MAC=${var_mac:-""} - VLAN=${var_vlan:-""} - SSH=${var_ssh:-"no"} - SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} - UDHCPC_FIX=${var_udhcpc_fix:-""} - TAGS="community-script,${var_tags:-}" - ENABLE_FUSE=${var_fuse:-"${1:-no}"} - ENABLE_TUN=${var_tun:-"${1:-no}"} - - # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts - if [ -z "$var_os" ]; then - var_os="debian" - fi - if [ -z "$var_version" ]; then - var_version="12" - fi -} - -# ------------------------------------------------------------------------------ -# default_var_settings -# -# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing) -# - Loads var_* values from default.vars (safe parser, no source/eval) -# - Precedence: ENV var_* > default.vars > built-in defaults -# - Maps var_verbose → VERBOSE -# - Calls base_settings "$VERBOSE" and echo_default -# ------------------------------------------------------------------------------ -default_var_settings() { - # Allowed var_* keys (alphabetically sorted) - # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) - local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse - var_gateway var_hostname var_ipv6_method var_mac var_mtu - var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) - - # Snapshot: environment variables (highest precedence) - declare -A _HARD_ENV=() - local _k - for _k in "${VAR_WHITELIST[@]}"; do - if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi - done - - # Find default.vars location - local _find_default_vars - _find_default_vars() { - local f - for f in \ - /usr/local/community-scripts/default.vars \ - "$HOME/.config/community-scripts/default.vars" \ - "./default.vars"; do - [ -f "$f" ] && { - echo "$f" - return 0 - } - done - return 1 - } - # Allow override of storages via env (for non-interactive use cases) - [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage" - [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage" - - # Create once, with storages already selected, no var_ctid/var_hostname lines - local _ensure_default_vars - _ensure_default_vars() { - _find_default_vars >/dev/null 2>&1 && return 0 - - local canonical="/usr/local/community-scripts/default.vars" - # Silent creation - no msg_info output - mkdir -p /usr/local/community-scripts - - # Pick storages before writing the file (always ask unless only one) - # Create a minimal temp file to write into - : >"$canonical" - - # Base content (no var_ctid / var_hostname here) - cat >"$canonical" <<'EOF' -# Community-Scripts defaults (var_* only). Lines starting with # are comments. -# Precedence: ENV var_* > default.vars > built-ins. -# Keep keys alphabetically sorted. - -# Container type -var_unprivileged=1 - -# Resources -var_cpu=1 -var_disk=4 -var_ram=1024 - -# Network -var_brg=vmbr0 -var_net=dhcp -var_ipv6_method=none -# var_gateway= -# var_vlan= -# var_mtu= -# var_mac= -# var_ns= - -# SSH -var_ssh=no -# var_ssh_authorized_key= - -# APT cacher (optional - with example) -# var_apt_cacher=yes -# var_apt_cacher_ip=192.168.1.10 - -# Features/Tags/verbosity -var_fuse=no -var_tun=no -var_tags=community-script -var_verbose=no - -# Security (root PW) – empty => autologin -# var_pw= -EOF - - # Now choose storages (always prompt unless just one exists) - choose_and_set_storage_for_file "$canonical" template - choose_and_set_storage_for_file "$canonical" container - - chmod 0644 "$canonical" - # Silent creation - no output message - } - - # Whitelist check - local _is_whitelisted_key - _is_whitelisted_key() { - local k="$1" - local w - for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done - return 1 - } - - # 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 - # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) - declare -ag VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse - var_gateway var_hostname var_ipv6_method var_mac var_mtu - var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) -fi - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .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")" - # var_ipv6_static removed - static IPs are unique, can't be default - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_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_custom "ℹ️" "${BL}" "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_custom "🚫" "${YW}" "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - # Silent operation - no output message -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ============================================================================== -# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# advanced_settings() -# -# - Interactive whiptail menu for comprehensive container configuration -# - Allows user to customize: -# * Container type (privileged/unprivileged) -# * Root password -# * Container ID (CTID) -# * Hostname -# * Resources (disk size, CPU cores, RAM) -# * Network (IPv4/IPv6, gateway, DNS, MAC, VLAN, MTU) -# * SSH settings and key injection -# * Advanced features (FUSE, TUN, keyctl) -# * Tags for organization -# * Verbose/debug mode -# - Loops until user confirms or cancels -# - Validates all input and shows current selections -# ------------------------------------------------------------------------------ -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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" - advanced_settings - fi -} - -# ============================================================================== -# SECTION 7: USER INTERFACE & DIAGNOSTICS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# diagnostics_check() -# -# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics -# - Asks user whether to send anonymous diagnostic data -# - Saves DIAGNOSTICS=yes/no in the config file -# - Creates file if missing with default DIAGNOSTICS=yes -# - Reads current diagnostics setting from file -# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out -# ------------------------------------------------------------------------------ -diagnostics_check() { - if ! [ -d "/usr/local/community-scripts" ]; then - mkdir -p /usr/local/community-scripts - fi - - if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "[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 -} - -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 -} - -# ------------------------------------------------------------------------------ -# echo_default() -# -# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) -# - Uses icons and formatting for readability -# - Convert CT_TYPE to description -# ------------------------------------------------------------------------------ -echo_default() { - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# install_script() -# -# - Main entrypoint for installation mode -# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) -# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) -# - Applies chosen settings and triggers container build -# ------------------------------------------------------------------------------ -install_script() { - pve_check - shell_check - root_check - arch_check - ssh_check - maxkeys_check - diagnostics_check - - if systemctl is-active -q ping-instances.service; then - systemctl -q stop ping-instances.service - fi - - NEXTID=$(pvesh get /cluster/nextid) - timezone=$(cat /etc/timezone) - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "My Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - if [ -z "$CHOICE" ]; then - - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - ;; - 2 | advanced | ADVANCED) - header_info - - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - ;; - 3 | mydefaults | MYDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - defaults_target="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - "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 -} - -# ------------------------------------------------------------------------------ -# 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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 -} - -# ============================================================================== -# SECTION 8: CONTAINER CREATION & DEPLOYMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# build_container() -# -# - Main function for creating and configuring LXC container -# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6) -# - Creates container via pct create with all specified settings -# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough -# - Starts container and waits for network connectivity -# - Installs base packages (curl, sudo, etc.) -# - Injects SSH keys if configured -# - Executes -install.sh inside container -# - Posts installation telemetry to API if diagnostics enabled -# ------------------------------------------------------------------------------ -build_container() { - # if [ "$VERBOSE" == "yes" ]; then set -x; fi - - NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" - - # MAC - if [[ -n "$MAC" ]]; then - case "$MAC" in - ,hwaddr=*) NET_STRING+="$MAC" ;; - *) NET_STRING+=",hwaddr=$MAC" ;; - esac - fi - - # IP (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 SESSION_ID="$SESSION_ID" - export CACHER="$APT_CACHER" - export CACHER_IP="$APT_CACHER_IP" - export tz="$timezone" - export APPLICATION="$APP" - export app="$NSAPP" - export PASSWORD="$PW" - export VERBOSE="$VERBOSE" - export SSH_ROOT="${SSH}" - export SSH_AUTHORIZED_KEY - export CTID="$CT_ID" - export CTTYPE="$CT_TYPE" - export ENABLE_FUSE="$ENABLE_FUSE" - export ENABLE_TUN="$ENABLE_TUN" - export PCT_OSTYPE="$var_os" - export PCT_OSVERSION="$var_version" - export PCT_DISK_SIZE="$DISK_SIZE" - export PCT_OPTIONS=" - -features $FEATURES - -hostname $HN - -tags $TAGS - $SD - $NS - $NET_STRING - -onboot 1 - -cores $CORE_COUNT - -memory $RAM_SIZE - -unprivileged $CT_TYPE - $PW -" - export TEMPLATE_STORAGE="${var_template_storage:-}" - export CONTAINER_STORAGE="${var_container_storage:-}" - create_lxc_container || exit $? - - LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" - - # ============================================================================ - # GPU/USB PASSTHROUGH CONFIGURATION - # ============================================================================ - - # List of applications that benefit from GPU acceleration - GPU_APPS=( - "immich" "channels" "emby" "ersatztv" "frigate" - "jellyfin" "plex" "scrypted" "tdarr" "unmanic" - "ollama" "fileflows" "open-webui" "tunarr" "debian" - "handbrake" "sunshine" "moonlight" "kodi" "stremio" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_custom "🎮" "${BL}" "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_custom "🎮" "${RD}" "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" - - # Simple passthrough - just bind /dev/nvidia* devices if they exist - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" - else - msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found" - msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)" - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_custom "⚙️" "${GN}" "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # Add lxc.mount.entry for each device - for dev in "${devices[@]}"; do - echo "lxc.mount.entry: $dev $dev none bind,optional,create=file" >>"$LXC_CONFIG" - - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - also add cgroup allows - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "No NVIDIA devices available for passthrough" - return 0 - fi - - # Add lxc.mount.entry for each NVIDIA device - for dev in "${NVIDIA_DEVICES[@]}"; do - echo "lxc.mount.entry: $dev $dev none bind,optional,create=file" >>"$LXC_CONFIG" - - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - also add cgroup allows - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # TUN device passthrough - if [ "$ENABLE_TUN" == "yes" ]; then - cat <>"$LXC_CONFIG" -lxc.cgroup2.devices.allow: c 10:200 rwm -lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file -EOF - fi - - # Coral TPU passthrough - if [[ -e /dev/apex_0 ]]; then - msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - 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 - - # Simple connectivity check - just verify IP is assigned - msg_ok "Network configured (IP: $ip_in_lxc)" - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </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" - - # 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 - local exit_code=$? - # Try to copy installation log from container before exiting - if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then - pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-${SESSION_ID}.log" 2>/dev/null || true - fi - exit $exit_code - 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_custom "ℹ️" "${BL}" "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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - # Silent operation to avoid spinner conflicts - msg_custom "🔧" "${BL}" "Detecting and setting correct GPU group IDs" - - # Ermittle die tatsächlichen GIDs aus dem Container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Fallbacks wenn Gruppen nicht existieren - if [[ -z "$video_gid" ]]; then - # Versuche die video Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback - fi - - if [[ -z "$render_gid" ]]; then - # Versuche die render Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback - fi - - msg_custom "ℹ️" "${DGN}" "Container GIDs detected - video:${video_gid}, render:${render_gid}" - - # Prüfe ob die GIDs von den Defaults abweichen - local need_update=0 - if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then - need_update=1 - fi - - if [[ $need_update -eq 1 ]]; then - msg_custom "🔄" "${YW}" "Updating device GIDs in container config" - - # Stoppe Container für Config-Update - pct stop "$CTID" >/dev/null 2>&1 - - # Update die dev Einträge mit korrekten GIDs - # Backup der Config - cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" - - # Parse und update jeden dev Eintrag - while IFS= read -r line; do - if [[ "$line" =~ ^dev[0-9]+: ]]; then - # Extract device path - local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') - local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') - - if [[ "$device_path" =~ renderD ]]; then - # RenderD device - use render GID - echo "${dev_num}: ${device_path},gid=${render_gid}" - elif [[ "$device_path" =~ card ]]; then - # Card device - use video GID - echo "${dev_num}: ${device_path},gid=${video_gid}" - else - # Keep original line - echo "$line" - fi - else - # Keep non-dev lines - echo "$line" - fi - done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" - - mv "${LXC_CONFIG}.new" "$LXC_CONFIG" - - # Starte Container wieder - pct start "$CTID" >/dev/null 2>&1 - sleep 3 - - msg_ok "Device GIDs updated successfully" - else - msg_ok "Device GIDs are already correct" - fi - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -check_storage_support() { - local CONTENT="$1" VALID=0 - while IFS= read -r line; do - local STORAGE_NAME - STORAGE_NAME=$(awk '{print $1}' <<<"$line") - [[ -n "$STORAGE_NAME" ]] && VALID=1 - done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') - [[ $VALID -eq 1 ]] -} - -select_storage() { - local CLASS=$1 CONTENT CONTENT_LABEL - case $CLASS in - container) - CONTENT='rootdir' - CONTENT_LABEL='Container' - ;; - template) - CONTENT='vztmpl' - CONTENT_LABEL='Container template' - ;; - iso) - CONTENT='iso' - CONTENT_LABEL='ISO image' - ;; - images) - CONTENT='images' - CONTENT_LABEL='VM Disk image' - ;; - backup) - CONTENT='backup' - CONTENT_LABEL='Backup' - ;; - snippets) - CONTENT='snippets' - CONTENT_LABEL='Snippets' - ;; - *) - msg_error "Invalid storage class '$CLASS'" - return 1 - ;; - esac - - declare -A STORAGE_MAP - local -a MENU=() - local COL_WIDTH=0 - - while read -r TAG TYPE _ TOTAL USED FREE _; do - [[ -n "$TAG" && -n "$TYPE" ]] || continue - local DISPLAY="${TAG} (${TYPE})" - local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") - local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") - local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" - STORAGE_MAP["$DISPLAY"]="$TAG" - MENU+=("$DISPLAY" "$INFO" "OFF") - ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') - - if [[ ${#MENU[@]} -eq 0 ]]; then - msg_error "No storage found for content type '$CONTENT'." - return 2 - fi - - if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then - STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" - STORAGE_INFO="${MENU[1]}" - return 0 - fi - - local WIDTH=$((COL_WIDTH + 42)) - while true; do - local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "[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 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - msg_ok "Template search completed" - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_custom "ℹ️" "${YW}" "Please check:" - msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)" - msg_custom " •" "${YW}" "Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE" - fi - fi - - if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then - [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" - for attempt in {1..3}; do - msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE" - if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then - msg_ok "Template download successful." - break - fi - if [[ $attempt -eq 3 ]]; then - msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" - exit 222 - fi - sleep $((attempt * 5)) - done - fi - - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then - msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download." - exit 223 - fi - - # ------------------------------------------------------------------------------ - # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins) - # ------------------------------------------------------------------------------ - if [[ "$PCT_OSTYPE" == "debian" ]]; then - OSVER="$(parse_template_osver "$TEMPLATE")" - if [[ -n "$OSVER" ]]; then - # Proactive, 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - 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." -} - -# ============================================================================== -# SECTION 9: POST-INSTALLATION & FINALIZATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# description() -# -# - Sets container description with formatted HTML content -# - Includes: -# * Community-Scripts logo -# * Application name -# * Links to GitHub, Discussions, Issues -# * Ko-fi donation badge -# - Restarts ping-instances.service if present (monitoring) -# - Posts final "done" status to API telemetry -# ------------------------------------------------------------------------------ -description() { - IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - - # Generate LXC Description - DESCRIPTION=$( - cat < - - 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" -} - -# ============================================================================== -# SECTION 10: ERROR HANDLING & EXIT TRAPS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# api_exit_script() -# -# - Exit trap handler for reporting to API telemetry -# - Captures exit code and reports to API using centralized error descriptions -# - Uses explain_exit_code() from error_handler.func for consistent error messages -# - Posts failure status with exit code to API (error description added automatically) -# - Only executes on non-zero exit codes -# ------------------------------------------------------------------------------ -api_exit_script() { - exit_code=$? - if [ $exit_code -ne 0 ]; then - post_update_to_api "failed" "$exit_code" - fi -} - -if command -v pveversion >/dev/null 2>&1; then - trap 'api_exit_script' EXIT -fi -trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR -trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT -trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM diff --git a/bak/core_bak/build refactor_menu.func b/bak/core_bak/build refactor_menu.func deleted file mode 100644 index 0c37261e2..000000000 --- a/bak/core_bak/build refactor_menu.func +++ /dev/null @@ -1,3768 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE -# Revision: 1 - -# ============================================================================== -# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION -# ============================================================================== -# -# This file provides the main build functions for creating and configuring -# LXC containers in Proxmox VE. It handles: -# -# - Variable initialization and defaults -# - Container creation and resource allocation -# - Storage selection and management -# - Advanced configuration and customization -# - User interaction menus and prompts -# -# Usage: -# - Sourced automatically by CT creation scripts -# - Requires core.func and error_handler.func to be loaded first -# -# ============================================================================== - -# ============================================================================== -# SECTION 1: INITIALIZATION & CORE VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# variables() -# -# - Initializes core variables for container creation -# - Normalizes application name (NSAPP = lowercase, no spaces) -# - Builds installer filename (var_install) -# - Defines regex patterns for validation -# - Fetches Proxmox hostname and version -# - Generates unique session ID for tracking and logging -# - Captures app-declared resource defaults (CPU, RAM, Disk) -# ------------------------------------------------------------------------------ -variables() { - NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. - var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. - INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern. - PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase - DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. - METHOD="default" # sets the METHOD variable to "default", used for the API call. - RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. - SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files - BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log - CTTYPE="${CTTYPE:-${CT_TYPE:-1}}" - - # Parse dev_mode early - parse_dev_mode - - # Setup persistent log directory if logs mode active - if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then - mkdir -p /var/log/community-scripts - BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log" - fi - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) - - # Capture app-declared defaults (for precedence logic) - # These values are set by the app script BEFORE default.vars is loaded - # If app declares higher values than default.vars, app values take precedence - if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_CPU="${var_cpu}" - fi - if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_RAM="${var_ram}" - fi - if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_DISK="${var_disk}" - fi -} - -# ----------------------------------------------------------------------------- -# 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 - -# ============================================================================== -# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# maxkeys_check() -# -# - Reads kernel keyring limits (maxkeys, maxbytes) -# - Checks current usage for LXC user (UID 100000) -# - Warns if usage is close to limits and suggests sysctl tuning -# - Exits if thresholds are exceeded -# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html -# ------------------------------------------------------------------------------ - -maxkeys_check() { - # Read kernel parameters - per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) - per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) - - # Exit if kernel parameters are unavailable - if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then - echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}" - exit 1 - fi - - # Fetch key usage for user ID 100000 (typical for containers) - used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) - used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) - - # Calculate thresholds and suggested new limits - threshold_keys=$((per_user_maxkeys - 100)) - threshold_bytes=$((per_user_maxbytes - 1000)) - new_limit_keys=$((per_user_maxkeys * 2)) - new_limit_bytes=$((per_user_maxbytes * 2)) - - # Check if key or byte usage is near limits - failure=0 - if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then - echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}" - echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." - failure=1 - fi - if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then - echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}" - echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." - failure=1 - fi - - # Provide next steps if issues are detected - if [[ "$failure" -eq 1 ]]; then - echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}" - exit 1 - fi - - # Silent success - only show errors if they exist -} - -# ============================================================================== -# SECTION 3: CONTAINER SETUP UTILITIES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# get_current_ip() -# -# - Returns current container IP depending on OS type -# - Debian/Ubuntu: uses `hostname -I` -# - Alpine: parses eth0 via `ip -4 addr` -# - Returns "Unknown" if OS type cannot be determined -# ------------------------------------------------------------------------------ -get_current_ip() { - if [ -f /etc/os-release ]; then - # Check for Debian/Ubuntu (uses hostname -I) - if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then - CURRENT_IP=$(hostname -I | awk '{print $1}') - # Check for Alpine (uses ip command) - elif grep -q 'ID=alpine' /etc/os-release; then - CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) - else - CURRENT_IP="Unknown" - fi - fi - echo "$CURRENT_IP" -} - -# ------------------------------------------------------------------------------ -# update_motd_ip() -# -# - Updates /etc/motd with current container IP -# - Removes old IP entries to avoid duplicates -# ------------------------------------------------------------------------------ -update_motd_ip() { - MOTD_FILE="/etc/motd" - - if [ -f "$MOTD_FILE" ]; then - # Remove existing IP Address lines to prevent duplication - sed -i '/IP Address:/d' "$MOTD_FILE" - - IP=$(get_current_ip) - # Add the new IP address - echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" - fi -} - -# ------------------------------------------------------------------------------ -# install_ssh_keys_into_ct() -# -# - Installs SSH keys into container root account if SSH is enabled -# - Uses pct push or direct input to authorized_keys -# - Falls back to warning if no keys provided -# ------------------------------------------------------------------------------ -install_ssh_keys_into_ct() { - [[ "$SSH" != "yes" ]] && return 0 - - if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then - msg_info "Installing selected SSH keys into CT ${CTID}" - pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { - msg_error "prepare /root/.ssh failed" - return 1 - } - pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 || - pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || { - msg_error "write authorized_keys failed" - return 1 - } - pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true - msg_ok "Installed SSH keys into CT ${CTID}" - return 0 - fi - - # Fallback: nichts ausgewählt - msg_warn "No SSH keys to install (skipping)." - return 0 -} - -# ------------------------------------------------------------------------------ -# find_host_ssh_keys() -# -# - Scans system for available SSH keys -# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys) -# - Returns list of files containing valid SSH public keys -# - Sets FOUND_HOST_KEY_COUNT to number of keys found -# ------------------------------------------------------------------------------ -find_host_ssh_keys() { - local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))' - local -a files=() cand=() - local g="${var_ssh_import_glob:-}" - local total=0 f base c - - shopt -s nullglob - if [[ -n "$g" ]]; then - for pat in $g; do cand+=($pat); done - else - cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) - cand+=(/root/.ssh/*.pub) - cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) - fi - shopt -u nullglob - - for f in "${cand[@]}"; do - [[ -f "$f" && -r "$f" ]] || continue - base="$(basename -- "$f")" - case "$base" in - known_hosts | known_hosts.* | config) continue ;; - id_*) [[ "$f" != *.pub ]] && continue ;; - esac - - # CRLF safe check for host keys - c=$(tr -d '\r' <"$f" | awk ' - /^[[:space:]]*#/ {next} - /^[[:space:]]*$/ {next} - {print} - ' | grep -E -c '"$re"' || true) - - if ((c > 0)); then - files+=("$f") - total=$((total + c)) - fi - done - - # Fallback to /root/.ssh/authorized_keys - if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then - if grep -E -q "$re" /root/.ssh/authorized_keys; then - files+=(/root/.ssh/authorized_keys) - total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0))) - fi - fi - - FOUND_HOST_KEY_COUNT="$total" - ( - IFS=: - echo "${files[*]}" - ) -} - -# ============================================================================== -# SECTION 4: STORAGE & RESOURCE MANAGEMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# _write_storage_to_vars() -# -# - Writes storage selection to vars file -# - Removes old entries (commented and uncommented) to avoid duplicates -# - Arguments: vars_file, key (var_container_storage/var_template_storage), value -# ------------------------------------------------------------------------------ -_write_storage_to_vars() { - # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value - local vf="$1" key="$2" val="$3" - # remove uncommented and commented versions to avoid duplicates - sed -i "/^[#[:space:]]*${key}=/d" "$vf" - echo "${key}=${val}" >>"$vf" -} - -choose_and_set_storage_for_file() { - # $1 = vars_file, $2 = class ('container'|'template') - local vf="$1" class="$2" key="" current="" - case "$class" in - container) key="var_container_storage" ;; - template) key="var_template_storage" ;; - *) - msg_error "Unknown storage class: $class" - return 1 - ;; - esac - - current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf") - - # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4). - local content="rootdir" - [[ "$class" == "template" ]] && content="vztmpl" - local count - count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l) - - if [[ "$count" -eq 1 ]]; then - STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') - STORAGE_INFO="" - else - # If the current value is preselectable, we could show it, but per your requirement we always offer selection - select_storage "$class" || return 1 - fi - - _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT" - - # Keep environment in sync for later steps (e.g. app-default save) - if [[ "$class" == "container" ]]; then - export var_container_storage="$STORAGE_RESULT" - export CONTAINER_STORAGE="$STORAGE_RESULT" - else - export var_template_storage="$STORAGE_RESULT" - export TEMPLATE_STORAGE="$STORAGE_RESULT" - fi - - # Silent operation - no output message -} - -# ============================================================================== -# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# base_settings() -# -# - Defines all base/default variables for container creation -# - Reads from environment variables (var_*) -# - Provides fallback defaults for OS type/version -# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK) -# - Sets up container type, resources, network, SSH, features, and tags -# ------------------------------------------------------------------------------ -base_settings() { - # Default Settings - CT_TYPE=${var_unprivileged:-"1"} - - # Resource allocation: App defaults take precedence if HIGHER - # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values - local final_disk="${var_disk:-4}" - local final_cpu="${var_cpu:-1}" - local final_ram="${var_ram:-1024}" - - # If app declared higher values, use those instead - if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then - final_disk="${APP_DEFAULT_DISK}" - fi - fi - - if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then - final_cpu="${APP_DEFAULT_CPU}" - fi - fi - - if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then - final_ram="${APP_DEFAULT_RAM}" - fi - fi - - DISK_SIZE="${final_disk}" - CORE_COUNT="${final_cpu}" - RAM_SIZE="${final_ram}" - VERBOSE=${var_verbose:-"${1:-no}"} - PW=${var_pw:-""} - CT_ID=${var_ctid:-$NEXTID} - HN=${var_hostname:-$NSAPP} - BRG=${var_brg:-"vmbr0"} - NET=${var_net:-"dhcp"} - IPV6_METHOD=${var_ipv6_method:-"none"} - IPV6_STATIC=${var_ipv6_static:-""} - GATE=${var_gateway:-""} - APT_CACHER=${var_apt_cacher:-""} - APT_CACHER_IP=${var_apt_cacher_ip:-""} - - # Runtime check: Verify APT cacher is reachable if configured - if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then - if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then - msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142" - msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation" - APT_CACHER="" - APT_CACHER_IP="" - else - msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142" - fi - fi - - MTU=${var_mtu:-""} - SD=${var_storage:-""} - NS=${var_ns:-""} - MAC=${var_mac:-""} - VLAN=${var_vlan:-""} - SSH=${var_ssh:-"no"} - SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} - UDHCPC_FIX=${var_udhcpc_fix:-""} - TAGS="community-script,${var_tags:-}" - ENABLE_FUSE=${var_fuse:-"${1:-no}"} - ENABLE_TUN=${var_tun:-"${1:-no}"} - - # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts - if [ -z "$var_os" ]; then - var_os="debian" - fi - if [ -z "$var_version" ]; then - var_version="12" - fi -} - -# ------------------------------------------------------------------------------ -# default_var_settings -# -# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing) -# - Loads var_* values from default.vars (safe parser, no source/eval) -# - Precedence: ENV var_* > default.vars > built-in defaults -# - Maps var_verbose → VERBOSE -# - Calls base_settings "$VERBOSE" and echo_default -# ------------------------------------------------------------------------------ -default_var_settings() { - # Allowed var_* keys (alphabetically sorted) - # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) - local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl - var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu - var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) - - # Snapshot: environment variables (highest precedence) - declare -A _HARD_ENV=() - local _k - for _k in "${VAR_WHITELIST[@]}"; do - if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi - done - - # Find default.vars location - local _find_default_vars - _find_default_vars() { - local f - for f in \ - /usr/local/community-scripts/default.vars \ - "$HOME/.config/community-scripts/default.vars" \ - "./default.vars"; do - [ -f "$f" ] && { - echo "$f" - return 0 - } - done - return 1 - } - # Allow override of storages via env (for non-interactive use cases) - [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage" - [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage" - - # Create once, with storages already selected, no var_ctid/var_hostname lines - local _ensure_default_vars - _ensure_default_vars() { - _find_default_vars >/dev/null 2>&1 && return 0 - - local canonical="/usr/local/community-scripts/default.vars" - # Silent creation - no msg_info output - mkdir -p /usr/local/community-scripts - - # Pick storages before writing the file (always ask unless only one) - # Create a minimal temp file to write into - : >"$canonical" - - # Base content (no var_ctid / var_hostname here) - cat >"$canonical" <<'EOF' -# Community-Scripts defaults (var_* only). Lines starting with # are comments. -# Precedence: ENV var_* > default.vars > built-ins. -# Keep keys alphabetically sorted. - -# Container type -var_unprivileged=1 - -# Resources -var_cpu=1 -var_disk=4 -var_ram=1024 - -# Network -var_brg=vmbr0 -var_net=dhcp -var_ipv6_method=none -# var_gateway= -# var_vlan= -# var_mtu= -# var_mac= -# var_ns= - -# SSH -var_ssh=no -# var_ssh_authorized_key= - -# APT cacher (optional - with example) -# var_apt_cacher=yes -# var_apt_cacher_ip=192.168.1.10 - -# Features/Tags/verbosity -var_fuse=no -var_tun=no - -# Advanced Settings (Proxmox-official features) -var_nesting=1 # Allow nesting (required for Docker/LXC in CT) -var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround) -var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental) -var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) -var_protection=no # Prevent accidental deletion of container -var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone) -var_tags=community-script -var_verbose=no - -# Security (root PW) – empty => autologin -# var_pw= -EOF - - # Now choose storages (always prompt unless just one exists) - choose_and_set_storage_for_file "$canonical" template - choose_and_set_storage_for_file "$canonical" container - - chmod 0644 "$canonical" - # Silent creation - no output message - } - - # Whitelist check - local _is_whitelisted_key - _is_whitelisted_key() { - local k="$1" - local w - for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done - return 1 - } - - # 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 User Defaults (default.vars) on node $PVEHOST_NAME${CL}" - echo_default -} - -# ------------------------------------------------------------------------------ -# get_app_defaults_path() -# -# - Returns full path for app-specific defaults file -# - Example: /usr/local/community-scripts/defaults/.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 - # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) - declare -ag VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse - var_gateway var_hostname var_ipv6_method var_mac var_mtu - var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) -fi - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .vars file from current advanced settings -_build_current_app_vars_tmp() { - tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac - - # IPv6 - _ipv6_method="${IPV6_METHOD:-auto}" - _ipv6_static="" - _ipv6_gateway="" - if [ "$_ipv6_method" = "static" ]; then - _ipv6_static="${IPV6_ADDR:-}" - _ipv6_gateway="${IPV6_GATE:-}" - fi - - # MTU/VLAN/MAC - _mtu="" - _vlan="" - _mac="" - case "${MTU:-}" in - ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; - esac - case "${VLAN:-}" in - ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; - esac - case "${MAC:-}" in - ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; - esac - - # DNS / Searchdomain - _ns="" - _searchdomain="" - case "${NS:-}" in - -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; - esac - case "${SD:-}" in - -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; - esac - - # SSH / APT / Features - _ssh="${SSH:-no}" - _ssh_auth="${SSH_AUTHORIZED_KEY:-}" - _apt_cacher="${APT_CACHER:-}" - _apt_cacher_ip="${APT_CACHER_IP:-}" - _fuse="${ENABLE_FUSE:-no}" - _tun="${ENABLE_TUN:-no}" - _nesting="${ENABLE_NESTING:-1}" - _keyctl="${ENABLE_KEYCTL:-0}" - _mknod="${ENABLE_MKNOD:-0}" - _mount_fs="${ALLOW_MOUNT_FS:-}" - _protect="${PROTECT_CT:-no}" - _timezone="${CT_TIMEZONE:-}" - _tags="${TAGS:-}" - _verbose="${VERBOSE:-no}" - - # Type / Resources / Identity - _unpriv="${CT_TYPE:-1}" - _cpu="${CORE_COUNT:-1}" - _ram="${RAM_SIZE:-1024}" - _disk="${DISK_SIZE:-4}" - _hostname="${HN:-$NSAPP}" - - # Storage - _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" - _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" - - { - echo "# App-specific defaults for ${APP} (${NSAPP})" - echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo - - echo "var_unprivileged=$(_sanitize_value "$_unpriv")" - echo "var_cpu=$(_sanitize_value "$_cpu")" - echo "var_ram=$(_sanitize_value "$_ram")" - echo "var_disk=$(_sanitize_value "$_disk")" - - [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" - [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" - [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" - [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" - [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" - [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" - [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" - - [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" - # var_ipv6_static removed - static IPs are unique, can't be default - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")" - [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")" - [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")" - [ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")" - [ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")" - [ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")" - [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" - [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" - - [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" - [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" - - [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" - [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" - } >"$tmpf" - - echo "$tmpf" -} - -# ------------------------------------------------------------------------------ -# maybe_offer_save_app_defaults() -# -# - Called after advanced_settings() -# - Offers to save current values as app defaults if not existing -# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel -# ------------------------------------------------------------------------------ -maybe_offer_save_app_defaults() { - local app_vars_path - app_vars_path="$(get_app_defaults_path)" - - # always build from current settings - local new_tmp diff_tmp - new_tmp="$(_build_current_app_vars_tmp)" - diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" - - # 1) if no file → offer to create - if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[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_custom "ℹ️" "${BL}" "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_custom "🚫" "${YW}" "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - # Silent operation - no output message -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ============================================================================== -# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# advanced_settings() -# -# - Interactive whiptail menu for comprehensive container configuration -# - Allows user to customize: -# * Container type (privileged/unprivileged) -# * Root password -# * Container ID (CTID) -# * Hostname -# * Resources (disk size, CPU cores, RAM) -# * Network (IPv4/IPv6, gateway, DNS, MAC, VLAN, MTU) -# * SSH settings and key injection -# * Advanced features (FUSE, TUN, keyctl) -# * Tags for organization -# * Verbose/debug mode -# - Loops until user confirms or cancels -# - Validates all input and shows current selections -# ------------------------------------------------------------------------------ -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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - - # Advanced Settings - Proxmox Features - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS" --yesno "Configure Advanced Proxmox Features?" 10 58); then - # keyctl: for Docker support - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Enable keyctl()" --yesno "Allow keyctl() system calls?\n\nNeeded for: Docker inside container, systemd-networkd\nDefault: No (not needed for most applications)" 10 58); then - ENABLE_KEYCTL="1" - else - ENABLE_KEYCTL="0" - fi - echo -e "${SEARCH}${BOLD}${DGN}Allow keyctl(): ${BGN}$ENABLE_KEYCTL${CL}" - - # mknod: device node creation - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Enable mknod()" --yesno "Allow device node creation?\n\nNeeded for: Complex device management (experimental, kernel 5.3+)\nDefault: No (rarely needed)" 10 58); then - ENABLE_MKNOD="1" - else - ENABLE_MKNOD="0" - fi - echo -e "${SEARCH}${BOLD}${DGN}Allow mknod(): ${BGN}$ENABLE_MKNOD${CL}" - - # mount: specific filesystems - if MOUNT_FS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allow specific filesystems (e.g., nfs,fuse,ext4)\nLeave blank for defaults" 8 58 "$ALLOW_MOUNT_FS" --title "Mount Filesystems" 3>&1 1>&2 2>&3); then - ALLOW_MOUNT_FS="$MOUNT_FS" - [ -z "$ALLOW_MOUNT_FS" ] && ALLOW_MOUNT_FS="(defaults)" - else - exit_script - fi - echo -e "${SEARCH}${BOLD}${DGN}Mount Filesystems: ${BGN}$ALLOW_MOUNT_FS${CL}" - - # Container protection - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Protection Flag" --yesno "Prevent accidental deletion?\n\nIf enabled, container cannot be deleted or its disk modified\nDefault: No" 10 58); then - PROTECT_CT="yes" - else - PROTECT_CT="no" - fi - echo -e "${SEARCH}${BOLD}${DGN}Container Protection: ${BGN}$PROTECT_CT${CL}" - - # Container timezone - if CT_TIMEZONE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set container timezone (e.g., Europe/Berlin)\nLeave blank to use host timezone" 8 58 "$CT_TIMEZONE" --title "Container Timezone" 3>&1 1>&2 2>&3); then - [ -z "$CT_TIMEZONE" ] && CT_TIMEZONE="(host)" - else - exit_script - fi - echo -e "${SEARCH}${BOLD}${DGN}Container Timezone: ${BGN}$CT_TIMEZONE${CL}" - fi - - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" - advanced_settings - fi -} - -# ============================================================================== -# SECTION 7: USER INTERFACE & DIAGNOSTICS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# diagnostics_check() -# -# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics -# - Asks user whether to send anonymous diagnostic data -# - Saves DIAGNOSTICS=yes/no in the config file -# - Creates file if missing with default DIAGNOSTICS=yes -# - Reads current diagnostics setting from file -# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out -# ------------------------------------------------------------------------------ -diagnostics_check() { - if ! [ -d "/usr/local/community-scripts" ]; then - mkdir -p /usr/local/community-scripts - fi - - if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "[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 -} - -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 -} - -# ------------------------------------------------------------------------------ -# echo_default() -# -# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) -# - Uses icons and formatting for readability -# - Convert CT_TYPE to description -# ------------------------------------------------------------------------------ -echo_default() { - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# install_script() -# -# - Main entrypoint for installation mode -# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) -# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) -# - Applies chosen settings and triggers container build -# ------------------------------------------------------------------------------ -install_script() { - pve_check - shell_check - root_check - arch_check - ssh_check - maxkeys_check - diagnostics_check - - if systemctl is-active -q ping-instances.service; then - systemctl -q stop ping-instances.service - fi - - NEXTID=$(pvesh get /cluster/nextid) - - # Get timezone using timedatectl (Debian 13+ compatible) - # Fallback to /etc/timezone for older systems - if command -v timedatectl >/dev/null 2>&1; then - timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC") - elif [ -f /etc/timezone ]; then - timezone=$(cat /etc/timezone) - else - timezone="UTC" - fi - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "User Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # Main menu loop - allows returning from Settings - while true; do - if [ -z "$CHOICE" ]; then - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - break - ;; - 2 | advanced | ADVANCED) - header_info - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - break - ;; - 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - break - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - break - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - # After settings menu, show main menu again - header_info - CHOICE="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - done - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - ) - if [ -f "$(get_app_defaults_path)" ]; then - settings_items+=("3" "Edit App.vars for ${APP}") - settings_items+=("4" "Exit") - else - settings_items+=("3" "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) nano /usr/local/community-scripts/default.vars ;; - 3) - if [ -f "$(get_app_defaults_path)" ]; then - nano "$(get_app_defaults_path)" - else - # Exit was selected (no app.vars available) - return - fi - ;; - 4) - # Exit - return to main menu - return - ;; - esac - done -} - -# ------------------------------------------------------------------------------ -# check_container_resources() -# -# - Compares host RAM/CPU with required values -# - Warns if under-provisioned and asks user to continue or abort -# ------------------------------------------------------------------------------ -check_container_resources() { - current_ram=$(free -m | awk 'NR==2{print $2}') - current_cpu=$(nproc) - - if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then - echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" - echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" - echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " - 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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 -} - -# ============================================================================== -# SECTION 8: CONTAINER CREATION & DEPLOYMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# build_container() -# -# - Main function for creating and configuring LXC container -# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6) -# - Creates container via pct create with all specified settings -# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough -# - Starts container and waits for network connectivity -# - Installs base packages (curl, sudo, etc.) -# - Injects SSH keys if configured -# - Executes -install.sh inside container -# - Posts installation telemetry to API if diagnostics enabled -# ------------------------------------------------------------------------------ -build_container() { - # if [ "$VERBOSE" == "yes" ]; then set -x; fi - - NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" - - # MAC - if [[ -n "$MAC" ]]; then - case "$MAC" in - ,hwaddr=*) NET_STRING+="$MAC" ;; - *) NET_STRING+=",hwaddr=$MAC" ;; - esac - fi - - # IP (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 - - # Build FEATURES string (simple working version) - 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 - - # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly - # Key insight: Bash cannot export arrays, so we build the options as a string - - 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 - - # Core exports for install.func - export DIAGNOSTICS="$DIAGNOSTICS" - export RANDOM_UUID="$RANDOM_UUID" - export SESSION_ID="$SESSION_ID" - export CACHER="$APT_CACHER" - export CACHER_IP="$APT_CACHER_IP" - export tz="$timezone" - export APPLICATION="$APP" - export app="$NSAPP" - export PASSWORD="$PW" - export VERBOSE="$VERBOSE" - export SSH_ROOT="${SSH}" - export SSH_AUTHORIZED_KEY - export CTID="$CT_ID" - export CTTYPE="$CT_TYPE" - export ENABLE_FUSE="$ENABLE_FUSE" - export ENABLE_TUN="$ENABLE_TUN" - export PCT_OSTYPE="$var_os" - export PCT_OSVERSION="$var_version" - export PCT_DISK_SIZE="$DISK_SIZE" - - # DEV_MODE exports (optional, for debugging) - export BUILD_LOG="$BUILD_LOG" - export INSTALL_LOG="/root/.install-${SESSION_ID}.log" - export dev_mode="${dev_mode:-}" - export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}" - export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}" - export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}" - export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}" - export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}" - export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" - export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" - - # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING=" -features $FEATURES - -hostname $HN - -tags $TAGS" - - # Add storage if specified - if [ -n "$SD" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $SD" - fi - - # Add nameserver if specified - if [ -n "$NS" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $NS" - fi - - # Network configuration - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $NET_STRING - -onboot 1 - -cores $CORE_COUNT - -memory $RAM_SIZE - -unprivileged $CT_TYPE" - - # Protection flag (if var_protection was set) - if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -protection 1" - fi - - # Timezone flag (if var_timezone was set) - if [ -n "${CT_TIMEZONE:-}" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -timezone $CT_TIMEZONE" - fi - - # Password (already formatted) - if [ -n "$PW" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $PW" - fi - - # Export as string (this works, unlike arrays!) - export PCT_OPTIONS="$PCT_OPTIONS_STRING" - export TEMPLATE_STORAGE="${var_template_storage:-}" - export CONTAINER_STORAGE="${var_container_storage:-}" - - # # DEBUG: Show final PCT_OPTIONS being exported - # echo "[DEBUG] PCT_OPTIONS to be exported:" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Calling create_lxc_container..." - - create_lxc_container || exit $? - - LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" - - # ============================================================================ - # GPU/USB PASSTHROUGH CONFIGURATION - # ============================================================================ - - # List of applications that benefit from GPU acceleration - GPU_APPS=( - "immich" "channels" "emby" "ersatztv" "frigate" - "jellyfin" "plex" "scrypted" "tdarr" "unmanic" - "ollama" "fileflows" "open-webui" "tunarr" "debian" - "handbrake" "sunshine" "moonlight" "kodi" "stremio" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_custom "🎮" "${BL}" "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_custom "🎮" "${RD}" "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" - - # Simple passthrough - just bind /dev/nvidia* devices if they exist - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" - else - msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found" - msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)" - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_ok "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # Use pct set to add devices with proper dev0/dev1 format - # GIDs will be detected and set after container starts - local dev_index=0 - for dev in "${devices[@]}"; do - # Add to config using pct set (will be visible in GUI) - echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" - dev_index=$((dev_index + 1)) - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "No NVIDIA devices available for passthrough" - return 0 - fi - - # Use pct set for NVIDIA devices - local dev_index=0 - for dev in "${NVIDIA_DEVICES[@]}"; do - echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" - dev_index=$((dev_index + 1)) - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # TUN device passthrough - if [ "$ENABLE_TUN" == "yes" ]; then - cat <>"$LXC_CONFIG" -lxc.cgroup2.devices.allow: c 10:200 rwm -lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file -EOF - fi - - # Coral TPU passthrough - if [[ -e /dev/apex_0 ]]; then - msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - 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 - - # Simple connectivity check - just verify IP is assigned - msg_ok "Network configured (IP: $ip_in_lxc)" - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories -http://dl-cdn.alpinelinux.org/alpine/latest-stable/main -http://dl-cdn.alpinelinux.org/alpine/latest-stable/community -EOF' - pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" - else - sleep 3 - pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen" - pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ - echo LANG=\$locale_line >/etc/default/locale && \ - locale-gen >/dev/null && \ - export LANG=\$locale_line" - - if [[ -z "${tz:-}" ]]; then - tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") - fi - - if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then - # Set timezone using symlink (Debian 13+ compatible) - # Create /etc/timezone for backwards compatibility with older scripts - pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true" - else - msg_warn "Skipping timezone setup – zone '$tz' not found in container" - fi - - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { - msg_error "apt-get base packages installation failed" - exit 1 - } - fi - - msg_ok "Customized LXC Container" - - # Install SSH keys - install_ssh_keys_into_ct - - # Run application installer - # NOTE: We disable error handling here because: - # 1. Container errors are caught by error_handler INSIDE container - # 2. Container creates flag file with exit code - # 3. We read flag file and handle cleanup manually below - # 4. We DON'T want host error_handler to fire for lxc-attach command itself - - set +Eeuo pipefail # Disable ALL error handling temporarily - trap - ERR # Remove ERR trap completely - - lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)" - local lxc_exit=$? - - set -Eeuo pipefail # Re-enable error handling - trap 'error_handler' ERR # Restore ERR trap - - # Check for error flag file in container (more reliable than lxc-attach exit code) - local install_exit_code=0 - if [[ -n "${SESSION_ID:-}" ]]; then - local error_flag="/root/.install-${SESSION_ID}.failed" - if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then - install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") - pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true - fi - fi - - # Fallback to lxc-attach exit code if no flag file - if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]]; then - install_exit_code=$lxc_exit - fi - - # Installation failed? - if [[ $install_exit_code -ne 0 ]]; then - msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})" - - # Copy both logs from container before potential deletion - local build_log_copied=false - local install_log_copied=false - - if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then - # Copy BUILD_LOG (creation log) if it exists - if [[ -f "${BUILD_LOG}" ]]; then - cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true - fi - - # Copy INSTALL_LOG from container - if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null; then - install_log_copied=true - fi - - # Show available logs - echo "" - [[ "$build_log_copied" == true ]] && echo -e "${GN}✔${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}" - [[ "$install_log_copied" == true ]] && echo -e "${GN}✔${CL} Installation log: ${BL}/tmp/install-lxc-${CTID}-${SESSION_ID}.log${CL}" - fi - - # Dev mode: Keep container or open breakpoint shell - if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then - msg_dev "Keep mode active - container ${CTID} preserved" - return 0 - elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then - msg_dev "Breakpoint mode - opening shell in container ${CTID}" - echo -e "${YW}Type 'exit' to return to host${CL}" - pct enter "$CTID" - echo "" - echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}" - if read -r response && [[ "$response" =~ ^[Yy]$ ]]; then - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - msg_ok "Container ${CTID} removed" - else - msg_dev "Container ${CTID} kept for debugging" - fi - exit $install_exit_code - fi - - # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner) - echo "" - echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" - - if read -t 60 -r response; then - if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then - # Remove container - echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" - elif [[ "$response" =~ ^[Nn]$ ]]; then - echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}" - - # Dev mode: Setup MOTD/SSH for debugging access to broken container - if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then - echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" - if pct exec "$CTID" -- bash -c " - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/install.func) - declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true - " >/dev/null 2>&1; then - local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) - echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}" - fi - fi - fi - else - # Timeout - auto-remove - echo -e "\n${YW}No response - auto-removing container${CL}" - echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" - fi - - exit $install_exit_code - fi -} - -destroy_lxc() { - if [[ -z "$CT_ID" ]]; then - msg_error "No CT_ID found. Nothing to remove." - return 1 - fi - - # 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_custom "ℹ️" "${BL}" "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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Get actual GIDs from container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Create groups if they don't exist - if [[ -z "$video_gid" ]]; then - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1 - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" - fi - - if [[ -z "$render_gid" ]]; then - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1 - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" - fi - - # Stop container to update config - pct stop "$CTID" >/dev/null 2>&1 - sleep 1 - - # Update dev entries with correct GIDs - sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG" - sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG" - - # Restart container - pct start "$CTID" >/dev/null 2>&1 - sleep 2 - - msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})" - - # For privileged containers: also fix permissions inside container - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -check_storage_support() { - local CONTENT="$1" VALID=0 - while IFS= read -r line; do - local STORAGE_NAME - STORAGE_NAME=$(awk '{print $1}' <<<"$line") - [[ -n "$STORAGE_NAME" ]] && VALID=1 - done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') - [[ $VALID -eq 1 ]] -} - -select_storage() { - local CLASS=$1 CONTENT CONTENT_LABEL - case $CLASS in - container) - CONTENT='rootdir' - CONTENT_LABEL='Container' - ;; - template) - CONTENT='vztmpl' - CONTENT_LABEL='Container template' - ;; - iso) - CONTENT='iso' - CONTENT_LABEL='ISO image' - ;; - images) - CONTENT='images' - CONTENT_LABEL='VM Disk image' - ;; - backup) - CONTENT='backup' - CONTENT_LABEL='Backup' - ;; - snippets) - CONTENT='snippets' - CONTENT_LABEL='Snippets' - ;; - *) - msg_error "Invalid storage class '$CLASS'" - return 1 - ;; - esac - - declare -A STORAGE_MAP - local -a MENU=() - local COL_WIDTH=0 - - while read -r TAG TYPE _ TOTAL USED FREE _; do - [[ -n "$TAG" && -n "$TYPE" ]] || continue - local DISPLAY="${TAG} (${TYPE})" - local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") - local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") - local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" - STORAGE_MAP["$DISPLAY"]="$TAG" - MENU+=("$DISPLAY" "$INFO" "OFF") - ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') - - if [[ ${#MENU[@]} -eq 0 ]]; then - msg_error "No storage found for content type '$CONTENT'." - return 2 - fi - - if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then - STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" - STORAGE_INFO="${MENU[1]}" - return 0 - fi - - local WIDTH=$((COL_WIDTH + 42)) - while true; do - local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "[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 $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then - msg_ok "LXC stack upgraded." - if [[ "$do_retry" == "yes" ]]; then - msg_info "Retrying container creation after upgrade" - if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - msg_ok "Container created successfully after upgrade." - return 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - msg_ok "Template search completed" - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_custom "ℹ️" "${YW}" "Please check:" - msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)" - msg_custom " •" "${YW}" "Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE" - fi - fi - - if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then - [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" - for attempt in {1..3}; do - msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE" - if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then - msg_ok "Template download successful." - break - fi - if [[ $attempt -eq 3 ]]; then - msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" - exit 222 - fi - sleep $((attempt * 5)) - done - fi - - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then - msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download." - exit 223 - fi - - # ------------------------------------------------------------------------------ - # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins) - # ------------------------------------------------------------------------------ - if [[ "$PCT_OSTYPE" == "debian" ]]; then - OSVER="$(parse_template_osver "$TEMPLATE")" - if [[ -n "$OSVER" ]]; then - # Proactive, 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 - - # PCT_OPTIONS is now a string (exported from build_container) - # Add rootfs if not already specified - if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then - PCT_OPTIONS="$PCT_OPTIONS - -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" - fi - - # Lock by template file (avoid concurrent downloads/creates) - lockfile="/tmp/template.${TEMPLATE}.lock" - exec 9>"$lockfile" || { - msg_error "Failed to create lock file '$lockfile'." - exit 200 - } - flock -w 60 9 || { - msg_error "Timeout while waiting for template lock." - exit 211 - } - - LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - - # # DEBUG: Show the actual command that will be executed - # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" - # echo "[DEBUG] CTID: $CTID" - # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" - # echo "[DEBUG] PCT_OPTIONS (will be word-split):" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Full command line:" - # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - # echo "[DEBUG] ========================================" - - msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - msg_debug "Logfile: $LOGFILE" - - # First attempt (PCT_OPTIONS is a multi-line string, use it directly) - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then - msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..." - - # Validate template file - if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - msg_warn "Template file too small or missing – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - else - msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." - fi - fi - - # Retry after repair - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - # Fallback to local storage if not already on local - if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - msg_info "Retrying container creation with fallback to local storage..." - LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then - msg_info "Downloading template to local..." - pveam download local "$TEMPLATE" >/dev/null 2>&1 - fi - if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - # Local fallback also failed - check for LXC stack version issue - if grep -qiE 'unsupported .* version' "$LOGFILE"; then - echo - echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." - echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" - set +x - fi - exit 209 - fi - else - msg_ok "Container successfully created using local fallback." - fi - else - # Already on local storage and still failed - check LXC stack version - if grep -qiE 'unsupported .* version' "$LOGFILE"; then - echo - echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." - echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" - set +x - fi - exit 209 - fi - fi - else - msg_ok "Container successfully created after template repair." - fi - fi - - # Verify container exists - pct list | awk '{print $1}' | grep -qx "$CTID" || { - msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE" - exit 215 - } - - # Verify config rootfs - grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { - msg_error "RootFS entry missing in container config. See $LOGFILE" - exit 216 - } - - msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." - - # Report container creation to API - post_to_api -} - -# ============================================================================== -# SECTION 9: POST-INSTALLATION & FINALIZATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# description() -# -# - Sets container description with formatted HTML content -# - Includes: -# * Community-Scripts logo -# * Application name -# * Links to GitHub, Discussions, Issues -# * Ko-fi donation badge -# - Restarts ping-instances.service if present (monitoring) -# - Posts final "done" status to API telemetry -# ------------------------------------------------------------------------------ -description() { - IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - - # Generate LXC Description - DESCRIPTION=$( - cat < - - 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" -} - -# ============================================================================== -# SECTION 10: ERROR HANDLING & EXIT TRAPS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# api_exit_script() -# -# - Exit trap handler for reporting to API telemetry -# - Captures exit code and reports to API using centralized error descriptions -# - Uses explain_exit_code() from error_handler.func for consistent error messages -# - Posts failure status with exit code to API (error description added automatically) -# - Only executes on non-zero exit codes -# ------------------------------------------------------------------------------ -api_exit_script() { - exit_code=$? - if [ $exit_code -ne 0 ]; then - post_update_to_api "failed" "$exit_code" - fi -} - -if command -v pveversion >/dev/null 2>&1; then - trap 'api_exit_script' EXIT -fi -trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR -trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT -trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM diff --git a/bak/core_bak/build.func - advanced-backup-20251127-154005.func b/bak/core_bak/build.func - advanced-backup-20251127-154005.func deleted file mode 100644 index f7a8acd96..000000000 --- a/bak/core_bak/build.func - advanced-backup-20251127-154005.func +++ /dev/null @@ -1,3778 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/branch/main/LICENSE -# Revision: 1 - -# ============================================================================== -# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION -# ============================================================================== -# -# This file provides the main build functions for creating and configuring -# LXC containers in Proxmox VE. It handles: -# -# - Variable initialization and defaults -# - Container creation and resource allocation -# - Storage selection and management -# - Advanced configuration and customization -# - User interaction menus and prompts -# -# Usage: -# - Sourced automatically by CT creation scripts -# - Requires core.func and error_handler.func to be loaded first -# -# ============================================================================== - -# ============================================================================== -# SECTION 1: INITIALIZATION & CORE VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# variables() -# -# - Initializes core variables for container creation -# - Normalizes application name (NSAPP = lowercase, no spaces) -# - Builds installer filename (var_install) -# - Defines regex patterns for validation -# - Fetches Proxmox hostname and version -# - Generates unique session ID for tracking and logging -# - Captures app-declared resource defaults (CPU, RAM, Disk) -# ------------------------------------------------------------------------------ -variables() { - NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. - var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. - INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern. - PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase - DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call. - METHOD="default" # sets the METHOD variable to "default", used for the API call. - RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable. - SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files - BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log - CTTYPE="${CTTYPE:-${CT_TYPE:-1}}" - - # Parse dev_mode early - parse_dev_mode - - # Setup persistent log directory if logs mode active - if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then - mkdir -p /var/log/community-scripts - BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log" - fi - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) - - # Capture app-declared defaults (for precedence logic) - # These values are set by the app script BEFORE default.vars is loaded - # If app declares higher values than default.vars, app values take precedence - if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_CPU="${var_cpu}" - fi - if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_RAM="${var_ram}" - fi - if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then - export APP_DEFAULT_DISK="${var_disk}" - fi -} - -# ----------------------------------------------------------------------------- -# 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 - -# ============================================================================== -# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# maxkeys_check() -# -# - Reads kernel keyring limits (maxkeys, maxbytes) -# - Checks current usage for LXC user (UID 100000) -# - Warns if usage is close to limits and suggests sysctl tuning -# - Exits if thresholds are exceeded -# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html -# ------------------------------------------------------------------------------ - -maxkeys_check() { - # Read kernel parameters - per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) - per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) - - # Exit if kernel parameters are unavailable - if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then - echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}" - exit 1 - fi - - # Fetch key usage for user ID 100000 (typical for containers) - used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) - used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) - - # Calculate thresholds and suggested new limits - threshold_keys=$((per_user_maxkeys - 100)) - threshold_bytes=$((per_user_maxbytes - 1000)) - new_limit_keys=$((per_user_maxkeys * 2)) - new_limit_bytes=$((per_user_maxbytes * 2)) - - # Check if key or byte usage is near limits - failure=0 - if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then - echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}" - echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." - failure=1 - fi - if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then - echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}" - echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." - failure=1 - fi - - # Provide next steps if issues are detected - if [[ "$failure" -eq 1 ]]; then - echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}" - exit 1 - fi - - # Silent success - only show errors if they exist -} - -# ============================================================================== -# SECTION 3: CONTAINER SETUP UTILITIES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# get_current_ip() -# -# - Returns current container IP depending on OS type -# - Debian/Ubuntu: uses `hostname -I` -# - Alpine: parses eth0 via `ip -4 addr` -# - Returns "Unknown" if OS type cannot be determined -# ------------------------------------------------------------------------------ -get_current_ip() { - if [ -f /etc/os-release ]; then - # Check for Debian/Ubuntu (uses hostname -I) - if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then - CURRENT_IP=$(hostname -I | awk '{print $1}') - # Check for Alpine (uses ip command) - elif grep -q 'ID=alpine' /etc/os-release; then - CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) - else - CURRENT_IP="Unknown" - fi - fi - echo "$CURRENT_IP" -} - -# ------------------------------------------------------------------------------ -# update_motd_ip() -# -# - Updates /etc/motd with current container IP -# - Removes old IP entries to avoid duplicates -# ------------------------------------------------------------------------------ -update_motd_ip() { - MOTD_FILE="/etc/motd" - - if [ -f "$MOTD_FILE" ]; then - # Remove existing IP Address lines to prevent duplication - sed -i '/IP Address:/d' "$MOTD_FILE" - - IP=$(get_current_ip) - # Add the new IP address - echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" - fi -} - -# ------------------------------------------------------------------------------ -# install_ssh_keys_into_ct() -# -# - Installs SSH keys into container root account if SSH is enabled -# - Uses pct push or direct input to authorized_keys -# - Falls back to warning if no keys provided -# ------------------------------------------------------------------------------ -install_ssh_keys_into_ct() { - [[ "$SSH" != "yes" ]] && return 0 - - if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then - msg_info "Installing selected SSH keys into CT ${CTID}" - pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { - msg_error "prepare /root/.ssh failed" - return 1 - } - pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 || - pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || { - msg_error "write authorized_keys failed" - return 1 - } - pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true - msg_ok "Installed SSH keys into CT ${CTID}" - return 0 - fi - - # Fallback: nichts ausgewählt - msg_warn "No SSH keys to install (skipping)." - return 0 -} - -# ------------------------------------------------------------------------------ -# find_host_ssh_keys() -# -# - Scans system for available SSH keys -# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys) -# - Returns list of files containing valid SSH public keys -# - Sets FOUND_HOST_KEY_COUNT to number of keys found -# ------------------------------------------------------------------------------ -find_host_ssh_keys() { - local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))' - local -a files=() cand=() - local g="${var_ssh_import_glob:-}" - local total=0 f base c - - shopt -s nullglob - if [[ -n "$g" ]]; then - for pat in $g; do cand+=($pat); done - else - cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2) - cand+=(/root/.ssh/*.pub) - cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*) - fi - shopt -u nullglob - - for f in "${cand[@]}"; do - [[ -f "$f" && -r "$f" ]] || continue - base="$(basename -- "$f")" - case "$base" in - known_hosts | known_hosts.* | config) continue ;; - id_*) [[ "$f" != *.pub ]] && continue ;; - esac - - # CRLF safe check for host keys - c=$(tr -d '\r' <"$f" | awk ' - /^[[:space:]]*#/ {next} - /^[[:space:]]*$/ {next} - {print} - ' | grep -E -c '"$re"' || true) - - if ((c > 0)); then - files+=("$f") - total=$((total + c)) - fi - done - - # Fallback to /root/.ssh/authorized_keys - if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then - if grep -E -q "$re" /root/.ssh/authorized_keys; then - files+=(/root/.ssh/authorized_keys) - total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0))) - fi - fi - - FOUND_HOST_KEY_COUNT="$total" - ( - IFS=: - echo "${files[*]}" - ) -} - -# ============================================================================== -# SECTION 4: STORAGE & RESOURCE MANAGEMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# _write_storage_to_vars() -# -# - Writes storage selection to vars file -# - Removes old entries (commented and uncommented) to avoid duplicates -# - Arguments: vars_file, key (var_container_storage/var_template_storage), value -# ------------------------------------------------------------------------------ -_write_storage_to_vars() { - # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value - local vf="$1" key="$2" val="$3" - # remove uncommented and commented versions to avoid duplicates - sed -i "/^[#[:space:]]*${key}=/d" "$vf" - echo "${key}=${val}" >>"$vf" -} - -choose_and_set_storage_for_file() { - # $1 = vars_file, $2 = class ('container'|'template') - local vf="$1" class="$2" key="" current="" - case "$class" in - container) key="var_container_storage" ;; - template) key="var_template_storage" ;; - *) - msg_error "Unknown storage class: $class" - return 1 - ;; - esac - - current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf") - - # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4). - local content="rootdir" - [[ "$class" == "template" ]] && content="vztmpl" - local count - count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l) - - if [[ "$count" -eq 1 ]]; then - STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') - STORAGE_INFO="" - else - # If the current value is preselectable, we could show it, but per your requirement we always offer selection - select_storage "$class" || return 1 - fi - - _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT" - - # Keep environment in sync for later steps (e.g. app-default save) - if [[ "$class" == "container" ]]; then - export var_container_storage="$STORAGE_RESULT" - export CONTAINER_STORAGE="$STORAGE_RESULT" - else - export var_template_storage="$STORAGE_RESULT" - export TEMPLATE_STORAGE="$STORAGE_RESULT" - fi - - # Silent operation - no output message -} - -# ============================================================================== -# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# base_settings() -# -# - Defines all base/default variables for container creation -# - Reads from environment variables (var_*) -# - Provides fallback defaults for OS type/version -# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK) -# - Sets up container type, resources, network, SSH, features, and tags -# ------------------------------------------------------------------------------ -base_settings() { - # Default Settings - CT_TYPE=${var_unprivileged:-"1"} - - # Resource allocation: App defaults take precedence if HIGHER - # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values - local final_disk="${var_disk:-4}" - local final_cpu="${var_cpu:-1}" - local final_ram="${var_ram:-1024}" - - # If app declared higher values, use those instead - if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then - final_disk="${APP_DEFAULT_DISK}" - fi - fi - - if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then - final_cpu="${APP_DEFAULT_CPU}" - fi - fi - - if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then - if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then - final_ram="${APP_DEFAULT_RAM}" - fi - fi - - DISK_SIZE="${final_disk}" - CORE_COUNT="${final_cpu}" - RAM_SIZE="${final_ram}" - VERBOSE=${var_verbose:-"${1:-no}"} - PW=${var_pw:-""} - CT_ID=${var_ctid:-$NEXTID} - HN=${var_hostname:-$NSAPP} - BRG=${var_brg:-"vmbr0"} - NET=${var_net:-"dhcp"} - IPV6_METHOD=${var_ipv6_method:-"none"} - IPV6_STATIC=${var_ipv6_static:-""} - GATE=${var_gateway:-""} - APT_CACHER=${var_apt_cacher:-""} - APT_CACHER_IP=${var_apt_cacher_ip:-""} - - # Runtime check: Verify APT cacher is reachable if configured - if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then - if ! curl -s --connect-timeout 2 "http://${APT_CACHER_IP}:3142" >/dev/null 2>&1; then - msg_warn "APT Cacher configured but not reachable at ${APT_CACHER_IP}:3142" - msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation" - APT_CACHER="" - APT_CACHER_IP="" - else - msg_ok "APT Cacher verified at ${APT_CACHER_IP}:3142" - fi - fi - - MTU=${var_mtu:-""} - SD=${var_storage:-""} - NS=${var_ns:-""} - MAC=${var_mac:-""} - VLAN=${var_vlan:-""} - SSH=${var_ssh:-"no"} - SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} - UDHCPC_FIX=${var_udhcpc_fix:-""} - TAGS="community-script,${var_tags:-}" - ENABLE_FUSE=${var_fuse:-"${1:-no}"} - ENABLE_TUN=${var_tun:-"${1:-no}"} - - # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts - if [ -z "$var_os" ]; then - var_os="debian" - fi - if [ -z "$var_version" ]; then - var_version="12" - fi -} - -# ------------------------------------------------------------------------------ -# load_vars_file() -# -# - Safe parser for KEY=VALUE lines from vars files -# - Used by default_var_settings and app defaults loading -# - Only loads whitelisted var_* keys -# ------------------------------------------------------------------------------ -load_vars_file() { - local file="$1" - [ -f "$file" ] || return 0 - msg_info "Loading defaults from ${file}" - - # Allowed var_* keys - local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl - var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu - var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) - - # Whitelist check helper - _is_whitelisted() { - local k="$1" w - for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done - return 1 - } - - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [[ -z "$line" || "$line" == \#* ]] && continue - if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then - local var_key="${BASH_REMATCH[1]}" - local var_val="${BASH_REMATCH[2]}" - - [[ "$var_key" != var_* ]] && continue - _is_whitelisted "$var_key" || continue - - # Strip quotes - if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then - var_val="${BASH_REMATCH[1]}" - elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then - var_val="${BASH_REMATCH[1]}" - fi - - # Set only if not already exported - [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" - fi - done <"$file" - msg_ok "Loaded ${file}" -} - -# ------------------------------------------------------------------------------ -# default_var_settings -# -# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing) -# - Loads var_* values from default.vars (safe parser, no source/eval) -# - Precedence: ENV var_* > default.vars > built-in defaults -# - Maps var_verbose → VERBOSE -# - Calls base_settings "$VERBOSE" and echo_default -# ------------------------------------------------------------------------------ -default_var_settings() { - # Allowed var_* keys (alphabetically sorted) - # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) - local VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_keyctl - var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu - var_net var_nesting var_ns var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) - - # Snapshot: environment variables (highest precedence) - declare -A _HARD_ENV=() - local _k - for _k in "${VAR_WHITELIST[@]}"; do - if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi - done - - # Find default.vars location - local _find_default_vars - _find_default_vars() { - local f - for f in \ - /usr/local/community-scripts/default.vars \ - "$HOME/.config/community-scripts/default.vars" \ - "./default.vars"; do - [ -f "$f" ] && { - echo "$f" - return 0 - } - done - return 1 - } - # Allow override of storages via env (for non-interactive use cases) - [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage" - [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage" - - # Create once, with storages already selected, no var_ctid/var_hostname lines - local _ensure_default_vars - _ensure_default_vars() { - _find_default_vars >/dev/null 2>&1 && return 0 - - local canonical="/usr/local/community-scripts/default.vars" - # Silent creation - no msg_info output - mkdir -p /usr/local/community-scripts - - # Pick storages before writing the file (always ask unless only one) - # Create a minimal temp file to write into - : >"$canonical" - - # Base content (no var_ctid / var_hostname here) - cat >"$canonical" <<'EOF' -# Community-Scripts defaults (var_* only). Lines starting with # are comments. -# Precedence: ENV var_* > default.vars > built-ins. -# Keep keys alphabetically sorted. - -# Container type -var_unprivileged=1 - -# Resources -var_cpu=1 -var_disk=4 -var_ram=1024 - -# Network -var_brg=vmbr0 -var_net=dhcp -var_ipv6_method=none -# var_gateway= -# var_vlan= -# var_mtu= -# var_mac= -# var_ns= - -# SSH -var_ssh=no -# var_ssh_authorized_key= - -# APT cacher (optional - with example) -# var_apt_cacher=yes -# var_apt_cacher_ip=192.168.1.10 - -# Features/Tags/verbosity -var_fuse=no -var_tun=no - -# Advanced Settings (Proxmox-official features) -var_nesting=1 # Allow nesting (required for Docker/LXC in CT) -var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround) -var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental) -var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults) -var_protection=no # Prevent accidental deletion of container -var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone) -var_tags=community-script -var_verbose=no - -# Security (root PW) – empty => autologin -# var_pw= -EOF - - # Now choose storages (always prompt unless just one exists) - choose_and_set_storage_for_file "$canonical" template - choose_and_set_storage_for_file "$canonical" container - - chmod 0644 "$canonical" - # Silent creation - no output message - } - - # Whitelist check - local _is_whitelisted_key - _is_whitelisted_key() { - local k="$1" - local w - for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done - return 1 - } - - # 1) Ensure file exists - _ensure_default_vars - - # 2) Load file - local dv - dv="$(_find_default_vars)" || { - msg_error "default.vars not found after ensure step" - return 1 - } - load_vars_file "$dv" - - # 3) Map var_verbose → VERBOSE - if [[ -n "${var_verbose:-}" ]]; then - case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac - else - VERBOSE="no" - fi - - # 4) Apply base settings and show summary - METHOD="mydefaults-global" - base_settings "$VERBOSE" - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}" - echo_default -} - -# ------------------------------------------------------------------------------ -# get_app_defaults_path() -# -# - Returns full path for app-specific defaults file -# - Example: /usr/local/community-scripts/defaults/.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 - # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique) - declare -ag VAR_WHITELIST=( - var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse - var_gateway var_hostname var_ipv6_method var_mac var_mtu - var_net var_ns var_pw var_ram var_tags var_tun var_unprivileged - var_verbose var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage - ) -fi - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .vars file from current advanced settings -_build_current_app_vars_tmp() { - tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac - - # IPv6 - _ipv6_method="${IPV6_METHOD:-auto}" - _ipv6_static="" - _ipv6_gateway="" - if [ "$_ipv6_method" = "static" ]; then - _ipv6_static="${IPV6_ADDR:-}" - _ipv6_gateway="${IPV6_GATE:-}" - fi - - # MTU/VLAN/MAC - _mtu="" - _vlan="" - _mac="" - case "${MTU:-}" in - ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; - esac - case "${VLAN:-}" in - ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; - esac - case "${MAC:-}" in - ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; - esac - - # DNS / Searchdomain - _ns="" - _searchdomain="" - case "${NS:-}" in - -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; - esac - case "${SD:-}" in - -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; - esac - - # SSH / APT / Features - _ssh="${SSH:-no}" - _ssh_auth="${SSH_AUTHORIZED_KEY:-}" - _apt_cacher="${APT_CACHER:-}" - _apt_cacher_ip="${APT_CACHER_IP:-}" - _fuse="${ENABLE_FUSE:-no}" - _tun="${ENABLE_TUN:-no}" - _nesting="${ENABLE_NESTING:-1}" - _keyctl="${ENABLE_KEYCTL:-0}" - _mknod="${ENABLE_MKNOD:-0}" - _mount_fs="${ALLOW_MOUNT_FS:-}" - _protect="${PROTECT_CT:-no}" - _timezone="${CT_TIMEZONE:-}" - _tags="${TAGS:-}" - _verbose="${VERBOSE:-no}" - - # Type / Resources / Identity - _unpriv="${CT_TYPE:-1}" - _cpu="${CORE_COUNT:-1}" - _ram="${RAM_SIZE:-1024}" - _disk="${DISK_SIZE:-4}" - _hostname="${HN:-$NSAPP}" - - # Storage - _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" - _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" - - { - echo "# App-specific defaults for ${APP} (${NSAPP})" - echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo - - echo "var_unprivileged=$(_sanitize_value "$_unpriv")" - echo "var_cpu=$(_sanitize_value "$_cpu")" - echo "var_ram=$(_sanitize_value "$_ram")" - echo "var_disk=$(_sanitize_value "$_disk")" - - [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" - [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" - [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" - [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" - [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" - [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" - [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" - - [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" - # var_ipv6_static removed - static IPs are unique, can't be default - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")" - [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")" - [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")" - [ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")" - [ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")" - [ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")" - [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" - [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" - - [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" - [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" - - [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" - [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" - } >"$tmpf" - - echo "$tmpf" -} - -# ------------------------------------------------------------------------------ -# maybe_offer_save_app_defaults() -# -# - Called after advanced_settings() -# - Offers to save current values as app defaults if not existing -# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel -# ------------------------------------------------------------------------------ -maybe_offer_save_app_defaults() { - local app_vars_path - app_vars_path="$(get_app_defaults_path)" - - # always build from current settings - local new_tmp diff_tmp - new_tmp="$(_build_current_app_vars_tmp)" - diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" - - # 1) if no file → offer to create - if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[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_custom "ℹ️" "${BL}" "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_custom "🚫" "${YW}" "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - # Silent operation - no output message -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ============================================================================== -# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# advanced_settings() -# -# - Interactive whiptail menu for comprehensive container configuration -# - Allows user to customize: -# * Container type (privileged/unprivileged) -# * Root password -# * Container ID (CTID) -# * Hostname -# * Resources (disk size, CPU cores, RAM) -# * Network (IPv4/IPv6, gateway, DNS, MAC, VLAN, MTU) -# * SSH settings and key injection -# * Advanced features (FUSE, TUN, keyctl) -# * Tags for organization -# * Verbose/debug mode -# - Loops until user confirms or cancels -# - Validates all input and shows current selections -# ------------------------------------------------------------------------------ -advanced_settings() { - # Enter alternate screen buffer to prevent flicker between dialogs - tput smcup 2>/dev/null || true - - # Trap to ensure we exit alternate screen on any exit - trap 'tput rmcup 2>/dev/null || true' RETURN - - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - - # Advanced Settings - Proxmox Features - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS" --yesno "Configure Advanced Proxmox Features?" 10 58); then - # keyctl: for Docker support - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Enable keyctl()" --yesno "Allow keyctl() system calls?\n\nNeeded for: Docker inside container, systemd-networkd\nDefault: No (not needed for most applications)" 10 58); then - ENABLE_KEYCTL="1" - else - ENABLE_KEYCTL="0" - fi - echo -e "${SEARCH}${BOLD}${DGN}Allow keyctl(): ${BGN}$ENABLE_KEYCTL${CL}" - - # mknod: device node creation - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Enable mknod()" --yesno "Allow device node creation?\n\nNeeded for: Complex device management (experimental, kernel 5.3+)\nDefault: No (rarely needed)" 10 58); then - ENABLE_MKNOD="1" - else - ENABLE_MKNOD="0" - fi - echo -e "${SEARCH}${BOLD}${DGN}Allow mknod(): ${BGN}$ENABLE_MKNOD${CL}" - - # mount: specific filesystems - if MOUNT_FS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allow specific filesystems (e.g., nfs,fuse,ext4)\nLeave blank for defaults" 8 58 "$ALLOW_MOUNT_FS" --title "Mount Filesystems" 3>&1 1>&2 2>&3); then - ALLOW_MOUNT_FS="$MOUNT_FS" - [ -z "$ALLOW_MOUNT_FS" ] && ALLOW_MOUNT_FS="(defaults)" - else - exit_script - fi - echo -e "${SEARCH}${BOLD}${DGN}Mount Filesystems: ${BGN}$ALLOW_MOUNT_FS${CL}" - - # Container protection - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Protection Flag" --yesno "Prevent accidental deletion?\n\nIf enabled, container cannot be deleted or its disk modified\nDefault: No" 10 58); then - PROTECT_CT="yes" - else - PROTECT_CT="no" - fi - echo -e "${SEARCH}${BOLD}${DGN}Container Protection: ${BGN}$PROTECT_CT${CL}" - - # Container timezone - if CT_TIMEZONE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set container timezone (e.g., Europe/Berlin)\nLeave blank to use host timezone" 8 58 "$CT_TIMEZONE" --title "Container Timezone" 3>&1 1>&2 2>&3); then - [ -z "$CT_TIMEZONE" ] && CT_TIMEZONE="(host)" - else - exit_script - fi - echo -e "${SEARCH}${BOLD}${DGN}Container Timezone: ${BGN}$CT_TIMEZONE${CL}" - fi - - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" - advanced_settings - fi -} - -# ============================================================================== -# SECTION 7: USER INTERFACE & DIAGNOSTICS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# diagnostics_check() -# -# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics -# - Asks user whether to send anonymous diagnostic data -# - Saves DIAGNOSTICS=yes/no in the config file -# - Creates file if missing with default DIAGNOSTICS=yes -# - Reads current diagnostics setting from file -# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out -# ------------------------------------------------------------------------------ -diagnostics_check() { - if ! [ -d "/usr/local/community-scripts" ]; then - mkdir -p /usr/local/community-scripts - fi - - if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then - if (whiptail --backtitle "[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 -} - -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 -} - -# ------------------------------------------------------------------------------ -# echo_default() -# -# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.) -# - Uses icons and formatting for readability -# - Convert CT_TYPE to description -# ------------------------------------------------------------------------------ -echo_default() { - CT_TYPE_DESC="Unprivileged" - if [ "$CT_TYPE" -eq 0 ]; then - CT_TYPE_DESC="Privileged" - fi - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# install_script() -# -# - Main entrypoint for installation mode -# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check) -# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit) -# - Applies chosen settings and triggers container build -# ------------------------------------------------------------------------------ -install_script() { - pve_check - shell_check - root_check - arch_check - ssh_check - maxkeys_check - diagnostics_check - - if systemctl is-active -q ping-instances.service; then - systemctl -q stop ping-instances.service - fi - - NEXTID=$(pvesh get /cluster/nextid) - - # Get timezone using timedatectl (Debian 13+ compatible) - # Fallback to /etc/timezone for older systems - if command -v timedatectl >/dev/null 2>&1; then - timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC") - elif [ -f /etc/timezone ]; then - timezone=$(cat /etc/timezone) - else - timezone="UTC" - fi - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "User Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # Main menu loop - allows returning from Settings - while true; do - if [ -z "$CHOICE" ]; then - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - break - ;; - 2 | advanced | ADVANCED) - header_info - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - break - ;; - 3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - break - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - break - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - # After settings menu, show main menu again - header_info - CHOICE="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - done - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - ) - if [ -f "$(get_app_defaults_path)" ]; then - settings_items+=("3" "Edit App.vars for ${APP}") - settings_items+=("4" "← Back to Main Menu") - else - settings_items+=("3" "← Back to Main Menu") - fi - - local choice - choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts SETTINGS Menu" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \ - "${settings_items[@]}" \ - 3>&1 1>&2 2>&3) || exit_script - - case "$choice" in - 1) diagnostics_menu ;; - 2) nano /usr/local/community-scripts/default.vars ;; - 3) - if [ -f "$(get_app_defaults_path)" ]; then - nano "$(get_app_defaults_path)" - else - # Back was selected (no app.vars available) - return - fi - ;; - 4) - # Back to main menu - return - ;; - esac - done -} - -# ------------------------------------------------------------------------------ -# check_container_resources() -# -# - Compares host RAM/CPU with required values -# - Warns if under-provisioned and asks user to continue or abort -# ------------------------------------------------------------------------------ -check_container_resources() { - current_ram=$(free -m | awk 'NR==2{print $2}') - current_cpu=$(nproc) - - if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then - echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" - echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" - echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " - 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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 -} - -# ============================================================================== -# SECTION 8: CONTAINER CREATION & DEPLOYMENT -# ============================================================================== - -# ------------------------------------------------------------------------------ -# build_container() -# -# - Main function for creating and configuring LXC container -# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6) -# - Creates container via pct create with all specified settings -# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough -# - Starts container and waits for network connectivity -# - Installs base packages (curl, sudo, etc.) -# - Injects SSH keys if configured -# - Executes -install.sh inside container -# - Posts installation telemetry to API if diagnostics enabled -# ------------------------------------------------------------------------------ -build_container() { - # if [ "$VERBOSE" == "yes" ]; then set -x; fi - - NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}" - - # MAC - if [[ -n "$MAC" ]]; then - case "$MAC" in - ,hwaddr=*) NET_STRING+="$MAC" ;; - *) NET_STRING+=",hwaddr=$MAC" ;; - esac - fi - - # IP (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 - - # Build FEATURES string (simple working version) - 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 - - # NEW IMPLEMENTATION (Fixed): Build PCT_OPTIONS properly - # Key insight: Bash cannot export arrays, so we build the options as a string - - 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 - - # Core exports for install.func - export DIAGNOSTICS="$DIAGNOSTICS" - export RANDOM_UUID="$RANDOM_UUID" - export SESSION_ID="$SESSION_ID" - export CACHER="$APT_CACHER" - export CACHER_IP="$APT_CACHER_IP" - export tz="$timezone" - export APPLICATION="$APP" - export app="$NSAPP" - export PASSWORD="$PW" - export VERBOSE="$VERBOSE" - export SSH_ROOT="${SSH}" - export SSH_AUTHORIZED_KEY - export CTID="$CT_ID" - export CTTYPE="$CT_TYPE" - export ENABLE_FUSE="$ENABLE_FUSE" - export ENABLE_TUN="$ENABLE_TUN" - export PCT_OSTYPE="$var_os" - export PCT_OSVERSION="$var_version" - export PCT_DISK_SIZE="$DISK_SIZE" - - # DEV_MODE exports (optional, for debugging) - export BUILD_LOG="$BUILD_LOG" - export INSTALL_LOG="/root/.install-${SESSION_ID}.log" - export dev_mode="${dev_mode:-}" - export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}" - export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}" - export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}" - export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}" - export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}" - export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}" - export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" - - # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING=" -features $FEATURES - -hostname $HN - -tags $TAGS" - - # Add storage if specified - if [ -n "$SD" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $SD" - fi - - # Add nameserver if specified - if [ -n "$NS" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $NS" - fi - - # Network configuration - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $NET_STRING - -onboot 1 - -cores $CORE_COUNT - -memory $RAM_SIZE - -unprivileged $CT_TYPE" - - # Protection flag (if var_protection was set) - if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -protection 1" - fi - - # Timezone flag (if var_timezone was set) - if [ -n "${CT_TIMEZONE:-}" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - -timezone $CT_TIMEZONE" - fi - - # Password (already formatted) - if [ -n "$PW" ]; then - PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING - $PW" - fi - - # Export as string (this works, unlike arrays!) - export PCT_OPTIONS="$PCT_OPTIONS_STRING" - export TEMPLATE_STORAGE="${var_template_storage:-}" - export CONTAINER_STORAGE="${var_container_storage:-}" - - # # DEBUG: Show final PCT_OPTIONS being exported - # echo "[DEBUG] PCT_OPTIONS to be exported:" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Calling create_lxc_container..." - - create_lxc_container || exit $? - - LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" - - # ============================================================================ - # GPU/USB PASSTHROUGH CONFIGURATION - # ============================================================================ - - # List of applications that benefit from GPU acceleration - GPU_APPS=( - "immich" "channels" "emby" "ersatztv" "frigate" - "jellyfin" "plex" "scrypted" "tdarr" "unmanic" - "ollama" "fileflows" "open-webui" "tunarr" "debian" - "handbrake" "sunshine" "moonlight" "kodi" "stremio" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_custom "🎮" "${BL}" "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_custom "🎮" "${RD}" "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_custom "🎮" "${GN}" "Detected NVIDIA GPU" - - # Simple passthrough - just bind /dev/nvidia* devices if they exist - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough" - else - msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found" - msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)" - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_ok "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # Use pct set to add devices with proper dev0/dev1 format - # GIDs will be detected and set after container starts - local dev_index=0 - for dev in "${devices[@]}"; do - # Add to config using pct set (will be visible in GUI) - echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" - dev_index=$((dev_index + 1)) - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "No NVIDIA devices available for passthrough" - return 0 - fi - - # Use pct set for NVIDIA devices - local dev_index=0 - for dev in "${NVIDIA_DEVICES[@]}"; do - echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG" - dev_index=$((dev_index + 1)) - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # TUN device passthrough - if [ "$ENABLE_TUN" == "yes" ]; then - cat <>"$LXC_CONFIG" -lxc.cgroup2.devices.allow: c 10:200 rwm -lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file -EOF - fi - - # Coral TPU passthrough - if [[ -e /dev/apex_0 ]]; then - msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - 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 - - # Simple connectivity check - just verify IP is assigned - msg_ok "Network configured (IP: $ip_in_lxc)" - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories -http://dl-cdn.alpinelinux.org/alpine/latest-stable/main -http://dl-cdn.alpinelinux.org/alpine/latest-stable/community -EOF' - pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null" - else - sleep 3 - pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen" - pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ - echo LANG=\$locale_line >/etc/default/locale && \ - locale-gen >/dev/null && \ - export LANG=\$locale_line" - - if [[ -z "${tz:-}" ]]; then - tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") - fi - - if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then - # Set timezone using symlink (Debian 13+ compatible) - # Create /etc/timezone for backwards compatibility with older scripts - pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true" - else - msg_warn "Skipping timezone setup – zone '$tz' not found in container" - fi - - pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null" || { - msg_error "apt-get base packages installation failed" - exit 1 - } - fi - - msg_ok "Customized LXC Container" - - # Install SSH keys - install_ssh_keys_into_ct - - # Run application installer - # NOTE: We disable error handling here because: - # 1. Container errors are caught by error_handler INSIDE container - # 2. Container creates flag file with exit code - # 3. We read flag file and handle cleanup manually below - # 4. We DON'T want host error_handler to fire for lxc-attach command itself - - set +Eeuo pipefail # Disable ALL error handling temporarily - trap - ERR # Remove ERR trap completely - - lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/${var_install}.sh)" - local lxc_exit=$? - - set -Eeuo pipefail # Re-enable error handling - trap 'error_handler' ERR # Restore ERR trap - - # Check for error flag file in container (more reliable than lxc-attach exit code) - local install_exit_code=0 - if [[ -n "${SESSION_ID:-}" ]]; then - local error_flag="/root/.install-${SESSION_ID}.failed" - if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then - install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1") - pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true - fi - fi - - # Fallback to lxc-attach exit code if no flag file - if [[ $install_exit_code -eq 0 && $lxc_exit -ne 0 ]]; then - install_exit_code=$lxc_exit - fi - - # Installation failed? - if [[ $install_exit_code -ne 0 ]]; then - msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})" - - # Copy both logs from container before potential deletion - local build_log_copied=false - local install_log_copied=false - - if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then - # Copy BUILD_LOG (creation log) if it exists - if [[ -f "${BUILD_LOG}" ]]; then - cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true - fi - - # Copy INSTALL_LOG from container - if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "/tmp/install-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null; then - install_log_copied=true - fi - - # Show available logs - echo "" - [[ "$build_log_copied" == true ]] && echo -e "${GN}✔${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}" - [[ "$install_log_copied" == true ]] && echo -e "${GN}✔${CL} Installation log: ${BL}/tmp/install-lxc-${CTID}-${SESSION_ID}.log${CL}" - fi - - # Dev mode: Keep container or open breakpoint shell - if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then - msg_dev "Keep mode active - container ${CTID} preserved" - return 0 - elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then - msg_dev "Breakpoint mode - opening shell in container ${CTID}" - echo -e "${YW}Type 'exit' to return to host${CL}" - pct enter "$CTID" - echo "" - echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}" - if read -r response && [[ "$response" =~ ^[Yy]$ ]]; then - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - msg_ok "Container ${CTID} removed" - else - msg_dev "Container ${CTID} kept for debugging" - fi - exit $install_exit_code - fi - - # Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner) - echo "" - echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}" - - if read -t 60 -r response; then - if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then - # Remove container - echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" - elif [[ "$response" =~ ^[Nn]$ ]]; then - echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}" - - # Dev mode: Setup MOTD/SSH for debugging access to broken container - if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then - echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}" - if pct exec "$CTID" -- bash -c " - source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/install.func) - declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true - " >/dev/null 2>&1; then - local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) - echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}" - fi - fi - fi - else - # Timeout - auto-remove - echo -e "\n${YW}No response - auto-removing container${CL}" - echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}" - pct stop "$CTID" &>/dev/null || true - pct destroy "$CTID" &>/dev/null || true - echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}" - fi - - exit $install_exit_code - fi -} - -destroy_lxc() { - if [[ -z "$CT_ID" ]]; then - msg_error "No CT_ID found. Nothing to remove." - return 1 - fi - - # 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_custom "ℹ️" "${BL}" "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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Get actual GIDs from container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Create groups if they don't exist - if [[ -z "$video_gid" ]]; then - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1 - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" - fi - - if [[ -z "$render_gid" ]]; then - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1 - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" - fi - - # Stop container to update config - pct stop "$CTID" >/dev/null 2>&1 - sleep 1 - - # Update dev entries with correct GIDs - sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG" - sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG" - - # Restart container - pct start "$CTID" >/dev/null 2>&1 - sleep 2 - - msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})" - - # For privileged containers: also fix permissions inside container - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -check_storage_support() { - local CONTENT="$1" VALID=0 - while IFS= read -r line; do - local STORAGE_NAME - STORAGE_NAME=$(awk '{print $1}' <<<"$line") - [[ -n "$STORAGE_NAME" ]] && VALID=1 - done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') - [[ $VALID -eq 1 ]] -} - -select_storage() { - local CLASS=$1 CONTENT CONTENT_LABEL - case $CLASS in - container) - CONTENT='rootdir' - CONTENT_LABEL='Container' - ;; - template) - CONTENT='vztmpl' - CONTENT_LABEL='Container template' - ;; - iso) - CONTENT='iso' - CONTENT_LABEL='ISO image' - ;; - images) - CONTENT='images' - CONTENT_LABEL='VM Disk image' - ;; - backup) - CONTENT='backup' - CONTENT_LABEL='Backup' - ;; - snippets) - CONTENT='snippets' - CONTENT_LABEL='Snippets' - ;; - *) - msg_error "Invalid storage class '$CLASS'" - return 1 - ;; - esac - - declare -A STORAGE_MAP - local -a MENU=() - local COL_WIDTH=0 - - while read -r TAG TYPE _ TOTAL USED FREE _; do - [[ -n "$TAG" && -n "$TYPE" ]] || continue - local DISPLAY="${TAG} (${TYPE})" - local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") - local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") - local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" - STORAGE_MAP["$DISPLAY"]="$TAG" - MENU+=("$DISPLAY" "$INFO" "OFF") - ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') - - if [[ ${#MENU[@]} -eq 0 ]]; then - msg_error "No storage found for content type '$CONTENT'." - return 2 - fi - - if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then - STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" - STORAGE_INFO="${MENU[1]}" - return 0 - fi - - local WIDTH=$((COL_WIDTH + 42)) - while true; do - local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "[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 $STD apt-get update && $STD apt-get install -y --only-upgrade pve-container lxc-pve; then - msg_ok "LXC stack upgraded." - if [[ "$do_retry" == "yes" ]]; then - msg_info "Retrying container creation after upgrade" - if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - msg_ok "Container created successfully after upgrade." - return 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - msg_ok "Template search completed" - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_custom "🚫" "${YW}" "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_custom "ℹ️" "${YW}" "Please check:" - msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)" - msg_custom " •" "${YW}" "Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE" - fi - fi - - if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then - [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH" - for attempt in {1..3}; do - msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE" - if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then - msg_ok "Template download successful." - break - fi - if [[ $attempt -eq 3 ]]; then - msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE" - exit 222 - fi - sleep $((attempt * 5)) - done - fi - - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then - msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download." - exit 223 - fi - - # ------------------------------------------------------------------------------ - # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins) - # ------------------------------------------------------------------------------ - if [[ "$PCT_OSTYPE" == "debian" ]]; then - OSVER="$(parse_template_osver "$TEMPLATE")" - if [[ -n "$OSVER" ]]; then - # Proactive, 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 - - # PCT_OPTIONS is now a string (exported from build_container) - # Add rootfs if not already specified - if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then - PCT_OPTIONS="$PCT_OPTIONS - -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" - fi - - # Lock by template file (avoid concurrent downloads/creates) - lockfile="/tmp/template.${TEMPLATE}.lock" - exec 9>"$lockfile" || { - msg_error "Failed to create lock file '$lockfile'." - exit 200 - } - flock -w 60 9 || { - msg_error "Timeout while waiting for template lock." - exit 211 - } - - LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" - - # # DEBUG: Show the actual command that will be executed - # echo "[DEBUG] ===== PCT CREATE COMMAND DETAILS =====" - # echo "[DEBUG] CTID: $CTID" - # echo "[DEBUG] Template: ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" - # echo "[DEBUG] PCT_OPTIONS (will be word-split):" - # echo "$PCT_OPTIONS" | sed 's/^/ /' - # echo "[DEBUG] Full command line:" - # echo " pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - # echo "[DEBUG] ========================================" - - msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" - msg_debug "Logfile: $LOGFILE" - - # First attempt (PCT_OPTIONS is a multi-line string, use it directly) - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then - msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..." - - # Validate template file - if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - msg_warn "Template file too small or missing – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - else - msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." - fi - fi - - # Retry after repair - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - # Fallback to local storage if not already on local - if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - msg_info "Retrying container creation with fallback to local storage..." - LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then - msg_info "Downloading template to local..." - pveam download local "$TEMPLATE" >/dev/null 2>&1 - fi - if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then - # Local fallback also failed - check for LXC stack version issue - if grep -qiE 'unsupported .* version' "$LOGFILE"; then - echo - echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." - echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" - set +x - fi - exit 209 - fi - else - msg_ok "Container successfully created using local fallback." - fi - else - # Already on local storage and still failed - check LXC stack version - if grep -qiE 'unsupported .* version' "$LOGFILE"; then - echo - echo "pct reported 'unsupported ... version' – your LXC stack might be too old for this template." - echo "We can try to upgrade 'pve-container' and 'lxc-pve' now and retry automatically." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE" - set +x - fi - exit 209 - fi - fi - else - msg_ok "Container successfully created after template repair." - fi - fi - - # Verify container exists - pct list | awk '{print $1}' | grep -qx "$CTID" || { - msg_error "Container ID $CTID not listed in 'pct list'. See $LOGFILE" - exit 215 - } - - # Verify config rootfs - grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || { - msg_error "RootFS entry missing in container config. See $LOGFILE" - exit 216 - } - - msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." - - # Report container creation to API - post_to_api -} - -# ============================================================================== -# SECTION 9: POST-INSTALLATION & FINALIZATION -# ============================================================================== - -# ------------------------------------------------------------------------------ -# description() -# -# - Sets container description with formatted HTML content -# - Includes: -# * Community-Scripts logo -# * Application name -# * Links to GitHub, Discussions, Issues -# * Ko-fi donation badge -# - Restarts ping-instances.service if present (monitoring) -# - Posts final "done" status to API telemetry -# ------------------------------------------------------------------------------ -description() { - IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - - # Generate LXC Description - DESCRIPTION=$( - cat < - - 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" -} - -# ============================================================================== -# SECTION 10: ERROR HANDLING & EXIT TRAPS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# api_exit_script() -# -# - Exit trap handler for reporting to API telemetry -# - Captures exit code and reports to API using centralized error descriptions -# - Uses explain_exit_code() from error_handler.func for consistent error messages -# - Posts failure status with exit code to API (error description added automatically) -# - Only executes on non-zero exit codes -# ------------------------------------------------------------------------------ -api_exit_script() { - exit_code=$? - if [ $exit_code -ne 0 ]; then - post_update_to_api "failed" "$exit_code" - fi -} - -if command -v pveversion >/dev/null 2>&1; then - trap 'api_exit_script' EXIT -fi -trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR -trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT -trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM diff --git a/bak/core_bak/test-tools-func.sh b/bak/core_bak/test-tools-func.sh deleted file mode 100644 index a1655219e..000000000 --- a/bak/core_bak/test-tools-func.sh +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env bash - -# ============================================================================== -# TEST SUITE FOR tools.func -# ============================================================================== -# This script tests all setup_* functions from tools.func -# Can be run standalone in any Debian-based system -# -# Usage: -# bash <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/test-tools-func.sh) -# ============================================================================== - -set -uo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -# Counters -TESTS_PASSED=0 -TESTS_FAILED=0 -TESTS_SKIPPED=0 - -# Log file -TEST_LOG="/tmp/tools-func-test-$(date +%Y%m%d-%H%M%S).log" - -echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" -echo -e "${CYAN} TOOLS.FUNC TEST SUITE${NC}" -echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" -echo -e "Log file: ${TEST_LOG}\n" - -# Source tools.func from repository -echo -e "${BLUE}► Sourcing tools.func from repository...${NC}" -if ! source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func); then - echo -e "${RED}✖ Failed to source tools.func${NC}" - exit 1 -fi -echo -e "${GREEN}✔ tools.func loaded${NC}\n" - -# Source core functions if available -if curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func &>/dev/null; then - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) || true -fi - -# Override STD to show all output for debugging -export STD='' - -# Force non-interactive mode for all apt operations -export DEBIAN_FRONTEND=noninteractive - -# Update PATH to include common installation directories -export PATH="/usr/local/bin:/usr/local/go/bin:/root/.cargo/bin:/root/.rbenv/bin:/root/.rbenv/shims:/opt/java/bin:$PATH" - -# Helper functions (override if needed from core.func) -msg_info() { echo -e "${BLUE}ℹ ${1}${CL:-${NC}}"; } -msg_ok() { echo -e "${GREEN}✔ ${1}${CL:-${NC}}"; } -msg_error() { echo -e "${RED}✖ ${1}${CL:-${NC}}"; } -msg_warn() { echo -e "${YELLOW}⚠ ${1}${CL:-${NC}}"; } - -# Color definitions if not already set -GN="${GN:-${GREEN}}" -BL="${BL:-${BLUE}}" -RD="${RD:-${RED}}" -YW="${YW:-${YELLOW}}" -CL="${CL:-${NC}}" - -# Reload environment helper -reload_path() { - export PATH="/usr/local/bin:/usr/local/go/bin:/root/.cargo/bin:/root/.rbenv/bin:/root/.rbenv/shims:/opt/java/bin:$PATH" - # Source profile files if they exist - [ -f "/root/.bashrc" ] && source /root/.bashrc 2>/dev/null || true - [ -f "/root/.profile" ] && source /root/.profile 2>/dev/null || true - [ -f "/root/.cargo/env" ] && source /root/.cargo/env 2>/dev/null || true -} - -# Clean up before test to avoid interactive prompts and locks -cleanup_before_test() { - # Kill any hanging apt processes - killall apt-get apt 2>/dev/null || true - - # Remove apt locks - rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock 2>/dev/null || true - - # Clean up broken repository files from previous tests - # Remove all custom sources files - rm -f /etc/apt/sources.list.d/*.sources 2>/dev/null || true - rm -f /etc/apt/sources.list.d/*.list 2>/dev/null || true - - # Remove all keyrings - rm -f /etc/apt/keyrings/*.gpg 2>/dev/null || true - rm -f /etc/apt/keyrings/*.asc 2>/dev/null || true - - # Update package lists to ensure clean state - apt-get update -qq 2>/dev/null || true - - # Wait a moment for processes to clean up - sleep 1 -} -[ -f "/root/.profile" ] && source /root/.profile 2>/dev/null || true -[ -f "/root/.cargo/env" ] && source /root/.cargo/env 2>/dev/null || true - -# Test validation function -test_function() { - local test_name="$1" - local test_command="$2" - local validation_cmd="${3:-}" - - # Clean up before starting test - cleanup_before_test - - echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${CYAN}Testing: ${test_name}${NC}" - echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - - { - echo "=== Test: ${test_name} ===" - echo "Command: ${test_command}" - echo "Started: $(date)" - } | tee -a "$TEST_LOG" - - # Execute installation with output visible AND logged - if eval "$test_command" 2>&1 | tee -a "$TEST_LOG"; then - # Reload PATH after installation - reload_path - - if [[ -n "$validation_cmd" ]]; then - local output - if output=$(bash -c "$validation_cmd" 2>&1); then - msg_ok "${test_name} - $(echo "$output" | head -n1)" - ((TESTS_PASSED++)) - else - msg_error "${test_name} - Installation succeeded but validation failed" - { - echo "Validation command: $validation_cmd" - echo "Validation output: $output" - echo "PATH: $PATH" - } | tee -a "$TEST_LOG" - ((TESTS_FAILED++)) - fi - else - msg_ok "${test_name}" - ((TESTS_PASSED++)) - fi - else - msg_error "${test_name} - Installation failed" - echo "Installation failed" | tee -a "$TEST_LOG" - ((TESTS_FAILED++)) - fi - - echo "Completed: $(date)" | tee -a "$TEST_LOG" - echo "" | tee -a "$TEST_LOG" -} - -# Skip test with reason -skip_test() { - local test_name="$1" - local reason="$2" - - echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${CYAN}Testing: ${test_name}${NC}" - echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - msg_warn "Skipped: ${reason}" - ((TESTS_SKIPPED++)) -} - -# Update system -msg_info "Updating system packages" -apt-get update &>/dev/null && msg_ok "System updated" - -# Install base dependencies -msg_info "Installing base dependencies" -apt-get install -y curl wget gpg jq git build-essential ca-certificates &>/dev/null && msg_ok "Base dependencies installed" - -# ============================================================================== -# TEST 1: YQ - YAML Processor -# ============================================================================== -# test_function "YQ" \ -# "setup_yq" \ -# "yq --version" - -# ============================================================================== -# TEST 2: ADMINER - Database Management -# ============================================================================== -# test_function "Adminer" \ -# "setup_adminer" \ -# "dpkg -l adminer 2>/dev/null | grep -q '^ii' && a2query -c adminer 2>/dev/null && echo 'Adminer installed'" - -# ============================================================================== -# TEST 3: CLICKHOUSE -# ============================================================================== -# test_function "ClickHouse" \ -# "setup_clickhouse" \ -# "clickhouse-server --version" - -# ============================================================================== -# TEST 4: POSTGRESQL -# ============================================================================== -test_function "PostgreSQL 16" \ - "PG_VERSION=16 setup_postgresql" \ - "psql --version" - -# ============================================================================== -# TEST 6: MARIADB -# ============================================================================== -test_function "MariaDB 11.4" \ - "MARIADB_VERSION=11.4 setup_mariadb" \ - "mariadb --version" - -# ============================================================================== -# TEST 7: MYSQL (Remove MariaDB first) -# ============================================================================== -msg_info "Removing MariaDB before MySQL installation" -systemctl stop mariadb &>/dev/null || true -apt-get purge -y mariadb-server mariadb-client mariadb-common &>/dev/null || true -apt-get autoremove -y &>/dev/null -rm -rf /etc/mysql /var/lib/mysql -msg_ok "MariaDB removed" - -test_function "MySQL 8.0" \ - "MYSQL_VERSION=8.0 setup_mysql" \ - "mysql --version" - -# ============================================================================== -# TEST 8: MONGODB (Check AVX support) -# ============================================================================== -# if grep -q avx /proc/cpuinfo; then -# test_function "MongoDB 8.0" \ -# "MONGO_VERSION=8.0 setup_mongodb" \ -# "mongod --version" -# else -# skip_test "MongoDB 8.0" "CPU does not support AVX" -# fi - -# ============================================================================== -# TEST 9: NODE.JS -# ============================================================================== -# test_function "Node.js 22 with modules" \ -# "NODE_VERSION=22 NODE_MODULE='yarn,pnpm@10.1.0,pm2' setup_nodejs" \ -# "node --version && npm --version && yarn --version && pnpm --version && pm2 --version" - -# ============================================================================== -# TEST 10: PYTHON (UV) -# ============================================================================== -# test_function "Python 3.12 via uv" \ -# "PYTHON_VERSION=3.12 setup_uv" \ -# "uv --version" - -# ============================================================================== -# TEST 11: PHP -# ============================================================================== -# test_function "PHP 8.3 with FPM" \ -# "PHP_VERSION=8.3 PHP_FPM=YES PHP_MODULE='redis,imagick,apcu,zip,mbstring' setup_php" \ -# "php --version" - -# ============================================================================== -# TEST 12: COMPOSER -# # ============================================================================== -# test_function "Composer" \ -# "setup_composer" \ -# "composer --version" - -# ============================================================================== -# TEST 13: JAVA -# ============================================================================== -# test_function "Java Temurin 21" \ -# "JAVA_VERSION=21 setup_java" \ -# "java --version" - -# ============================================================================== -# TEST 14: GO -# ============================================================================== -# test_function "Go (latest)" \ -# "GO_VERSION=latest setup_go" \ -# "go version" - -# ============================================================================== -# TEST 15: RUBY -# ============================================================================== -test_function "Ruby 3.4.1 with Rails" \ - "RUBY_VERSION=3.4.1 RUBY_INSTALL_RAILS=true setup_ruby" \ - "ruby --version" - -# ============================================================================== -# TEST 16: RUST -# ============================================================================== -# test_function "Rust (stable)" \ -# "RUST_TOOLCHAIN=stable RUST_CRATES='cargo-edit' setup_rust" \ -# "source \$HOME/.cargo/env && rustc --version" - -# ============================================================================== -# TEST 17: GHOSTSCRIPT -# ============================================================================== -# test_function "Ghostscript" \ -# "setup_gs" \ -# "gs --version" - -# ============================================================================== -# TEST 18: IMAGEMAGICK -# ============================================================================== -# test_function "ImageMagick" \ -# "setup_imagemagick" \ -# "magick --version" - -# ============================================================================== -# TEST 19: FFMPEG -# ============================================================================== -# test_function "FFmpeg n7.1.1 (full)" \ -# "FFMPEG_VERSION=n7.1.1 FFMPEG_TYPE=full setup_ffmpeg" \ -# "ffmpeg -version" - -# ============================================================================== -# FINAL SUMMARY -# ============================================================================== -echo -e "\n${CYAN}═══════════════════════════════════════════════════════════${NC}" -echo -e "${CYAN} TEST SUMMARY${NC}" -echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" -echo -e "${GREEN}✔ Passed: ${TESTS_PASSED}${NC}" -echo -e "${RED}✖ Failed: ${TESTS_FAILED}${NC}" -echo -e "${YELLOW}⚠ Skipped: ${TESTS_SKIPPED}${NC}" -echo -e "\nDetailed log: ${TEST_LOG}" - -# Generate summary report -{ - echo "" - echo "=== FINAL SUMMARY ===" - echo "Tests Passed: ${TESTS_PASSED}" - echo "Tests Failed: ${TESTS_FAILED}" - echo "Tests Skipped: ${TESTS_SKIPPED}" - echo "" - echo "=== Installed Versions ===" - command -v yq &>/dev/null && echo "yq: $(yq --version 2>&1)" - command -v clickhouse-server &>/dev/null && echo "ClickHouse: $(clickhouse-server --version 2>&1 | head -n1)" - command -v psql &>/dev/null && echo "PostgreSQL: $(psql --version)" - command -v mysql &>/dev/null && echo "MySQL: $(mysql --version)" - command -v mongod &>/dev/null && echo "MongoDB: $(mongod --version 2>&1 | head -n1)" - command -v node &>/dev/null && echo "Node.js: $(node --version)" - command -v php &>/dev/null && echo "PHP: $(php --version | head -n1)" - command -v java &>/dev/null && echo "Java: $(java --version 2>&1 | head -n1)" - command -v go &>/dev/null && echo "Go: $(go version)" - command -v ruby &>/dev/null && echo "Ruby: $(ruby --version)" - command -v rustc &>/dev/null && echo "Rust: $(rustc --version)" - command -v ffmpeg &>/dev/null && echo "FFmpeg: $(ffmpeg -version 2>&1 | head -n1)" -} >>"$TEST_LOG" - -if [ $TESTS_FAILED -eq 0 ]; then - echo -e "\n${GREEN}All tests completed successfully!${NC}" - exit 0 -else - echo -e "\n${RED}Some tests failed. Check the log for details.${NC}" - exit 1 -fi diff --git a/bak/core_bak/tools.func.bak b/bak/core_bak/tools.func.bak deleted file mode 100644 index e98332517..000000000 --- a/bak/core_bak/tools.func.bak +++ /dev/null @@ -1,2208 +0,0 @@ -#!/bin/bash - -# ------------------------------------------------------------------------------ -# Installs Microsoft .NET -# -# Description: -# - Installs specified .NET version using Microsoft APT repo -# -# Variables: -# NET_VERSION - .NET version to install (default: "8.0") -# ------------------------------------------------------------------------------ - -function setup_dotnet() { - local NET_VERSION="${NET_VERSION:-8.0}" - local DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release) - local NEED_NET_INSTALL=false - - if command -v dotnet >/dev/null; then - CURRENT_NET_VERSION=$(dotnet --version | awk -F. '{print $1 "." $2}') - if [[ "$CURRENT_NET_VERSION" == "$NET_VERSION" ]]; then - : - fi - else - msg_info "Setup .NET $NET_VERSION" - NEED_NET_INSTALL=true - fi - - if [[ "$NEED_NET_INSTALL" == true ]]; then - curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg - if [[ "$DISTRO_CODENAME" != "trixie" ]]; then - curl -fsSL https://packages.microsoft.com/config/debian/12/prod.list -o /etc/apt/sources.list.d/msprod.list - else - curl -fsSL https://packages.microsoft.com/config/debian/13/prod.list -o /etc/apt/sources.list.d/msprod.list - fi - $STD apt-get update - $STD apt-get install -y dotnet-sdk-$NET_VERSION - msg_ok "Setup .NET ${NET_VERSION}" - fi -} - -# ------------------------------------------------------------------------------ -# Installs Node.js and optional global modules. -# -# Description: -# - Installs specified Node.js version using NodeSource APT repo -# - Optionally installs or updates global npm modules -# -# Variables: -# NODE_VERSION - Node.js version to install (default: 22) -# NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0") -# ------------------------------------------------------------------------------ - -function setup_nodejs() { - local NODE_VERSION="${NODE_VERSION:-22}" - local NODE_MODULE="${NODE_MODULE:-}" - local CURRENT_NODE_VERSION="" - local NEED_NODE_INSTALL=false - - if command -v node >/dev/null; then - CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')" - if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then - msg_info "Old Node.js $CURRENT_NODE_VERSION found, replacing with $NODE_VERSION" - NEED_NODE_INSTALL=true - fi - else - msg_info "Setup Node.js $NODE_VERSION" - NEED_NODE_INSTALL=true - fi - - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq || { - msg_error "Failed to install jq" - return 1 - } - fi - - if [[ "$NEED_NODE_INSTALL" == true ]]; then - $STD apt-get purge -y nodejs - rm -f /etc/apt/sources.list.d/nodesource.list /usr/share/keyrings/nodesource.gpg - - mkdir -p /usr/share/keyrings - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | - gpg --dearmor -o /usr/share/keyrings/nodesource.gpg || { - msg_error "Failed to import NodeSource GPG key" - exit 1 - } - chmod 644 /usr/share/keyrings/nodesource.gpg - - local ARCH - ARCH=$(dpkg --print-architecture) - if ! [[ "$ARCH" =~ ^(amd64|arm64|armhf)$ ]]; then - msg_error "Unsupported architecture: $ARCH" - exit 1 - fi - - echo "deb [arch=$ARCH signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" >/etc/apt/sources.list.d/nodesource.list - - cat </etc/apt/preferences.d/nodejs -Package: nodejs -Pin: origin deb.nodesource.com -Pin-Priority: 700 -EOF - - sleep 2 - if ! apt-get update >/dev/null 2>&1; then - msg_warn "APT update failed – retrying in 5s" - sleep 5 - if ! apt-get update >/dev/null 2>&1; then - msg_error "Failed to update APT repositories after adding NodeSource" - exit 1 - fi - fi - - if ! apt-get install -y -t nodistro nodejs >/dev/null 2>&1; then - msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" - apt-cache policy nodejs | tee "$STD" - exit 1 - fi - - $STD npm install -g npm@latest || { - msg_error "Failed to update npm to latest version" - } - msg_ok "Setup Node.js ${NODE_VERSION}" - fi - - export NODE_OPTIONS="--max-old-space-size=4096" - - [[ -d /opt ]] || mkdir -p /opt - cd /opt || { - msg_error "Failed to set safe working directory before npm install" - exit 1 - } - - if [[ -n "$NODE_MODULE" ]]; then - IFS=',' read -ra MODULES <<<"$NODE_MODULE" - for mod in "${MODULES[@]}"; do - local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION - if [[ "$mod" == @*/*@* ]]; then - MODULE_NAME="${mod%@*}" - MODULE_REQ_VERSION="${mod##*@}" - elif [[ "$mod" == *"@"* ]]; then - MODULE_NAME="${mod%@*}" - MODULE_REQ_VERSION="${mod##*@}" - else - MODULE_NAME="$mod" - MODULE_REQ_VERSION="latest" - fi - - if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then - MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')" - if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then - msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION" - $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" || { - msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" - exit 1 - } - elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then - msg_info "Updating $MODULE_NAME to latest version" - $STD npm install -g "${MODULE_NAME}@latest" || { - msg_error "Failed to update $MODULE_NAME to latest version" - exit 1 - } - fi - else - msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" - $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}" || { - msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" - exit 1 - } - fi - done - msg_ok "Installed Node.js modules: $NODE_MODULE" - fi -} - -# ------------------------------------------------------------------------------ -# Installs or upgrades PostgreSQL and optional extensions/modules. -# -# Description: -# - Detects existing PostgreSQL version -# - Dumps all databases before upgrade -# - Adds PGDG repo and installs specified version -# - Installs optional PG_MODULES (e.g. postgis, contrib) -# - Restores dumped data post-upgrade -# -# Variables: -# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16) -# PG_MODULES - Comma-separated list of extensions (e.g. "postgis,contrib") -# ------------------------------------------------------------------------------ -function setup_postgresql() { - local PG_VERSION="${PG_VERSION:-16}" - local PG_MODULES="${PG_MODULES:-}" - local CURRENT_PG_VERSION="" - local DISTRO - local NEED_PG_INSTALL=false - DISTRO="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)" - - if command -v psql >/dev/null; then - CURRENT_PG_VERSION="$(psql -V | awk '{print $3}' | cut -d. -f1)" - if [[ "$CURRENT_PG_VERSION" == "$PG_VERSION" ]]; then - : # PostgreSQL is already at the desired version – no action needed - else - $STD msg_info "Detected PostgreSQL $CURRENT_PG_VERSION, preparing upgrade to $PG_VERSION" - NEED_PG_INSTALL=true - fi - else - NEED_PG_INSTALL=true - fi - - if [[ "$NEED_PG_INSTALL" == true ]]; then - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD msg_info "Dumping PostgreSQL $CURRENT_PG_VERSION data" - su - postgres -c "pg_dumpall > /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" - $STD msg_ok "Data dump completed" - - systemctl stop postgresql - fi - - rm -f /etc/apt/sources.list.d/pgdg.list /etc/apt/trusted.gpg.d/postgresql.gpg - - $STD msg_info "Adding PostgreSQL PGDG repository" - curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | - gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg - - echo "deb https://apt.postgresql.org/pub/repos/apt ${DISTRO}-pgdg main" \ - >/etc/apt/sources.list.d/pgdg.list - $STD apt-get update - $STD msg_ok "Repository added" - - msg_info "Setup PostgreSQL $PG_VERSION" - $STD apt-get install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" - - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD apt-get purge -y "postgresql-${CURRENT_PG_VERSION}" "postgresql-client-${CURRENT_PG_VERSION}" || true - fi - systemctl enable -q --now postgresql - - if [[ -n "$CURRENT_PG_VERSION" ]]; then - $STD msg_info "Restoring dumped data" - su - postgres -c "psql < /var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql" - $STD msg_ok "Data restored" - fi - - $STD msg_ok "PostgreSQL $PG_VERSION installed" - fi - - # Install optional PostgreSQL modules - if [[ -n "$PG_MODULES" ]]; then - IFS=',' read -ra MODULES <<<"$PG_MODULES" - for module in "${MODULES[@]}"; do - local pkg="postgresql-${PG_VERSION}-${module}" - $STD msg_info "Setup PostgreSQL module/s: $pkg" - $STD apt-get install -y "$pkg" || { - msg_error "Failed to install $pkg" - continue - } - done - $STD msg_ok "Setup PostgreSQL modules" - fi -} - -# ------------------------------------------------------------------------------ -# Installs or updates MariaDB from official repo. -# -# Description: -# - Detects current MariaDB version and replaces it if necessary -# - Preserves existing database data -# - Dynamically determines latest GA version if "latest" is given -# -# Variables: -# MARIADB_VERSION - MariaDB version to install (e.g. 10.11, latest) (default: latest) -# ------------------------------------------------------------------------------ test - -setup_mariadb() { - local MARIADB_VERSION="${MARIADB_VERSION:-latest}" - local DISTRO_CODENAME - DISTRO_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)" - CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" - - if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then - msg_error "MariaDB mirror not reachable" - return 1 - fi - - msg_info "Setting up MariaDB $MARIADB_VERSION" - # Grab dynamic latest LTS version - if [[ "$MARIADB_VERSION" == "latest" ]]; then - MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ | - grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | - grep -vE 'rc/|rolling/' | - sed 's|/||' | - sort -Vr | - head -n1) - if [[ -z "$MARIADB_VERSION" ]]; then - msg_error "Could not determine latest GA MariaDB version" - return 1 - fi - fi - - local CURRENT_VERSION="" - if command -v mariadb >/dev/null; then - CURRENT_VERSION=$(mariadb --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') - fi - - if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then - $STD msg_info "MariaDB $MARIADB_VERSION, upgrading" - $STD apt-get update - $STD apt-get install --only-upgrade -y mariadb-server mariadb-client - $STD msg_ok "MariaDB upgraded to $MARIADB_VERSION" - return 0 - fi - - if [[ -n "$CURRENT_VERSION" ]]; then - $STD msg_info "Upgrading MariaDB $CURRENT_VERSION to $MARIADB_VERSION" - $STD systemctl stop mariadb >/dev/null 2>&1 || true - $STD apt-get purge -y 'mariadb*' || true - rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg - else - $STD msg_info "Setup MariaDB $MARIADB_VERSION" - fi - - curl -fsSL "https://mariadb.org/mariadb_release_signing_key.asc" | - gpg --dearmor -o /etc/apt/trusted.gpg.d/mariadb.gpg - - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mariadb.gpg] http://mirror.mariadb.org/repo/${MARIADB_VERSION}/${CURRENT_OS} ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/mariadb.list - - $STD apt-get update - - local MARIADB_MAJOR_MINOR - MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}') - if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then - echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections - else - for ver in 12.1 12.0 11.4 11.3 11.2 11.1 11.0 10.11 10.6 10.5 10.4 10.3; do - echo "mariadb-server-$ver mariadb-server/feedback boolean false" | debconf-set-selections - done - fi - DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { - msg_warn "Failed to install MariaDB ${MARIADB_VERSION} from upstream repo – trying distro package as fallback..." - # Cleanup, remove upstream repo to avoid conflicts - rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg - $STD apt-get update - # Final fallback: distro package - DEBIAN_FRONTEND=noninteractive $STD apt-get install -y mariadb-server mariadb-client || { - msg_error "MariaDB installation failed even with distro fallback!" - return 1 - } - msg_ok "Setup MariaDB (distro fallback)" - return 0 - } - - msg_ok "Setup MariaDB $MARIADB_VERSION" -} - -# ------------------------------------------------------------------------------ -# Installs or upgrades MySQL and configures APT repo. -# -# Description: -# - Detects existing MySQL installation -# - Purges conflicting packages before installation -# - Supports clean upgrade -# -# Variables: -# MYSQL_VERSION - MySQL version to install (e.g. 5.7, 8.0) (default: 8.0) -# ------------------------------------------------------------------------------ - -function setup_mysql() { - local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" - local CURRENT_VERSION="" - local NEED_INSTALL=false - CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" - - if command -v mysql >/dev/null; then - CURRENT_VERSION="$(mysql --version | grep -oP 'Distrib\s+\K[0-9]+\.[0-9]+')" - if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then - $STD msg_info "MySQL $CURRENT_VERSION will be upgraded to $MYSQL_VERSION" - NEED_INSTALL=true - else - # Check for patch-level updates - if apt list --upgradable 2>/dev/null | grep -q '^mysql-server/'; then - $STD msg_info "MySQL $CURRENT_VERSION available for upgrade" - $STD apt-get update - $STD apt-get install --only-upgrade -y mysql-server - $STD msg_ok "MySQL upgraded" - fi - return - fi - else - msg_info "Setup MySQL $MYSQL_VERSION" - NEED_INSTALL=true - fi - - if [[ "$NEED_INSTALL" == true ]]; then - $STD systemctl stop mysql || true - $STD apt-get purge -y "^mysql-server.*" "^mysql-client.*" "^mysql-common.*" || true - rm -f /etc/apt/sources.list.d/mysql.list /etc/apt/trusted.gpg.d/mysql.gpg - - local DISTRO_CODENAME - DISTRO_CODENAME="$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)" - curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2023 | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/${CURRENT_OS}/ ${DISTRO_CODENAME} mysql-${MYSQL_VERSION}" \ - >/etc/apt/sources.list.d/mysql.list - - export DEBIAN_FRONTEND=noninteractive - $STD apt-get update - $STD apt-get install -y mysql-server - msg_ok "Setup MySQL $MYSQL_VERSION" - fi -} - -# ------------------------------------------------------------------------------ -# Installs PHP with selected modules and configures Apache/FPM support. -# -# Description: -# - Adds Sury PHP repo if needed -# - Installs default and user-defined modules -# - Patches php.ini for CLI, Apache, and FPM as needed -# -# Variables: -# PHP_VERSION - PHP version to install (default: 8.4) -# PHP_MODULE - Additional comma-separated modules -# PHP_APACHE - Set YES to enable PHP with Apache -# PHP_FPM - Set YES to enable PHP-FPM -# PHP_MEMORY_LIMIT - (default: 512M) -# PHP_UPLOAD_MAX_FILESIZE - (default: 128M) -# PHP_POST_MAX_SIZE - (default: 128M) -# PHP_MAX_EXECUTION_TIME - (default: 300) -# ------------------------------------------------------------------------------ - -function setup_php() { - local PHP_VERSION="${PHP_VERSION:-8.4}" - local PHP_MODULE="${PHP_MODULE:-}" - local PHP_APACHE="${PHP_APACHE:-NO}" - local PHP_FPM="${PHP_FPM:-NO}" - local DISTRO_CODENAME - DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) - - local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip" - local COMBINED_MODULES - - local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}" - local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}" - local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}" - local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}" - - # Merge default + user-defined modules - if [[ -n "$PHP_MODULE" ]]; then - COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}" - else - COMBINED_MODULES="${DEFAULT_MODULES}" - fi - - # Deduplicate - COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -) - - # Get current PHP-CLI version - local CURRENT_PHP="" - if command -v php >/dev/null 2>&1; then - CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) - fi - - if [[ -z "$CURRENT_PHP" ]]; then - msg_info "Setup PHP $PHP_VERSION" - elif [[ "$CURRENT_PHP" != "$PHP_VERSION" ]]; then - msg_info "Old PHP $CURRENT_PHP detected, Setup new PHP $PHP_VERSION" - $STD apt-get purge -y "php${CURRENT_PHP//./}"* || true - fi - - # Ensure Sury repo is available - if [[ ! -f /etc/apt/sources.list.d/php.list ]]; then - $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb - $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb - echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/php.list - $STD apt-get update - fi - - # Build module list - local MODULE_LIST="php${PHP_VERSION}" - IFS=',' read -ra MODULES <<<"$COMBINED_MODULES" - for mod in "${MODULES[@]}"; do - if apt-cache show "php${PHP_VERSION}-${mod}" >/dev/null 2>&1; then - MODULE_LIST+=" php${PHP_VERSION}-${mod}" - else - msg_warn "PHP-Module ${mod} for PHP ${PHP_VERSION} not found – skipping" - fi - done - if [[ "$PHP_FPM" == "YES" ]]; then - MODULE_LIST+=" php${PHP_VERSION}-fpm" - fi - - # install apache2 with PHP support if requested - if [[ "$PHP_APACHE" == "YES" ]]; then - if ! dpkg -l | grep -q "libapache2-mod-php${PHP_VERSION}"; then - msg_info "Installing Apache with PHP${PHP_VERSION} support" - $STD apt-get install -y apache2 libapache2-mod-php${PHP_VERSION} - else - msg_info "Apache with PHP${PHP_VERSION} already installed – skipping install" - fi - fi - - # setup / update PHP modules - $STD apt-get install -y $MODULE_LIST - msg_ok "Setup PHP $PHP_VERSION" - - # optional stop old PHP-FPM service - if [[ "$PHP_FPM" == "YES" && -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then - $STD systemctl stop php"${CURRENT_PHP}"-fpm || true - $STD systemctl disable php"${CURRENT_PHP}"-fpm || true - fi - - # Patch all relevant php.ini files - local PHP_INI_PATHS=("/etc/php/${PHP_VERSION}/cli/php.ini") - [[ "$PHP_FPM" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/fpm/php.ini") - [[ "$PHP_APACHE" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/apache2/php.ini") - for ini in "${PHP_INI_PATHS[@]}"; do - if [[ -f "$ini" ]]; then - $STD msg_info "Patching $ini" - sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" - sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" - sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" - sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" - $STD msg_ok "Patched $ini" - fi - done - - # patch Apache configuration if needed - if [[ "$PHP_APACHE" == "YES" ]]; then - for mod in $(ls /etc/apache2/mods-enabled/ 2>/dev/null | grep -E '^php[0-9]\.[0-9]\.conf$' | sed 's/\.conf//'); do - if [[ "$mod" != "php${PHP_VERSION}" ]]; then - $STD a2dismod "$mod" || true - fi - done - $STD a2enmod mpm_prefork - $STD a2enmod "php${PHP_VERSION}" - $STD systemctl restart apache2 || true - fi - - # enable and restart PHP-FPM if requested - if [[ "$PHP_FPM" == "YES" ]]; then - if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then - $STD systemctl enable php${PHP_VERSION}-fpm - $STD systemctl restart php${PHP_VERSION}-fpm - else - msg_warn "FPM requested but service php${PHP_VERSION}-fpm not found" - fi - fi -} - -# ------------------------------------------------------------------------------ -# Installs or updates Composer globally. -# -# Description: -# - Downloads latest version from getcomposer.org -# - Installs to /usr/local/bin/composer -# ------------------------------------------------------------------------------ - -function setup_composer() { - local COMPOSER_BIN="/usr/local/bin/composer" - export COMPOSER_ALLOW_SUPERUSER=1 - - # Check if composer is already installed - if [[ -x "$COMPOSER_BIN" ]]; then - local CURRENT_VERSION - CURRENT_VERSION=$("$COMPOSER_BIN" --version | awk '{print $3}') - $STD msg_info "Old Composer $CURRENT_VERSION found, updating to latest" - else - msg_info "Setup Composer" - fi - - # Download and install latest composer - curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 - - if [[ $? -ne 0 ]]; then - msg_error "Failed to install Composer" - return 1 - fi - - chmod +x "$COMPOSER_BIN" - $STD composer diagnose - msg_ok "Setup Composer" -} - -# ------------------------------------------------------------------------------ -# Installs Go (Golang) from official tarball. -# -# Description: -# - Determines system architecture -# - Downloads latest version if GO_VERSION not set -# -# Variables: -# GO_VERSION - Version to install (e.g. 1.22.2 or latest) -# ------------------------------------------------------------------------------ - -function setup_go() { - local ARCH - case "$(uname -m)" in - x86_64) ARCH="amd64" ;; - aarch64) ARCH="arm64" ;; - *) - msg_error "Unsupported architecture: $(uname -m)" - return 1 - ;; - esac - - # Determine version - if [[ -z "${GO_VERSION:-}" || "${GO_VERSION}" == "latest" ]]; then - GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//') - if [[ -z "$GO_VERSION" ]]; then - msg_error "Could not determine latest Go version" - return 1 - fi - fi - - local GO_BIN="/usr/local/bin/go" - local GO_INSTALL_DIR="/usr/local/go" - - if [[ -x "$GO_BIN" ]]; then - local CURRENT_VERSION - CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//') - if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then - return 0 - else - $STD msg_info "Old Go Installation ($CURRENT_VERSION) found, upgrading to $GO_VERSION" - rm -rf "$GO_INSTALL_DIR" - fi - else - msg_info "Setup Go $GO_VERSION" - fi - - local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" - local URL="https://go.dev/dl/${TARBALL}" - local TMP_TAR=$(mktemp) - - curl -fsSL "$URL" -o "$TMP_TAR" || { - msg_error "Failed to download $TARBALL" - return 1 - } - - tar -C /usr/local -xzf "$TMP_TAR" - ln -sf /usr/local/go/bin/go /usr/local/bin/go - ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt - rm -f "$TMP_TAR" - - msg_ok "Setup Go $GO_VERSION" -} - -# ------------------------------------------------------------------------------ -# Installs Temurin JDK via Adoptium APT repository. -# -# Description: -# - Removes previous JDK if version mismatch -# - Installs or upgrades to specified JAVA_VERSION -# -# Variables: -# JAVA_VERSION - Temurin JDK version to install (e.g. 17, 21) -# ------------------------------------------------------------------------------ - -function setup_java() { - local JAVA_VERSION="${JAVA_VERSION:-21}" - local DISTRO_CODENAME - DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) - local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" - - # Add Adoptium repo if missing - if [[ ! -f /etc/apt/sources.list.d/adoptium.list ]]; then - $STD msg_info "Setting up Adoptium Repository" - mkdir -p /etc/apt/keyrings - curl -fsSL "https://packages.adoptium.net/artifactory/api/gpg/key/public" | gpg --dearmor -o /etc/apt/trusted.gpg.d/adoptium.gpg - echo "deb [signed-by=/etc/apt/trusted.gpg.d/adoptium.gpg] https://packages.adoptium.net/artifactory/deb ${DISTRO_CODENAME} main" \ - >/etc/apt/sources.list.d/adoptium.list - $STD apt-get update - $STD msg_ok "Set up Adoptium Repository" - fi - - # Detect currently installed temurin version - local INSTALLED_VERSION="" - if dpkg -l | grep -q "temurin-.*-jdk"; then - INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+') - fi - - if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then - $STD msg_info "Upgrading Temurin JDK $JAVA_VERSION" - $STD apt-get update - $STD apt-get install --only-upgrade -y "$DESIRED_PACKAGE" - $STD msg_ok "Upgraded Temurin JDK $JAVA_VERSION" - else - if [[ -n "$INSTALLED_VERSION" ]]; then - $STD msg_info "Removing Temurin JDK $INSTALLED_VERSION" - $STD apt-get purge -y "temurin-${INSTALLED_VERSION}-jdk" - fi - - msg_info "Setup Temurin JDK $JAVA_VERSION" - $STD apt-get install -y "$DESIRED_PACKAGE" - msg_ok "Setup Temurin JDK $JAVA_VERSION" - fi -} - -# ------------------------------------------------------------------------------ -# Installs or updates MongoDB to specified major version. -# -# Description: -# - Preserves data across installations -# - Adds official MongoDB repo -# -# Variables: -# MONGO_VERSION - MongoDB major version to install (e.g. 7.0, 8.0) -# ------------------------------------------------------------------------------ - -function setup_mongodb() { - local MONGO_VERSION="${MONGO_VERSION:-8.0}" - local DISTRO_ID DISTRO_CODENAME MONGO_BASE_URL - DISTRO_ID=$(awk -F= '/^ID=/{ gsub(/"/,"",$2); print $2 }' /etc/os-release) - DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{ print $2 }' /etc/os-release) - - # Check AVX support - if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then - local major="${MONGO_VERSION%%.*}" - if ((major > 5)); then - msg_error "MongoDB ${MONGO_VERSION} requires AVX support, which is not available on this system." - return 1 - fi - fi - - case "$DISTRO_ID" in - ubuntu) - MONGO_BASE_URL="https://repo.mongodb.org/apt/ubuntu" - REPO_COMPONENT="multiverse" - ;; - debian) - MONGO_BASE_URL="https://repo.mongodb.org/apt/debian" - REPO_COMPONENT="main" - ;; - *) - msg_error "Unsupported distribution: $DISTRO_ID" - return 1 - ;; - esac - - local REPO_LIST="/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.list" - - local INSTALLED_VERSION="" - if command -v mongod >/dev/null; then - INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) - fi - - if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then - $STD msg_info "Upgrading MongoDB $MONGO_VERSION" - $STD apt-get update - $STD apt-get install --only-upgrade -y mongodb-org - $STD msg_ok "Upgraded MongoDB $MONGO_VERSION" - return 0 - fi - - if [[ -n "$INSTALLED_VERSION" ]]; then - $STD systemctl stop mongod || true - $STD apt-get purge -y mongodb-org || true - rm -f /etc/apt/sources.list.d/mongodb-org-*.list - rm -f /etc/apt/trusted.gpg.d/mongodb-*.gpg - else - msg_info "Setup MongoDB $MONGO_VERSION" - fi - - curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg" - echo "deb [signed-by=/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg] ${MONGO_BASE_URL} ${DISTRO_CODENAME}/mongodb-org/${MONGO_VERSION} main" \ - >"$REPO_LIST" - - $STD apt-get update || { - msg_error "APT update failed — invalid MongoDB repo for ${DISTRO_ID}-${DISTRO_CODENAME}?" - return 1 - } - - $STD apt-get install -y mongodb-org - - mkdir -p /var/lib/mongodb - chown -R mongodb:mongodb /var/lib/mongodb - - $STD systemctl enable mongod - $STD systemctl start mongod - msg_ok "Setup MongoDB $MONGO_VERSION" -} - -# ------------------------------------------------------------------------------ -# Downloads and deploys latest GitHub release (source, binary, tarball, asset). -# -# Description: -# - Fetches latest release metadata from GitHub API -# - Supports the following modes: -# - tarball: Source code tarball (default if omitted) -# - source: Alias for tarball (same behavior) -# - binary: .deb package install (arch-dependent) -# - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries) -# - singlefile: Standalone binary (no archive, direct chmod +x install) -# - Handles download, extraction/installation and version tracking in ~/. -# -# Parameters: -# $1 APP - Application name (used for install path and version file) -# $2 REPO - GitHub repository in form user/repo -# $3 MODE - Release type: -# tarball → source tarball (.tar.gz) -# binary → .deb file (auto-arch matched) -# prebuild → prebuilt archive (e.g. tar.gz) -# singlefile→ standalone binary (chmod +x) -# $4 VERSION - Optional release tag (default: latest) -# $5 TARGET_DIR - Optional install path (default: /opt/) -# $6 ASSET_FILENAME - Required for: -# - prebuild → archive filename or pattern -# - singlefile→ binary filename or pattern -# -# Optional: -# - Set GITHUB_TOKEN env var to increase API rate limit (recommended for CI/CD). -# -# Examples: -# # 1. Minimal: Fetch and deploy source tarball -# fetch_and_deploy_gh_release "myapp" "myuser/myapp" -# -# # 2. Binary install via .deb asset (architecture auto-detected) -# fetch_and_deploy_gh_release "myapp" "myuser/myapp" "binary" -# -# # 3. Prebuilt archive (.tar.gz) with asset filename match -# fetch_and_deploy_gh_release "hanko" "teamhanko/hanko" "prebuild" "latest" "/opt/hanko" "hanko_Linux_x86_64.tar.gz" -# -# # 4. Single binary (chmod +x) like Argus, Promtail etc. -# fetch_and_deploy_gh_release "argus" "release-argus/Argus" "singlefile" "0.26.3" "/opt/argus" "Argus-.*linux-amd64" -# ------------------------------------------------------------------------------ - -function fetch_and_deploy_gh_release() { - local app="$1" - local repo="$2" - local mode="${3:-tarball}" # tarball | binary | prebuild | singlefile - local version="${4:-latest}" - local target="${5:-/opt/$app}" - local asset_pattern="${6:-}" - - local app_lc=$(echo "${app,,}" | tr -d ' ') - local version_file="$HOME/.${app_lc}" - - local api_timeout="--connect-timeout 10 --max-time 60" - local download_timeout="--connect-timeout 15 --max-time 900" - - local current_version="" - [[ -f "$version_file" ]] && current_version=$(<"$version_file") - - if ! command -v jq &>/dev/null; then - $STD apt-get install -y jq &>/dev/null - fi - - local api_url="https://api.github.com/repos/$repo/releases" - [[ "$version" != "latest" ]] && api_url="$api_url/tags/$version" || api_url="$api_url/latest" - local header=() - [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN") - - # dns pre check - local gh_host - gh_host=$(awk -F/ '{print $3}' <<<"$api_url") - if ! getent hosts "$gh_host" &>/dev/null; then - msg_error "DNS resolution failed for $gh_host – check /etc/resolv.conf or networking" - return 1 - fi - - local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code - - while ((attempt <= max_retries)); do - resp=$(curl $api_timeout -fsSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url") && success=true && break - sleep "$retry_delay" - ((attempt++)) - done - - if ! $success; then - msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts" - return 1 - fi - - http_code="${resp:(-3)}" - [[ "$http_code" != "200" ]] && { - msg_error "GitHub API returned HTTP $http_code" - return 1 - } - - local json tag_name - json=$(/dev/null || uname -m) - [[ "$arch" == "x86_64" ]] && arch="amd64" - [[ "$arch" == "aarch64" ]] && arch="arm64" - - local assets url_match="" - assets=$(echo "$json" | jq -r '.assets[].browser_download_url') - - # If explicit filename pattern is provided (param $6), match that first - if [[ -n "$asset_pattern" ]]; then - for u in $assets; do - case "${u##*/}" in - $asset_pattern) - url_match="$u" - break - ;; - esac - done - fi - - # If no match via explicit pattern, fall back to architecture heuristic - if [[ -z "$url_match" ]]; then - for u in $assets; do - if [[ "$u" =~ ($arch|amd64|x86_64|aarch64|arm64).*\.deb$ ]]; then - url_match="$u" - break - fi - done - fi - - # Fallback: any .deb file - if [[ -z "$url_match" ]]; then - for u in $assets; do - [[ "$u" =~ \.deb$ ]] && url_match="$u" && break - done - fi - - if [[ -z "$url_match" ]]; then - msg_error "No suitable .deb asset found for $app" - rm -rf "$tmpdir" - return 1 - fi - - filename="${url_match##*/}" - curl $download_timeout -fsSL -o "$tmpdir/$filename" "$url_match" || { - msg_error "Download failed: $url_match" - rm -rf "$tmpdir" - return 1 - } - - chmod 644 "$tmpdir/$filename" - $STD apt-get install -y "$tmpdir/$filename" || { - $STD dpkg -i "$tmpdir/$filename" || { - msg_error "Both apt and dpkg installation failed" - rm -rf "$tmpdir" - return 1 - } - } - - ### Prebuild Mode ### - elif [[ "$mode" == "prebuild" ]]; then - local pattern="${6%\"}" - pattern="${pattern#\"}" - [[ -z "$pattern" ]] && { - msg_error "Mode 'prebuild' requires 6th parameter (asset filename pattern)" - rm -rf "$tmpdir" - return 1 - } - - local asset_url="" - for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do - filename_candidate="${u##*/}" - case "$filename_candidate" in - $pattern) - asset_url="$u" - break - ;; - esac - done - - [[ -z "$asset_url" ]] && { - msg_error "No asset matching '$pattern' found" - rm -rf "$tmpdir" - return 1 - } - - filename="${asset_url##*/}" - curl $download_timeout -fsSL -o "$tmpdir/$filename" "$asset_url" || { - msg_error "Download failed: $asset_url" - rm -rf "$tmpdir" - return 1 - } - - local unpack_tmp - unpack_tmp=$(mktemp -d) - mkdir -p "$target" - if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then - rm -rf "${target:?}/"* - fi - - if [[ "$filename" == *.zip ]]; then - if ! command -v unzip &>/dev/null; then - $STD apt-get install -y unzip - fi - unzip -q "$tmpdir/$filename" -d "$unpack_tmp" - elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then - tar -xf "$tmpdir/$filename" -C "$unpack_tmp" - else - msg_error "Unsupported archive format: $filename" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - - local top_dirs - top_dirs=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1 -type d | wc -l) - local top_entries inner_dir - top_entries=$(find "$unpack_tmp" -mindepth 1 -maxdepth 1) - if [[ "$(echo "$top_entries" | wc -l)" -eq 1 && -d "$top_entries" ]]; then - # Strip leading folder - inner_dir="$top_entries" - shopt -s dotglob nullglob - if compgen -G "$inner_dir/*" >/dev/null; then - cp -r "$inner_dir"/* "$target/" || { - msg_error "Failed to copy contents from $inner_dir to $target" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Inner directory is empty: $inner_dir" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - shopt -u dotglob nullglob - else - # Copy all contents - shopt -s dotglob nullglob - if compgen -G "$unpack_tmp/*" >/dev/null; then - cp -r "$unpack_tmp"/* "$target/" || { - msg_error "Failed to copy contents to $target" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - } - else - msg_error "Unpacked archive is empty" - rm -rf "$tmpdir" "$unpack_tmp" - return 1 - fi - shopt -u dotglob nullglob - fi - - ### Singlefile Mode ### - elif [[ "$mode" == "singlefile" ]]; then - local pattern="${6%\"}" - pattern="${pattern#\"}" - [[ -z "$pattern" ]] && { - msg_error "Mode 'singlefile' requires 6th parameter (asset filename pattern)" - rm -rf "$tmpdir" - return 1 - } - - local asset_url="" - for u in $(echo "$json" | jq -r '.assets[].browser_download_url'); do - filename_candidate="${u##*/}" - case "$filename_candidate" in - $pattern) - asset_url="$u" - break - ;; - esac - done - - [[ -z "$asset_url" ]] && { - msg_error "No asset matching '$pattern' found" - rm -rf "$tmpdir" - return 1 - } - - filename="${asset_url##*/}" - mkdir -p "$target" - - local use_filename="${USE_ORIGINAL_FILENAME:-false}" - local target_file="$app" - [[ "$use_filename" == "true" ]] && target_file="$filename" - - curl $download_timeout -fsSL -o "$target/$target_file" "$asset_url" || { - msg_error "Download failed: $asset_url" - rm -rf "$tmpdir" - return 1 - } - - if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then - chmod +x "$target/$target_file" - fi - - else - msg_error "Unknown mode: $mode" - rm -rf "$tmpdir" - return 1 - fi - - echo "$version" >"$version_file" - msg_ok "Deployed: $app ($version)" - rm -rf "$tmpdir" -} - -# ------------------------------------------------------------------------------ -# Installs a local IP updater script using networkd-dispatcher. -# -# Description: -# - Stores current IP in /run/local-ip.env -# - Automatically runs on network changes -# ------------------------------------------------------------------------------ - -function setup_local_ip_helper() { - local BASE_DIR="/usr/local/community-scripts/ip-management" - local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh" - local IP_FILE="/run/local-ip.env" - local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh" - - mkdir -p "$BASE_DIR" - - # Install networkd-dispatcher if not present - if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then - $STD apt-get update - $STD apt-get install -y networkd-dispatcher - fi - - # Write update_local_ip.sh - cat <<'EOF' >"$SCRIPT_PATH" -#!/bin/bash -set -euo pipefail - -IP_FILE="/run/local-ip.env" -mkdir -p "$(dirname "$IP_FILE")" - -get_current_ip() { - local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") - local ip - - for target in "${targets[@]}"; do - if [[ "$target" == "default" ]]; then - ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - else - ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - fi - if [[ -n "$ip" ]]; then - echo "$ip" - return 0 - fi - done - - return 1 -} - -current_ip="$(get_current_ip)" - -if [[ -z "$current_ip" ]]; then - echo "[ERROR] Could not detect local IP" >&2 - exit 1 -fi - -if [[ -f "$IP_FILE" ]]; then - source "$IP_FILE" - [[ "$LOCAL_IP" == "$current_ip" ]] && exit 0 -fi - -echo "LOCAL_IP=$current_ip" > "$IP_FILE" -echo "[INFO] LOCAL_IP updated to $current_ip" -EOF - - chmod +x "$SCRIPT_PATH" - - # Install dispatcher hook - mkdir -p "$(dirname "$DISPATCHER_SCRIPT")" - cat <"$DISPATCHER_SCRIPT" -#!/bin/bash -$SCRIPT_PATH -EOF - - chmod +x "$DISPATCHER_SCRIPT" - systemctl enable -q --now networkd-dispatcher.service -} - -# ------------------------------------------------------------------------------ -# Loads LOCAL_IP from persistent store or detects if missing. -# -# Description: -# - Loads from /run/local-ip.env or performs runtime lookup -# ------------------------------------------------------------------------------ - -function import_local_ip() { - local IP_FILE="/run/local-ip.env" - if [[ -f "$IP_FILE" ]]; then - # shellcheck disable=SC1090 - source "$IP_FILE" - fi - - if [[ -z "${LOCAL_IP:-}" ]]; then - get_current_ip() { - local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") - local ip - - for target in "${targets[@]}"; do - if [[ "$target" == "default" ]]; then - ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - else - ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - fi - if [[ -n "$ip" ]]; then - echo "$ip" - return 0 - fi - done - - return 1 - } - - LOCAL_IP="$(get_current_ip || true)" - if [[ -z "$LOCAL_IP" ]]; then - msg_error "Could not determine LOCAL_IP" - return 1 - fi - fi - - export LOCAL_IP -} - -# ------------------------------------------------------------------------------ -# Downloads file with optional progress indicator using pv. -# -# Arguments: -# $1 - URL -# $2 - Destination path -# ------------------------------------------------------------------------------ - -function download_with_progress() { - local url="$1" - local output="$2" - if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi - - if ! command -v pv &>/dev/null; then - $STD apt-get install -y pv - fi - set -o pipefail - - # Content-Length aus HTTP-Header holen - local content_length - content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true) - - if [[ -z "$content_length" ]]; then - if ! curl -fL# -o "$output" "$url"; then - msg_error "Download failed" - return 1 - fi - else - if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then - msg_error "Download failed" - return 1 - fi - fi -} - -# ------------------------------------------------------------------------------ -# Installs or upgrades uv (Python package manager) from GitHub releases. -# - Downloads platform-specific tarball (no install.sh!) -# - Extracts uv binary -# - Places it in /usr/local/bin -# - Optionally installs a specific Python version via uv -# ------------------------------------------------------------------------------ - -function setup_uv() { - local UV_BIN="/usr/local/bin/uv" - local TMP_DIR - TMP_DIR=$(mktemp -d) - - # Determine system architecture - local ARCH - ARCH=$(uname -m) - local UV_TAR - - case "$ARCH" in - x86_64) - if grep -qi "alpine" /etc/os-release; then - UV_TAR="uv-x86_64-unknown-linux-musl.tar.gz" - else - UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz" - fi - ;; - aarch64) - if grep -qi "alpine" /etc/os-release; then - UV_TAR="uv-aarch64-unknown-linux-musl.tar.gz" - else - UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz" - fi - ;; - *) - msg_error "Unsupported architecture: $ARCH" - rm -rf "$TMP_DIR" - return 1 - ;; - esac - - # Get latest version from GitHub - local LATEST_VERSION - LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | - grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') - - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not fetch latest uv version from GitHub." - rm -rf "$TMP_DIR" - return 1 - fi - - # Check if uv is already up to date - if [[ -x "$UV_BIN" ]]; then - local INSTALLED_VERSION - INSTALLED_VERSION=$($UV_BIN -V | awk '{print $2}') - if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then - rm -rf "$TMP_DIR" - [[ ":$PATH:" != *":/usr/local/bin:"* ]] && export PATH="/usr/local/bin:$PATH" - return 0 - else - msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION" - fi - else - msg_info "Setup uv $LATEST_VERSION" - fi - - # Download and install manually - local UV_URL="https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" - if ! curl -fsSL "$UV_URL" -o "$TMP_DIR/uv.tar.gz"; then - msg_error "Failed to download $UV_URL" - rm -rf "$TMP_DIR" - return 1 - fi - - if ! tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR"; then - msg_error "Failed to extract uv archive" - rm -rf "$TMP_DIR" - return 1 - fi - - install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" || { - msg_error "Failed to install uv binary" - rm -rf "$TMP_DIR" - return 1 - } - - rm -rf "$TMP_DIR" - #ensure_usr_local_bin_persist - $STD uv python update-shell || { - msg_error "Failed to update uv shell integration" - return 1 - } - msg_ok "Setup uv $LATEST_VERSION" - - # Optional: install specific Python version - if [[ -n "${PYTHON_VERSION:-}" ]]; then - local VERSION_MATCH - VERSION_MATCH=$(uv python list --only-downloads | - grep -E "^cpython-${PYTHON_VERSION//./\\.}\.[0-9]+-linux" | - cut -d'-' -f2 | sort -V | tail -n1) - - if [[ -z "$VERSION_MATCH" ]]; then - msg_error "No matching Python $PYTHON_VERSION.x version found via uv" - return 1 - fi - - if ! uv python list | grep -q "cpython-${VERSION_MATCH}-linux.*uv/python"; then - if ! $STD uv python install "$VERSION_MATCH"; then - msg_error "Failed to install Python $VERSION_MATCH via uv" - return 1 - fi - msg_ok "Setup Python $VERSION_MATCH via uv" - fi - fi -} - -# ------------------------------------------------------------------------------ -# Ensures /usr/local/bin is permanently in system PATH. -# -# Description: -# - Adds to /etc/profile.d if not present -# ------------------------------------------------------------------------------ - -function ensure_usr_local_bin_persist() { - local PROFILE_FILE="/etc/profile.d/custom_path.sh" - - if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then - echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE" - chmod +x "$PROFILE_FILE" - fi -} - -# ------------------------------------------------------------------------------ -# Installs or updates Ghostscript (gs) from source. -# -# Description: -# - Fetches latest release -# - Builds and installs system-wide -# ------------------------------------------------------------------------------ - -function setup_gs() { - mkdir -p /tmp - TMP_DIR=$(mktemp -d) - CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") - - RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) - LATEST_VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name":' | head -n1 | cut -d '"' -f4 | sed 's/^gs//') - LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | grep '"name":' | head -n1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') - - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not determine latest Ghostscript version from GitHub." - rm -rf "$TMP_DIR" - return - fi - - if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED"; then - rm -rf "$TMP_DIR" - return - fi - - msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED (Patience)" - curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" - - if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then - msg_error "Failed to extract Ghostscript archive." - rm -rf "$TMP_DIR" - return - fi - - cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { - msg_error "Failed to enter Ghostscript source directory." - rm -rf "$TMP_DIR" - } - $STD apt-get install -y build-essential libpng-dev zlib1g-dev - $STD ./configure >/dev/null - $STD make - $STD sudo make install - local EXIT_CODE=$? - hash -r - if [[ ! -x "$(command -v gs)" ]]; then - if [[ -x /usr/local/bin/gs ]]; then - ln -sf /usr/local/bin/gs /usr/bin/gs - fi - fi - - rm -rf "$TMP_DIR" - - if [[ $EXIT_CODE -eq 0 ]]; then - msg_ok "Setup Ghostscript $LATEST_VERSION_DOTTED" - else - msg_error "Ghostscript installation failed" - fi -} - -# ------------------------------------------------------------------------------ -# Installs rbenv and ruby-build, installs Ruby and optionally Rails. -# -# Description: -# - Downloads rbenv and ruby-build from GitHub -# - Compiles and installs target Ruby version -# - Optionally installs Rails via gem -# -# Variables: -# RUBY_VERSION - Ruby version to install (default: 3.4.4) -# RUBY_INSTALL_RAILS - true/false to install Rails (default: true) -# ------------------------------------------------------------------------------ - -function setup_ruby() { - local RUBY_VERSION="${RUBY_VERSION:-3.4.4}" - local RUBY_INSTALL_RAILS="${RUBY_INSTALL_RAILS:-true}" - - local RBENV_DIR="$HOME/.rbenv" - local RBENV_BIN="$RBENV_DIR/bin/rbenv" - local PROFILE_FILE="$HOME/.profile" - local TMP_DIR - TMP_DIR=$(mktemp -d) - - msg_info "Setup Ruby $RUBY_VERSION" - - local RBENV_RELEASE - RBENV_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/rbenv/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') - if [[ -z "$RBENV_RELEASE" ]]; then - msg_error "Failed to fetch latest rbenv version" - rm -rf "$TMP_DIR" - return 1 - fi - - curl -fsSL "https://github.com/rbenv/rbenv/archive/refs/tags/v${RBENV_RELEASE}.tar.gz" -o "$TMP_DIR/rbenv.tar.gz" - tar -xzf "$TMP_DIR/rbenv.tar.gz" -C "$TMP_DIR" - mkdir -p "$RBENV_DIR" - cp -r "$TMP_DIR/rbenv-${RBENV_RELEASE}/." "$RBENV_DIR/" - cd "$RBENV_DIR" && src/configure && $STD make -C src - - local RUBY_BUILD_RELEASE - RUBY_BUILD_RELEASE=$(curl -fsSL https://api.github.com/repos/rbenv/ruby-build/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') - if [[ -z "$RUBY_BUILD_RELEASE" ]]; then - msg_error "Failed to fetch latest ruby-build version" - rm -rf "$TMP_DIR" - return 1 - fi - - curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/v${RUBY_BUILD_RELEASE}.tar.gz" -o "$TMP_DIR/ruby-build.tar.gz" - tar -xzf "$TMP_DIR/ruby-build.tar.gz" -C "$TMP_DIR" - mkdir -p "$RBENV_DIR/plugins/ruby-build" - cp -r "$TMP_DIR/ruby-build-${RUBY_BUILD_RELEASE}/." "$RBENV_DIR/plugins/ruby-build/" - echo "$RUBY_BUILD_RELEASE" >"$RBENV_DIR/plugins/ruby-build/RUBY_BUILD_version.txt" - - if ! grep -q 'rbenv init' "$PROFILE_FILE"; then - echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >>"$PROFILE_FILE" - echo 'eval "$(rbenv init -)"' >>"$PROFILE_FILE" - fi - - export PATH="$RBENV_DIR/bin:$PATH" - eval "$("$RBENV_BIN" init - bash)" - - if ! "$RBENV_BIN" versions --bare | grep -qx "$RUBY_VERSION"; then - $STD "$RBENV_BIN" install "$RUBY_VERSION" - fi - - "$RBENV_BIN" global "$RUBY_VERSION" - hash -r - - if [[ "$RUBY_INSTALL_RAILS" == "true" ]]; then - msg_info "Setup Rails via gem" - gem install rails - msg_ok "Setup Rails $(rails -v)" - fi - - rm -rf "$TMP_DIR" - msg_ok "Setup Ruby $RUBY_VERSION" -} - -# ------------------------------------------------------------------------------ -# Creates and installs self-signed certificates. -# -# Description: -# - Create a self-signed certificate with option to override application name -# -# Variables: -# APP - Application name (default: $APPLICATION variable) -# ------------------------------------------------------------------------------ -function create_selfsigned_certs() { - local app=${APP:-$(echo "${APPLICATION,,}" | tr -d ' ')} - $STD openssl req -x509 -nodes -days 365 -newkey rsa:4096 \ - -keyout /etc/ssl/private/"$app"-selfsigned.key \ - -out /etc/ssl/certs/"$app"-selfsigned.crt \ - -subj "/C=US/O=$app/OU=Domain Control Validated/CN=localhost" -} - -# ------------------------------------------------------------------------------ -# Installs Rust toolchain and optional global crates via cargo. -# -# Description: -# - Installs rustup (if missing) -# - Installs or updates desired Rust toolchain (stable, nightly, or versioned) -# - Installs or updates specified global crates using `cargo install` -# -# Notes: -# - Skips crate install if exact version is already present -# - Updates crate if newer version or different version is requested -# -# Variables: -# RUST_TOOLCHAIN - Rust toolchain to install (default: stable) -# RUST_CRATES - Comma-separated list of crates (e.g. "cargo-edit,wasm-pack@0.12.1") -# ------------------------------------------------------------------------------ - -function setup_rust() { - local RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-stable}" - local RUST_CRATES="${RUST_CRATES:-}" - local CARGO_BIN="${HOME}/.cargo/bin" - - # rustup & toolchain - if ! command -v rustup &>/dev/null; then - msg_info "Setup Rust" - curl -fsSL https://sh.rustup.rs | $STD sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" - export PATH="$CARGO_BIN:$PATH" - echo 'export PATH="$HOME/.cargo/bin:$PATH"' >>"$HOME/.profile" - msg_ok "Setup Rust" - else - $STD rustup install "$RUST_TOOLCHAIN" - $STD rustup default "$RUST_TOOLCHAIN" - $STD rustup update "$RUST_TOOLCHAIN" - msg_ok "Rust toolchain set to $RUST_TOOLCHAIN" - fi - - # install/update crates - if [[ -n "$RUST_CRATES" ]]; then - IFS=',' read -ra CRATES <<<"$RUST_CRATES" - for crate in "${CRATES[@]}"; do - local NAME VER INSTALLED_VER - if [[ "$crate" == *"@"* ]]; then - NAME="${crate%@*}" - VER="${crate##*@}" - else - NAME="$crate" - VER="" - fi - - INSTALLED_VER=$(cargo install --list 2>/dev/null | awk "/^$NAME v[0-9]/ {print \$2}" | tr -d 'v') - - if [[ -n "$INSTALLED_VER" ]]; then - if [[ -n "$VER" && "$VER" != "$INSTALLED_VER" ]]; then - msg_info "Update $NAME: $INSTALLED_VER → $VER" - $STD cargo install "$NAME" --version "$VER" --force - msg_ok "Updated $NAME to $VER" - elif [[ -z "$VER" ]]; then - msg_info "Update $NAME: $INSTALLED_VER → latest" - $STD cargo install "$NAME" --force - msg_ok "Updated $NAME to latest" - fi - else - msg_info "Setup $NAME ${VER:+($VER)}" - $STD cargo install "$NAME" ${VER:+--version "$VER"} - msg_ok "Setup $NAME ${VER:-latest}" - fi - done - msg_ok "Setup Rust" - fi -} - -# ------------------------------------------------------------------------------ -# Installs Adminer (Debian/Ubuntu via APT, Alpine via direct download). -# -# Description: -# - Adds Adminer to Apache or web root -# - Supports Alpine and Debian-based systems -# ------------------------------------------------------------------------------ - -function setup_adminer() { - if grep -qi alpine /etc/os-release; then - msg_info "Setup Adminer (Alpine)" - mkdir -p /var/www/localhost/htdocs/adminer - if ! curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ - -o /var/www/localhost/htdocs/adminer/index.php; then - msg_error "Failed to download Adminer" - return 1 - fi - msg_ok "Adminer available at /adminer (Alpine)" - else - msg_info "Setup Adminer (Debian/Ubuntu)" - $STD apt-get install -y adminer - $STD a2enconf adminer - $STD systemctl reload apache2 - msg_ok "Adminer available at /adminer (Debian/Ubuntu)" - fi -} - -# ------------------------------------------------------------------------------ -# Installs or updates yq (mikefarah/yq - Go version). -# -# Description: -# - Checks if yq is installed and from correct source -# - Compares with latest release on GitHub -# - Updates if outdated or wrong implementation -# ------------------------------------------------------------------------------ - -function setup_yq() { - local TMP_DIR - TMP_DIR=$(mktemp -d) - local CURRENT_VERSION="" - local BINARY_PATH="/usr/local/bin/yq" - local GITHUB_REPO="mikefarah/yq" - - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq || { - msg_error "Failed to install jq" - rm -rf "$TMP_DIR" - return 1 - } - fi - - if command -v yq &>/dev/null; then - if ! yq --version 2>&1 | grep -q 'mikefarah'; then - rm -f "$(command -v yq)" - else - CURRENT_VERSION=$(yq --version | awk '{print $NF}' | sed 's/^v//') - fi - fi - - local RELEASE_JSON - RELEASE_JSON=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") - local LATEST_VERSION - LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name' | sed 's/^v//') - - if [[ -z "$LATEST_VERSION" ]]; then - msg_error "Could not determine latest yq version from GitHub." - rm -rf "$TMP_DIR" - return 1 - fi - - if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$LATEST_VERSION" ]]; then - return - fi - - msg_info "Setup yq ($LATEST_VERSION)" - curl -fsSL "https://github.com/${GITHUB_REPO}/releases/download/v${LATEST_VERSION}/yq_linux_amd64" -o "$TMP_DIR/yq" - chmod +x "$TMP_DIR/yq" - mv "$TMP_DIR/yq" "$BINARY_PATH" - - if [[ ! -x "$BINARY_PATH" ]]; then - msg_error "Failed to install yq to $BINARY_PATH" - rm -rf "$TMP_DIR" - return 1 - fi - - rm -rf "$TMP_DIR" - hash -r - - local FINAL_VERSION - FINAL_VERSION=$("$BINARY_PATH" --version 2>/dev/null | awk '{print $NF}') - if [[ "$FINAL_VERSION" == "v$LATEST_VERSION" ]]; then - msg_ok "Setup yq ($LATEST_VERSION)" - else - msg_error "yq installation incomplete or version mismatch" - fi -} - -# ------------------------------------------------------------------------------ -# Installs ImageMagick 7 from source (Debian/Ubuntu only). -# -# Description: -# - Downloads the latest ImageMagick source tarball -# - Builds and installs ImageMagick to /usr/local -# - Configures dynamic linker (ldconfig) -# -# Notes: -# - Requires: build-essential, libtool, libjpeg-dev, libpng-dev, etc. -# ------------------------------------------------------------------------------ -function setup_imagemagick() { - local TMP_DIR - TMP_DIR=$(mktemp -d) - local VERSION="" - local BINARY_PATH="/usr/local/bin/magick" - - if command -v magick &>/dev/null; then - VERSION=$(magick -version | awk '/^Version/ {print $3}') - msg_ok "ImageMagick already installed ($VERSION)" - return 0 - fi - - msg_info "Setup ImageMagick (Patience)" - $STD apt-get update - $STD apt-get install -y \ - build-essential \ - libtool \ - libjpeg-dev \ - libpng-dev \ - libtiff-dev \ - libwebp-dev \ - libheif-dev \ - libde265-dev \ - libopenjp2-7-dev \ - libxml2-dev \ - liblcms2-dev \ - libfreetype6-dev \ - libraw-dev \ - libfftw3-dev \ - liblqr-1-0-dev \ - libgsl-dev \ - pkg-config \ - ghostscript - - curl -fsSL https://imagemagick.org/archive/ImageMagick.tar.gz -o "$TMP_DIR/ImageMagick.tar.gz" - tar -xzf "$TMP_DIR/ImageMagick.tar.gz" -C "$TMP_DIR" - cd "$TMP_DIR"/ImageMagick-* || { - msg_error "Source extraction failed" - rm -rf "$TMP_DIR" - return 1 - } - - ./configure --disable-static >/dev/null - $STD make - $STD make install - $STD ldconfig /usr/local/lib - - if [[ ! -x "$BINARY_PATH" ]]; then - msg_error "ImageMagick installation failed" - rm -rf "$TMP_DIR" - return 1 - fi - - VERSION=$("$BINARY_PATH" -version | awk '/^Version/ {print $3}') - rm -rf "$TMP_DIR" - ensure_usr_local_bin_persist - msg_ok "Setup ImageMagick $VERSION" -} - -# ------------------------------------------------------------------------------ -# Installs FFmpeg from source or prebuilt binary (Debian/Ubuntu only). -# -# Description: -# - Downloads and builds FFmpeg from GitHub (https://github.com/FFmpeg/FFmpeg) -# - Supports specific version override via FFMPEG_VERSION (e.g. n7.1.1) -# - Supports build profile via FFMPEG_TYPE: -# - minimal : x264, vpx, mp3 only -# - medium : adds subtitles, fonts, opus, vorbis -# - full : adds dav1d, svt-av1, zlib, numa -# - binary : downloads static build (johnvansickle.com) -# - Defaults to latest stable version and full feature set -# -# Notes: -# - Requires: curl, jq, build-essential, and matching codec libraries -# - Result is installed to /usr/local/bin/ffmpeg -# ------------------------------------------------------------------------------ - -function setup_ffmpeg() { - local TMP_DIR - TMP_DIR=$(mktemp -d) - local GITHUB_REPO="FFmpeg/FFmpeg" - local VERSION="${FFMPEG_VERSION:-latest}" - local TYPE="${FFMPEG_TYPE:-full}" - local BIN_PATH="/usr/local/bin/ffmpeg" - - # Binary fallback mode - if [[ "$TYPE" == "binary" ]]; then - msg_info "Installing FFmpeg (static binary)" - curl -fsSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz -o "$TMP_DIR/ffmpeg.tar.xz" - tar -xf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" - local EXTRACTED_DIR - EXTRACTED_DIR=$(find "$TMP_DIR" -maxdepth 1 -type d -name "ffmpeg-*") - cp "$EXTRACTED_DIR/ffmpeg" "$BIN_PATH" - cp "$EXTRACTED_DIR/ffprobe" /usr/local/bin/ffprobe - chmod +x "$BIN_PATH" /usr/local/bin/ffprobe - rm -rf "$TMP_DIR" - msg_ok "Installed FFmpeg binary ($($BIN_PATH -version | head -n1))" - return - fi - - if ! command -v jq &>/dev/null; then - $STD apt-get update - $STD apt-get install -y jq - fi - - # Auto-detect latest stable version if none specified - if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then - msg_info "Resolving latest FFmpeg tag" - VERSION=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/tags" | - jq -r '.[].name' | - grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' | - sort -V | tail -n1) - fi - - if [[ -z "$VERSION" ]]; then - msg_error "Could not determine FFmpeg version" - rm -rf "$TMP_DIR" - return 1 - fi - - msg_info "Installing FFmpeg ${VERSION} ($TYPE)" - - # Dependency selection - local DEPS=(build-essential yasm nasm pkg-config) - case "$TYPE" in - minimal) - DEPS+=(libx264-dev libvpx-dev libmp3lame-dev) - ;; - medium) - DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev) - ;; - full) - DEPS+=( - libx264-dev libx265-dev libvpx-dev libmp3lame-dev - libfreetype6-dev libass-dev libopus-dev libvorbis-dev - libdav1d-dev libsvtav1-dev zlib1g-dev libnuma-dev - ) - ;; - *) - msg_error "Invalid FFMPEG_TYPE: $TYPE" - rm -rf "$TMP_DIR" - return 1 - ;; - esac - - $STD apt-get update - $STD apt-get install -y "${DEPS[@]}" - - curl -fsSL "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" -o "$TMP_DIR/ffmpeg.tar.gz" - tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" - cd "$TMP_DIR/FFmpeg-"* || { - msg_error "Source extraction failed" - rm -rf "$TMP_DIR" - return 1 - } - - local args=( - --enable-gpl - --enable-shared - --enable-nonfree - --disable-static - --enable-libx264 - --enable-libvpx - --enable-libmp3lame - ) - - if [[ "$TYPE" != "minimal" ]]; then - args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis) - fi - - if [[ "$TYPE" == "full" ]]; then - args+=(--enable-libx265 --enable-libdav1d --enable-zlib) - fi - - if [[ ${#args[@]} -eq 0 ]]; then - msg_error "FFmpeg configure args array is empty – aborting." - rm -rf "$TMP_DIR" - return 1 - fi - - ./configure "${args[@]}" >"$TMP_DIR/configure.log" 2>&1 || { - msg_error "FFmpeg ./configure failed (see $TMP_DIR/configure.log)" - cat "$TMP_DIR/configure.log" | tail -n 20 - rm -rf "$TMP_DIR" - return 1 - } - - $STD make -j"$(nproc)" - $STD make install - echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf - ldconfig - - ldconfig -p | grep libavdevice >/dev/null || { - msg_error "libavdevice not registered with dynamic linker" - return 1 - } - - if ! command -v ffmpeg &>/dev/null; then - msg_error "FFmpeg installation failed" - rm -rf "$TMP_DIR" - return 1 - fi - - local FINAL_VERSION - FINAL_VERSION=$(ffmpeg -version | head -n1 | awk '{print $3}') - rm -rf "$TMP_DIR" - ensure_usr_local_bin_persist - msg_ok "Setup FFmpeg $FINAL_VERSION" -} - -# ------------------------------------------------------------------------------ -# Installs ClickHouse server and client, sets up DB/user with credentials. -# -# Description: -# - Adds official ClickHouse APT repo with GPG key -# - Installs clickhouse-server and clickhouse-client -# - Creates database and user (credentials optionally overrideable via env) -# -# Variables: -# CLICKHOUSE_DB - Database name (default: analytics) -# CLICKHOUSE_USER - Username (default: analytics_user) -# CLICKHOUSE_PASS - Password (default: auto-generated) -# ------------------------------------------------------------------------------ - -function setup_clickhouse() { - local CLICKHOUSE_DB="${CLICKHOUSE_DB:-analytics}" - local CLICKHOUSE_USER="${CLICKHOUSE_USER:-analytics_user}" - local CLICKHOUSE_PASS="${CLICKHOUSE_PASS:-$(openssl rand -base64 18 | cut -c1-13)}" - local GPG_URL="https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key" - local GPG_KEY_PATH="/usr/share/keyrings/clickhouse-keyring.gpg" - local ARCH - ARCH=$(dpkg --print-architecture) - - if ! command -v clickhouse >/dev/null; then - msg_info "Setup ClickHouse" - - if ! curl -fsSL --connect-timeout 5 https://packages.clickhouse.com >/dev/null 2>&1; then - msg_error "Connection to packages.clickhouse.com:443 failed – possibly blocked" - echo "💡 Check AdGuard/Pi-hole or firewall rules" - return 1 - fi - - if ! curl -fsSL --retry 3 --connect-timeout 10 "$GPG_URL" | - gpg --dearmor -o "$GPG_KEY_PATH"; then - msg_error "Failed to fetch ClickHouse GPG key" - return 1 - fi - - echo "deb [signed-by=$GPG_KEY_PATH arch=$ARCH] https://packages.clickhouse.com/deb stable main" \ - >/etc/apt/sources.list.d/clickhouse.list - - env -u CLICKHOUSE_USER $STD apt-get update - env -u CLICKHOUSE_USER DEBIAN_FRONTEND=noninteractive $STD apt-get install -y clickhouse-server clickhouse-client - - $STD systemctl enable --now clickhouse-server - - msg_info "Waiting for ClickHouse to be ready" - for i in {1..10}; do - if clickhouse client --query "SELECT 1" &>/dev/null; then break; fi - sleep 1 - done - - # User anlegen - clickhouse client --query "CREATE DATABASE IF NOT EXISTS $CLICKHOUSE_DB" - clickhouse client --query "CREATE USER IF NOT EXISTS $CLICKHOUSE_USER IDENTIFIED WITH plaintext_password BY '$CLICKHOUSE_PASS'" - clickhouse client --query "GRANT ALL ON $CLICKHOUSE_DB.* TO $CLICKHOUSE_USER" - - # Default-User ggf. deaktivieren - cat </etc/clickhouse-server/users.d/disable-default.xml - - - - - -EOF - systemctl restart clickhouse-server - - msg_ok "Setup ClickHouse (DB: $CLICKHOUSE_DB, User: $CLICKHOUSE_USER)" - - { - echo "ClickHouse DB: $CLICKHOUSE_DB" - echo "ClickHouse User: $CLICKHOUSE_USER" - echo "ClickHouse Pass: $CLICKHOUSE_PASS" - } >>~/clickhouse.creds - else - msg_info "Updating ClickHouse packages" - env -u CLICKHOUSE_USER $STD apt-get update - env -u CLICKHOUSE_USER $STD apt-get install -y --only-upgrade clickhouse-server clickhouse-client - msg_ok "ClickHouse updated" - fi -} - -# ------------------------------------------------------------------------------ -# Checks for new GitHub release (latest tag). -# -# Description: -# - Queries the GitHub API for the latest release tag -# - Compares it to a local cached version (~/.) -# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0 -# -# Usage: -# check_for_gh_release "AppName" "user/repo" -# if [[ $? -eq 0 ]]; then -# echo "New version available: $CHECK_UPDATE_RELEASE" -# # trigger update... -# fi -# -# Notes: -# - Requires `jq` (auto-installed if missing) -# - Does not modify anything, only checks version state -# - Does not support pre-releases -# ------------------------------------------------------------------------------ -check_for_gh_release() { - local app="$1" - local source="$2" - local pinned_version="${3:-}" # optional - local current_file="$HOME/.${app,,}" - - msg_info "Check for update: ${app}" - - # DNS check for GitHub - if ! getent hosts api.github.com >/dev/null 2>&1; then - msg_error "Network error: cannot resolve api.github.com" - return 1 - fi - - # jq check - if ! command -v jq &>/dev/null; then - apt-get update -qq &>/dev/null && apt-get install -y jq &>/dev/null || { - msg_error "Failed to install jq" - return 1 - } - fi - - # get latest release - local release - release=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | - jq -r '.tag_name' | sed 's/^v//') - - if [[ -z "$release" ]]; then - msg_error "Unable to determine latest release for ${app}" - return 1 - fi - - local current="" - [[ -f "$current_file" ]] && current=$(<"$current_file") - - # PINNED Releases - if [[ -n "$pinned_version" ]]; then - if [[ "$pinned_version" == "$release" ]]; then - msg_ok "${app} pinned to v${pinned_version} (no update needed)" - return 1 - else - if [[ "$current" == "$pinned_version" ]]; then - msg_ok "${app} pinned to v${pinned_version} (already installed, upstream v${release})" - return 1 - fi - msg_info "${app} pinned to v${pinned_version} (upstream v${release}) → update/downgrade required" - CHECK_UPDATE_RELEASE="$pinned_version" - return 0 - fi - fi - - if [[ "$release" != "$current" ]] || [[ ! -f "$current_file" ]]; then - CHECK_UPDATE_RELEASE="$release" - msg_info "New release available: v${release} (current: v${current:-none})" - return 0 - else - msg_ok "${app} is up to date (v${release})" - return 1 - fi -} - -# ------------------------------------------------------------------------------ -# Hardware acceleration setup inside container -# Works with: Debian 12 (bookworm), Debian 13 (trixie), Ubuntu 24.04 (noble) -# Usage: hwaccel_setup_in_ct [--nonfree-intel] -# ------------------------------------------------------------------------------ - -hwaccel_setup_in_ct() { - local CTTYPE="$1" NONFREE=0 - [[ "$2" == "--nonfree-intel" ]] && NONFREE=1 - - # Detect OS info inside the container - local ID VERSION_CODENAME - if [[ -r /etc/os-release ]]; then - . /etc/os-release - fi - ID="${ID:-debian}" - VERSION_CODENAME="${VERSION_CODENAME:-bookworm}" - - msg_info "Setting up hardware acceleration for ${ID^} ($VERSION_CODENAME)" - - case "$ID" in - debian | ubuntu) - if ((NONFREE)) && [[ "$VERSION_CODENAME" =~ (trixie|noble) ]]; then - # Debian 13 / Ubuntu 24.04 → non-free Intel driver - cat >/etc/apt/sources.list.d/non-free.sources <<'SRC' -Types: deb deb-src -URIs: http://deb.debian.org/debian -Suites: trixie -Components: non-free non-free-firmware - -Types: deb deb-src -URIs: http://deb.debian.org/debian-security -Suites: trixie-security -Components: non-free non-free-firmware - -Types: deb deb-src -URIs: http://deb.debian.org/debian -Suites: trixie-updates -Components: non-free non-free-firmware -SRC - - $STD apt-get update - $STD apt-get install -y \ - intel-media-va-driver-non-free \ - ocl-icd-libopencl1 \ - mesa-opencl-icd \ - mesa-va-drivers \ - libvpl2 \ - vainfo \ - intel-gpu-tools - else - # Debian 12 (bookworm) and fallback for Debian 13/Ubuntu 24.04 without non-free - $STD apt-get update - $STD apt-get install -y \ - va-driver-all \ - ocl-icd-libopencl1 \ - mesa-opencl-icd \ - mesa-va-drivers \ - vainfo \ - intel-gpu-tools - fi - ;; - *) - msg_warn "Unsupported distribution ($ID $VERSION_CODENAME) – skipping HW accel setup" - return 0 - ;; - esac - - # Add current user to video/render groups (only for privileged CTs) - if [[ "$CTTYPE" == "0" ]]; then - $STD adduser "$(id -un)" video || true - $STD adduser "$(id -un)" render || true - fi - - msg_ok "Hardware acceleration is ready" -} diff --git a/bak/core_ref/newtemplateavailable.png b/bak/core_ref/newtemplateavailable.png deleted file mode 100644 index 14ca28639..000000000 Binary files a/bak/core_ref/newtemplateavailable.png and /dev/null differ diff --git a/bak/core_ref/pve-container-upgrader.png b/bak/core_ref/pve-container-upgrader.png deleted file mode 100644 index efabf8a81..000000000 Binary files a/bak/core_ref/pve-container-upgrader.png and /dev/null differ diff --git a/bak/deferred/build.func.backup-20251029-123804 b/bak/deferred/build.func.backup-20251029-123804 deleted file mode 100644 index 3bd10337c..000000000 --- a/bak/deferred/build.func.backup-20251029-123804 +++ /dev/null @@ -1,3516 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 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 -# - Get Proxmox VE version and kernel version -# ------------------------------------------------------------------------------ -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} - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1) - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) -} - -# ----------------------------------------------------------------------------- -# 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - 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() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - msg_ok "Storage configuration saved to $(basename "$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 -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ------------------------------------------------------------------------------ -# 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) - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "My Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - if [ -z "$CHOICE" ]; then - - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - ;; - 2 | advanced | ADVANCED) - header_info - echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - ;; - 3 | mydefaults | MYDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - defaults_target="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - "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}" -} - -# ------------------------------------------------------------------------------ -# 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_info "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_info "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_info "Detected NVIDIA GPU" - if ! check_nvidia_host_setup; then - msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough." - msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually." - return 0 - fi - - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found" - msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver" - else - if [[ "$CT_TYPE" == "0" ]]; then - cat <>"$LXC_CONFIG" - # NVIDIA GPU Passthrough (privileged) - lxc.cgroup2.devices.allow: c 195:* rwm - lxc.cgroup2.devices.allow: c 243:* rwm - lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file - lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file -EOF - - if [[ -e /dev/dri/renderD128 ]]; then - echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - - export GPU_TYPE="NVIDIA" - export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) - msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})" - else - msg_warn "NVIDIA passthrough only supported for privileged containers" - return 0 - fi - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_info "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_info "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # For Proxmox WebUI visibility, add as dev0, dev1 etc. - for dev in "${devices[@]}"; do - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - use dev entries for WebUI visibility - # Use initial GID 104 (render) for renderD*, 44 (video) for card* - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - - # Also add cgroup allows for privileged containers - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - else - # Unprivileged container - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - fi - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver" - return 1 - fi - - for dev in "${NVIDIA_DEVICES[@]}"; do - # NVIDIA devices typically need different handling - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - dev_idx=$((dev_idx + 1)) - - if [[ "$CT_TYPE" == "0" ]]; then - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # 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 [[ -e /dev/apex_0 ]]; then - msg_info "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - for i in {1..20}; do - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - [ -n "$ip_in_lxc" ] && break - sleep 1 - done - - if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" - exit 1 - fi - - # Try to reach gateway - gw_ok=0 - for i in {1..10}; do - if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then - gw_ok=1 - break - fi - sleep 1 - done - - if [ "$gw_ok" -eq 1 ]; then - msg_ok "Network in LXC is reachable (IP $ip_in_lxc)" - else - msg_warn "Network reachable but gateway check failed" - fi - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Ermittle die tatsächlichen GIDs aus dem Container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Fallbacks wenn Gruppen nicht existieren - if [[ -z "$video_gid" ]]; then - # Versuche die video Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback - fi - - if [[ -z "$render_gid" ]]; then - # Versuche die render Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback - fi - - msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}" - - # Prüfe ob die GIDs von den Defaults abweichen - local need_update=0 - if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then - need_update=1 - fi - - if [[ $need_update -eq 1 ]]; then - msg_info "Updating device GIDs in container config" - - # Stoppe Container für Config-Update - pct stop "$CTID" >/dev/null 2>&1 - - # Update die dev Einträge mit korrekten GIDs - # Backup der Config - cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" - - # Parse und update jeden dev Eintrag - while IFS= read -r line; do - if [[ "$line" =~ ^dev[0-9]+: ]]; then - # Extract device path - local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') - local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') - - if [[ "$device_path" =~ renderD ]]; then - # RenderD device - use render GID - echo "${dev_num}: ${device_path},gid=${render_gid}" - elif [[ "$device_path" =~ card ]]; then - # Card device - use video GID - echo "${dev_num}: ${device_path},gid=${video_gid}" - else - # Keep original line - echo "$line" - fi - else - # Keep non-dev lines - echo "$line" - fi - done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" - - mv "${LXC_CONFIG}.new" "$LXC_CONFIG" - - # Starte Container wieder - pct start "$CTID" >/dev/null 2>&1 - sleep 3 - - msg_ok "Device GIDs updated successfully" - else - msg_ok "Device GIDs are already correct" - fi - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -# NVIDIA-spezific check on host -check_nvidia_host_setup() { - if ! command -v nvidia-smi >/dev/null 2>&1; then - msg_warn "NVIDIA GPU detected but nvidia-smi not found on host" - msg_warn "Please install NVIDIA drivers on host first." - #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run" - #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms" - #echo " 3. Verify: nvidia-smi" - return 1 - fi - - # check if nvidia-smi works - if ! nvidia-smi >/dev/null 2>&1; then - msg_warn "nvidia-smi installed but not working. Driver issue?" - return 1 - fi - - 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 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #echo "[DEBUG] Online templates:" - for tmpl in "${ONLINE_TEMPLATES[@]}"; do - echo " - $tmpl" - done - fi - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_info "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_info "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_info "Please check:" - msg_info " - Is pveam catalog available? (run: pveam available -section system)" - msg_info " - Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - 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 diff --git a/bak/deferred/build.func.backup-20251029-124205 b/bak/deferred/build.func.backup-20251029-124205 deleted file mode 100644 index 919b0001a..000000000 --- a/bak/deferred/build.func.backup-20251029-124205 +++ /dev/null @@ -1,3516 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Revision: 1 - -# ============================================================================== -# SECTION 1: CORE INITIALIZATION & VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# 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 -# - Get Proxmox VE version and kernel version -# ------------------------------------------------------------------------------ -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} - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1) - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) -} - -# ----------------------------------------------------------------------------- -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION}${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - 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 "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - 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 - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .vars file from current advanced settings -_build_current_app_vars_tmp() { - tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac - - # IPv6 - _ipv6_method="${IPV6_METHOD:-auto}" - _ipv6_static="" - _ipv6_gateway="" - if [ "$_ipv6_method" = "static" ]; then - _ipv6_static="${IPV6_ADDR:-}" - _ipv6_gateway="${IPV6_GATE:-}" - fi - - # MTU/VLAN/MAC - _mtu="" - _vlan="" - _mac="" - case "${MTU:-}" in - ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; - esac - case "${VLAN:-}" in - ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; - esac - case "${MAC:-}" in - ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; - esac - - # DNS / Searchdomain - _ns="" - _searchdomain="" - case "${NS:-}" in - -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; - esac - case "${SD:-}" in - -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; - esac - - # SSH / APT / Features - _ssh="${SSH:-no}" - _ssh_auth="${SSH_AUTHORIZED_KEY:-}" - _apt_cacher="${APT_CACHER:-}" - _apt_cacher_ip="${APT_CACHER_IP:-}" - _fuse="${ENABLE_FUSE:-no}" - _tun="${ENABLE_TUN:-no}" - _tags="${TAGS:-}" - _verbose="${VERBOSE:-no}" - - # Type / Resources / Identity - _unpriv="${CT_TYPE:-1}" - _cpu="${CORE_COUNT:-1}" - _ram="${RAM_SIZE:-1024}" - _disk="${DISK_SIZE:-4}" - _hostname="${HN:-$NSAPP}" - - # Storage - _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" - _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" - - { - echo "# App-specific defaults for ${APP} (${NSAPP})" - echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo - - echo "var_unprivileged=$(_sanitize_value "$_unpriv")" - echo "var_cpu=$(_sanitize_value "$_cpu")" - echo "var_ram=$(_sanitize_value "$_ram")" - echo "var_disk=$(_sanitize_value "$_disk")" - - [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" - [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" - [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" - [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" - [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" - [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" - [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" - - [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" - [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")" - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" - [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" - - [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" - [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" - - [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" - [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" - } >"$tmpf" - - echo "$tmpf" -} - -# ------------------------------------------------------------------------------ -# maybe_offer_save_app_defaults() -# -# - Called after advanced_settings() -# - Offers to save current values as app defaults if not existing -# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel -# ------------------------------------------------------------------------------ -maybe_offer_save_app_defaults() { - local app_vars_path - app_vars_path="$(get_app_defaults_path)" - - # always build from current settings - local new_tmp diff_tmp - new_tmp="$(_build_current_app_vars_tmp)" - diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" - - # 1) if no file → offer to create - if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then - mkdir -p "$(dirname "$app_vars_path")" - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Saved app defaults: ${app_vars_path}" - fi - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 2) if file exists → build diff - _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" - - # if no differences → do nothing - if grep -q "^(No differences)$" "$diff_tmp"; then - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 3) if file exists → show menu with default selection "Update Defaults" - local app_vars_file - app_vars_file="$(basename "$app_vars_path")" - - while true; do - local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "APP DEFAULTS – ${APP}" \ - --menu "Differences detected. What do you want to do?" 20 78 10 \ - "Update Defaults" "Write new values to ${app_vars_file}" \ - "Keep Current" "Keep existing defaults (no changes)" \ - "View Diff" "Show a detailed diff" \ - "Cancel" "Abort without changes" \ - --default-item "Update Defaults" \ - 3>&1 1>&2 2>&3)" || { sel="Cancel"; } - - case "$sel" in - "Update Defaults") - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Updated app defaults: ${app_vars_path}" - break - ;; - "Keep Current") - msg_info "Keeping current app defaults: ${app_vars_path}" - break - ;; - "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "Diff – ${APP}" \ - --scrolltext --textbox "$diff_tmp" 25 100 - ;; - "Cancel" | *) - msg_info "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - msg_ok "Storage configuration saved to $(basename "$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 -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ------------------------------------------------------------------------------ -# 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) - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "My Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - if [ -z "$CHOICE" ]; then - - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - ;; - 2 | advanced | ADVANCED) - header_info - echo -e "${TAB}${INFO} ProxmoxVE Version ${PVEVERSION} | Kernel: ${KERNEL_VERSION}${CL}" - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - ;; - 3 | mydefaults | MYDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - defaults_target="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - "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}" -} - -# ------------------------------------------------------------------------------ -# 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_info "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_info "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_info "Detected NVIDIA GPU" - if ! check_nvidia_host_setup; then - msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough." - msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually." - return 0 - fi - - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found" - msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver" - else - if [[ "$CT_TYPE" == "0" ]]; then - cat <>"$LXC_CONFIG" - # NVIDIA GPU Passthrough (privileged) - lxc.cgroup2.devices.allow: c 195:* rwm - lxc.cgroup2.devices.allow: c 243:* rwm - lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file - lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file -EOF - - if [[ -e /dev/dri/renderD128 ]]; then - echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - - export GPU_TYPE="NVIDIA" - export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) - msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})" - else - msg_warn "NVIDIA passthrough only supported for privileged containers" - return 0 - fi - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_info "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_info "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # For Proxmox WebUI visibility, add as dev0, dev1 etc. - for dev in "${devices[@]}"; do - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - use dev entries for WebUI visibility - # Use initial GID 104 (render) for renderD*, 44 (video) for card* - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - - # Also add cgroup allows for privileged containers - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - else - # Unprivileged container - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - fi - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver" - return 1 - fi - - for dev in "${NVIDIA_DEVICES[@]}"; do - # NVIDIA devices typically need different handling - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - dev_idx=$((dev_idx + 1)) - - if [[ "$CT_TYPE" == "0" ]]; then - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # 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 [[ -e /dev/apex_0 ]]; then - msg_info "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - for i in {1..20}; do - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - [ -n "$ip_in_lxc" ] && break - sleep 1 - done - - if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" - exit 1 - fi - - # Try to reach gateway - gw_ok=0 - for i in {1..10}; do - if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then - gw_ok=1 - break - fi - sleep 1 - done - - if [ "$gw_ok" -eq 1 ]; then - msg_ok "Network in LXC is reachable (IP $ip_in_lxc)" - else - msg_warn "Network reachable but gateway check failed" - fi - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Ermittle die tatsächlichen GIDs aus dem Container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Fallbacks wenn Gruppen nicht existieren - if [[ -z "$video_gid" ]]; then - # Versuche die video Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback - fi - - if [[ -z "$render_gid" ]]; then - # Versuche die render Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback - fi - - msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}" - - # Prüfe ob die GIDs von den Defaults abweichen - local need_update=0 - if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then - need_update=1 - fi - - if [[ $need_update -eq 1 ]]; then - msg_info "Updating device GIDs in container config" - - # Stoppe Container für Config-Update - pct stop "$CTID" >/dev/null 2>&1 - - # Update die dev Einträge mit korrekten GIDs - # Backup der Config - cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" - - # Parse und update jeden dev Eintrag - while IFS= read -r line; do - if [[ "$line" =~ ^dev[0-9]+: ]]; then - # Extract device path - local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') - local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') - - if [[ "$device_path" =~ renderD ]]; then - # RenderD device - use render GID - echo "${dev_num}: ${device_path},gid=${render_gid}" - elif [[ "$device_path" =~ card ]]; then - # Card device - use video GID - echo "${dev_num}: ${device_path},gid=${video_gid}" - else - # Keep original line - echo "$line" - fi - else - # Keep non-dev lines - echo "$line" - fi - done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" - - mv "${LXC_CONFIG}.new" "$LXC_CONFIG" - - # Starte Container wieder - pct start "$CTID" >/dev/null 2>&1 - sleep 3 - - msg_ok "Device GIDs updated successfully" - else - msg_ok "Device GIDs are already correct" - fi - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -# NVIDIA-spezific check on host -check_nvidia_host_setup() { - if ! command -v nvidia-smi >/dev/null 2>&1; then - msg_warn "NVIDIA GPU detected but nvidia-smi not found on host" - msg_warn "Please install NVIDIA drivers on host first." - #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run" - #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms" - #echo " 3. Verify: nvidia-smi" - return 1 - fi - - # check if nvidia-smi works - if ! nvidia-smi >/dev/null 2>&1; then - msg_warn "nvidia-smi installed but not working. Driver issue?" - return 1 - fi - - 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 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #echo "[DEBUG] Online templates:" - for tmpl in "${ONLINE_TEMPLATES[@]}"; do - echo " - $tmpl" - done - fi - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_info "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_info "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_info "Please check:" - msg_info " - Is pveam catalog available? (run: pveam available -section system)" - msg_info " - Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - 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 diff --git a/bak/deferred/build.func.backup-20251029-124307 b/bak/deferred/build.func.backup-20251029-124307 deleted file mode 100644 index 2d514c644..000000000 --- a/bak/deferred/build.func.backup-20251029-124307 +++ /dev/null @@ -1,3517 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Revision: 1 - -# ============================================================================== -# SECTION 1: CORE INITIALIZATION & VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# 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 -# - Get Proxmox VE version and kernel version -# ------------------------------------------------------------------------------ -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} - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1) - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) -} - -# ----------------------------------------------------------------------------- -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .vars file from current advanced settings -_build_current_app_vars_tmp() { - tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac - - # IPv6 - _ipv6_method="${IPV6_METHOD:-auto}" - _ipv6_static="" - _ipv6_gateway="" - if [ "$_ipv6_method" = "static" ]; then - _ipv6_static="${IPV6_ADDR:-}" - _ipv6_gateway="${IPV6_GATE:-}" - fi - - # MTU/VLAN/MAC - _mtu="" - _vlan="" - _mac="" - case "${MTU:-}" in - ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; - esac - case "${VLAN:-}" in - ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; - esac - case "${MAC:-}" in - ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; - esac - - # DNS / Searchdomain - _ns="" - _searchdomain="" - case "${NS:-}" in - -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; - esac - case "${SD:-}" in - -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; - esac - - # SSH / APT / Features - _ssh="${SSH:-no}" - _ssh_auth="${SSH_AUTHORIZED_KEY:-}" - _apt_cacher="${APT_CACHER:-}" - _apt_cacher_ip="${APT_CACHER_IP:-}" - _fuse="${ENABLE_FUSE:-no}" - _tun="${ENABLE_TUN:-no}" - _tags="${TAGS:-}" - _verbose="${VERBOSE:-no}" - - # Type / Resources / Identity - _unpriv="${CT_TYPE:-1}" - _cpu="${CORE_COUNT:-1}" - _ram="${RAM_SIZE:-1024}" - _disk="${DISK_SIZE:-4}" - _hostname="${HN:-$NSAPP}" - - # Storage - _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" - _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" - - { - echo "# App-specific defaults for ${APP} (${NSAPP})" - echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo - - echo "var_unprivileged=$(_sanitize_value "$_unpriv")" - echo "var_cpu=$(_sanitize_value "$_cpu")" - echo "var_ram=$(_sanitize_value "$_ram")" - echo "var_disk=$(_sanitize_value "$_disk")" - - [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" - [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" - [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" - [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" - [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" - [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" - [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" - - [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" - [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")" - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" - [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" - - [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" - [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" - - [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" - [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" - } >"$tmpf" - - echo "$tmpf" -} - -# ------------------------------------------------------------------------------ -# maybe_offer_save_app_defaults() -# -# - Called after advanced_settings() -# - Offers to save current values as app defaults if not existing -# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel -# ------------------------------------------------------------------------------ -maybe_offer_save_app_defaults() { - local app_vars_path - app_vars_path="$(get_app_defaults_path)" - - # always build from current settings - local new_tmp diff_tmp - new_tmp="$(_build_current_app_vars_tmp)" - diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" - - # 1) if no file → offer to create - if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then - mkdir -p "$(dirname "$app_vars_path")" - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Saved app defaults: ${app_vars_path}" - fi - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 2) if file exists → build diff - _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" - - # if no differences → do nothing - if grep -q "^(No differences)$" "$diff_tmp"; then - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 3) if file exists → show menu with default selection "Update Defaults" - local app_vars_file - app_vars_file="$(basename "$app_vars_path")" - - while true; do - local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "APP DEFAULTS – ${APP}" \ - --menu "Differences detected. What do you want to do?" 20 78 10 \ - "Update Defaults" "Write new values to ${app_vars_file}" \ - "Keep Current" "Keep existing defaults (no changes)" \ - "View Diff" "Show a detailed diff" \ - "Cancel" "Abort without changes" \ - --default-item "Update Defaults" \ - 3>&1 1>&2 2>&3)" || { sel="Cancel"; } - - case "$sel" in - "Update Defaults") - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Updated app defaults: ${app_vars_path}" - break - ;; - "Keep Current") - msg_info "Keeping current app defaults: ${app_vars_path}" - break - ;; - "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "Diff – ${APP}" \ - --scrolltext --textbox "$diff_tmp" 25 100 - ;; - "Cancel" | *) - msg_info "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - msg_ok "Storage configuration saved to $(basename "$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 -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ------------------------------------------------------------------------------ -# 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) - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "My Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - if [ -z "$CHOICE" ]; then - - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - ;; - 2 | advanced | ADVANCED) - header_info - - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - ;; - 3 | mydefaults | MYDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - defaults_target="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - "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}" -} - -# ------------------------------------------------------------------------------ -# 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_info "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_info "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_info "Detected NVIDIA GPU" - if ! check_nvidia_host_setup; then - msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough." - msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually." - return 0 - fi - - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found" - msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver" - else - if [[ "$CT_TYPE" == "0" ]]; then - cat <>"$LXC_CONFIG" - # NVIDIA GPU Passthrough (privileged) - lxc.cgroup2.devices.allow: c 195:* rwm - lxc.cgroup2.devices.allow: c 243:* rwm - lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file - lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file -EOF - - if [[ -e /dev/dri/renderD128 ]]; then - echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - - export GPU_TYPE="NVIDIA" - export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) - msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})" - else - msg_warn "NVIDIA passthrough only supported for privileged containers" - return 0 - fi - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_info "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_info "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # For Proxmox WebUI visibility, add as dev0, dev1 etc. - for dev in "${devices[@]}"; do - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - use dev entries for WebUI visibility - # Use initial GID 104 (render) for renderD*, 44 (video) for card* - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - - # Also add cgroup allows for privileged containers - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - else - # Unprivileged container - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - fi - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver" - return 1 - fi - - for dev in "${NVIDIA_DEVICES[@]}"; do - # NVIDIA devices typically need different handling - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - dev_idx=$((dev_idx + 1)) - - if [[ "$CT_TYPE" == "0" ]]; then - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # 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 [[ -e /dev/apex_0 ]]; then - msg_info "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - for i in {1..20}; do - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - [ -n "$ip_in_lxc" ] && break - sleep 1 - done - - if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" - exit 1 - fi - - # Try to reach gateway - gw_ok=0 - for i in {1..10}; do - if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then - gw_ok=1 - break - fi - sleep 1 - done - - if [ "$gw_ok" -eq 1 ]; then - msg_ok "Network in LXC is reachable (IP $ip_in_lxc)" - else - msg_warn "Network reachable but gateway check failed" - fi - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Ermittle die tatsächlichen GIDs aus dem Container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Fallbacks wenn Gruppen nicht existieren - if [[ -z "$video_gid" ]]; then - # Versuche die video Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback - fi - - if [[ -z "$render_gid" ]]; then - # Versuche die render Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback - fi - - msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}" - - # Prüfe ob die GIDs von den Defaults abweichen - local need_update=0 - if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then - need_update=1 - fi - - if [[ $need_update -eq 1 ]]; then - msg_info "Updating device GIDs in container config" - - # Stoppe Container für Config-Update - pct stop "$CTID" >/dev/null 2>&1 - - # Update die dev Einträge mit korrekten GIDs - # Backup der Config - cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" - - # Parse und update jeden dev Eintrag - while IFS= read -r line; do - if [[ "$line" =~ ^dev[0-9]+: ]]; then - # Extract device path - local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') - local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') - - if [[ "$device_path" =~ renderD ]]; then - # RenderD device - use render GID - echo "${dev_num}: ${device_path},gid=${render_gid}" - elif [[ "$device_path" =~ card ]]; then - # Card device - use video GID - echo "${dev_num}: ${device_path},gid=${video_gid}" - else - # Keep original line - echo "$line" - fi - else - # Keep non-dev lines - echo "$line" - fi - done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" - - mv "${LXC_CONFIG}.new" "$LXC_CONFIG" - - # Starte Container wieder - pct start "$CTID" >/dev/null 2>&1 - sleep 3 - - msg_ok "Device GIDs updated successfully" - else - msg_ok "Device GIDs are already correct" - fi - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -# NVIDIA-spezific check on host -check_nvidia_host_setup() { - if ! command -v nvidia-smi >/dev/null 2>&1; then - msg_warn "NVIDIA GPU detected but nvidia-smi not found on host" - msg_warn "Please install NVIDIA drivers on host first." - #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run" - #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms" - #echo " 3. Verify: nvidia-smi" - return 1 - fi - - # check if nvidia-smi works - if ! nvidia-smi >/dev/null 2>&1; then - msg_warn "nvidia-smi installed but not working. Driver issue?" - return 1 - fi - - 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 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #echo "[DEBUG] Online templates:" - for tmpl in "${ONLINE_TEMPLATES[@]}"; do - echo " - $tmpl" - done - fi - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_info "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_info "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_info "Please check:" - msg_info " - Is pveam catalog available? (run: pveam available -section system)" - msg_info " - Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - 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 diff --git a/bak/deferred/build.func.backup-20251029-124334 b/bak/deferred/build.func.backup-20251029-124334 deleted file mode 100644 index 2d514c644..000000000 --- a/bak/deferred/build.func.backup-20251029-124334 +++ /dev/null @@ -1,3517 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Revision: 1 - -# ============================================================================== -# SECTION 1: CORE INITIALIZATION & VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# 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 -# - Get Proxmox VE version and kernel version -# ------------------------------------------------------------------------------ -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} - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1) - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) -} - -# ----------------------------------------------------------------------------- -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .vars file from current advanced settings -_build_current_app_vars_tmp() { - tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac - - # IPv6 - _ipv6_method="${IPV6_METHOD:-auto}" - _ipv6_static="" - _ipv6_gateway="" - if [ "$_ipv6_method" = "static" ]; then - _ipv6_static="${IPV6_ADDR:-}" - _ipv6_gateway="${IPV6_GATE:-}" - fi - - # MTU/VLAN/MAC - _mtu="" - _vlan="" - _mac="" - case "${MTU:-}" in - ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; - esac - case "${VLAN:-}" in - ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; - esac - case "${MAC:-}" in - ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; - esac - - # DNS / Searchdomain - _ns="" - _searchdomain="" - case "${NS:-}" in - -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; - esac - case "${SD:-}" in - -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; - esac - - # SSH / APT / Features - _ssh="${SSH:-no}" - _ssh_auth="${SSH_AUTHORIZED_KEY:-}" - _apt_cacher="${APT_CACHER:-}" - _apt_cacher_ip="${APT_CACHER_IP:-}" - _fuse="${ENABLE_FUSE:-no}" - _tun="${ENABLE_TUN:-no}" - _tags="${TAGS:-}" - _verbose="${VERBOSE:-no}" - - # Type / Resources / Identity - _unpriv="${CT_TYPE:-1}" - _cpu="${CORE_COUNT:-1}" - _ram="${RAM_SIZE:-1024}" - _disk="${DISK_SIZE:-4}" - _hostname="${HN:-$NSAPP}" - - # Storage - _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" - _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" - - { - echo "# App-specific defaults for ${APP} (${NSAPP})" - echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo - - echo "var_unprivileged=$(_sanitize_value "$_unpriv")" - echo "var_cpu=$(_sanitize_value "$_cpu")" - echo "var_ram=$(_sanitize_value "$_ram")" - echo "var_disk=$(_sanitize_value "$_disk")" - - [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" - [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" - [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" - [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" - [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" - [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" - [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" - - [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" - [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")" - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" - [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" - - [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" - [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" - - [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" - [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" - } >"$tmpf" - - echo "$tmpf" -} - -# ------------------------------------------------------------------------------ -# maybe_offer_save_app_defaults() -# -# - Called after advanced_settings() -# - Offers to save current values as app defaults if not existing -# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel -# ------------------------------------------------------------------------------ -maybe_offer_save_app_defaults() { - local app_vars_path - app_vars_path="$(get_app_defaults_path)" - - # always build from current settings - local new_tmp diff_tmp - new_tmp="$(_build_current_app_vars_tmp)" - diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" - - # 1) if no file → offer to create - if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then - mkdir -p "$(dirname "$app_vars_path")" - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Saved app defaults: ${app_vars_path}" - fi - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 2) if file exists → build diff - _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" - - # if no differences → do nothing - if grep -q "^(No differences)$" "$diff_tmp"; then - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 3) if file exists → show menu with default selection "Update Defaults" - local app_vars_file - app_vars_file="$(basename "$app_vars_path")" - - while true; do - local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "APP DEFAULTS – ${APP}" \ - --menu "Differences detected. What do you want to do?" 20 78 10 \ - "Update Defaults" "Write new values to ${app_vars_file}" \ - "Keep Current" "Keep existing defaults (no changes)" \ - "View Diff" "Show a detailed diff" \ - "Cancel" "Abort without changes" \ - --default-item "Update Defaults" \ - 3>&1 1>&2 2>&3)" || { sel="Cancel"; } - - case "$sel" in - "Update Defaults") - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Updated app defaults: ${app_vars_path}" - break - ;; - "Keep Current") - msg_info "Keeping current app defaults: ${app_vars_path}" - break - ;; - "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "Diff – ${APP}" \ - --scrolltext --textbox "$diff_tmp" 25 100 - ;; - "Cancel" | *) - msg_info "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - msg_ok "Storage configuration saved to $(basename "$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 -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ------------------------------------------------------------------------------ -# 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) - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "My Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - if [ -z "$CHOICE" ]; then - - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - ;; - 2 | advanced | ADVANCED) - header_info - - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - ;; - 3 | mydefaults | MYDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - defaults_target="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - "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}" -} - -# ------------------------------------------------------------------------------ -# 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_info "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_info "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_info "Detected NVIDIA GPU" - if ! check_nvidia_host_setup; then - msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough." - msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually." - return 0 - fi - - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found" - msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver" - else - if [[ "$CT_TYPE" == "0" ]]; then - cat <>"$LXC_CONFIG" - # NVIDIA GPU Passthrough (privileged) - lxc.cgroup2.devices.allow: c 195:* rwm - lxc.cgroup2.devices.allow: c 243:* rwm - lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file - lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file -EOF - - if [[ -e /dev/dri/renderD128 ]]; then - echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - - export GPU_TYPE="NVIDIA" - export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) - msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})" - else - msg_warn "NVIDIA passthrough only supported for privileged containers" - return 0 - fi - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_info "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_info "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # For Proxmox WebUI visibility, add as dev0, dev1 etc. - for dev in "${devices[@]}"; do - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - use dev entries for WebUI visibility - # Use initial GID 104 (render) for renderD*, 44 (video) for card* - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - - # Also add cgroup allows for privileged containers - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - else - # Unprivileged container - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - fi - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver" - return 1 - fi - - for dev in "${NVIDIA_DEVICES[@]}"; do - # NVIDIA devices typically need different handling - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - dev_idx=$((dev_idx + 1)) - - if [[ "$CT_TYPE" == "0" ]]; then - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # 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 [[ -e /dev/apex_0 ]]; then - msg_info "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - for i in {1..20}; do - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - [ -n "$ip_in_lxc" ] && break - sleep 1 - done - - if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" - exit 1 - fi - - # Try to reach gateway - gw_ok=0 - for i in {1..10}; do - if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then - gw_ok=1 - break - fi - sleep 1 - done - - if [ "$gw_ok" -eq 1 ]; then - msg_ok "Network in LXC is reachable (IP $ip_in_lxc)" - else - msg_warn "Network reachable but gateway check failed" - fi - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Ermittle die tatsächlichen GIDs aus dem Container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Fallbacks wenn Gruppen nicht existieren - if [[ -z "$video_gid" ]]; then - # Versuche die video Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback - fi - - if [[ -z "$render_gid" ]]; then - # Versuche die render Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback - fi - - msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}" - - # Prüfe ob die GIDs von den Defaults abweichen - local need_update=0 - if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then - need_update=1 - fi - - if [[ $need_update -eq 1 ]]; then - msg_info "Updating device GIDs in container config" - - # Stoppe Container für Config-Update - pct stop "$CTID" >/dev/null 2>&1 - - # Update die dev Einträge mit korrekten GIDs - # Backup der Config - cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" - - # Parse und update jeden dev Eintrag - while IFS= read -r line; do - if [[ "$line" =~ ^dev[0-9]+: ]]; then - # Extract device path - local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') - local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') - - if [[ "$device_path" =~ renderD ]]; then - # RenderD device - use render GID - echo "${dev_num}: ${device_path},gid=${render_gid}" - elif [[ "$device_path" =~ card ]]; then - # Card device - use video GID - echo "${dev_num}: ${device_path},gid=${video_gid}" - else - # Keep original line - echo "$line" - fi - else - # Keep non-dev lines - echo "$line" - fi - done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" - - mv "${LXC_CONFIG}.new" "$LXC_CONFIG" - - # Starte Container wieder - pct start "$CTID" >/dev/null 2>&1 - sleep 3 - - msg_ok "Device GIDs updated successfully" - else - msg_ok "Device GIDs are already correct" - fi - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -# NVIDIA-spezific check on host -check_nvidia_host_setup() { - if ! command -v nvidia-smi >/dev/null 2>&1; then - msg_warn "NVIDIA GPU detected but nvidia-smi not found on host" - msg_warn "Please install NVIDIA drivers on host first." - #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run" - #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms" - #echo " 3. Verify: nvidia-smi" - return 1 - fi - - # check if nvidia-smi works - if ! nvidia-smi >/dev/null 2>&1; then - msg_warn "nvidia-smi installed but not working. Driver issue?" - return 1 - fi - - 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 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #echo "[DEBUG] Online templates:" - for tmpl in "${ONLINE_TEMPLATES[@]}"; do - echo " - $tmpl" - done - fi - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_info "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_info "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_info "Please check:" - msg_info " - Is pveam catalog available? (run: pveam available -section system)" - msg_info " - Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - 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 diff --git a/bak/deferred/build.func.backup-refactoring-20251029-125644 b/bak/deferred/build.func.backup-refactoring-20251029-125644 deleted file mode 100644 index 2d514c644..000000000 --- a/bak/deferred/build.func.backup-refactoring-20251029-125644 +++ /dev/null @@ -1,3517 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 community-scripts ORG -# Author: tteck (tteckster) | MickLesk | michelroegl-brunner -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Revision: 1 - -# ============================================================================== -# SECTION 1: CORE INITIALIZATION & VARIABLES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# 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 -# - Get Proxmox VE version and kernel version -# ------------------------------------------------------------------------------ -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} - - # Get Proxmox VE version and kernel version - if command -v pveversion >/dev/null 2>&1; then - PVEVERSION=$(pveversion | grep "pve-manager" | awk '{print $2}' | cut -d'/' -f1) - else - PVEVERSION="N/A" - fi - KERNEL_VERSION=$(uname -r) -} - -# ----------------------------------------------------------------------------- -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - if [ "$VERBOSE" == "yes" ]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" - fi - echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" - echo -e " " -} - -# ------------------------------------------------------------------------------ -# 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_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 "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" - fi - else - exit_script - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - while true; do - if CT_NAME=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo "${CT_NAME,,}" | tr -d ' ') - fi - # Hostname validate (RFC 1123) - if [[ "$HN" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --msgbox "❌ Invalid hostname: '$HN'\n\nOnly lowercase letters, digits and hyphens (-) are allowed.\nUnderscores (_) or other characters are not permitted!" 10 70 - fi - else - exit_script - fi - done - - while true; do - DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - fi - - if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - break - else - whiptail --msgbox "Disk size must be a positive integer!" 8 58 - fi - done - - while true; do - CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - fi - - if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - break - else - whiptail --msgbox "CPU core count must be a positive integer!" 8 58 - fi - done - - while true; do - RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3) || exit_script - - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - fi - - if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - break - else - whiptail --msgbox "RAM size must be a positive integer!" 8 58 - fi - done - - IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f) - BRIDGES="" - OLD_IFS=$IFS - IFS=$'\n' - for iface_filepath in ${IFACE_FILEPATH_LIST}; do - - iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX') - (grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true - - if [ -f "${iface_indexes_tmpfile}" ]; then - - while read -r pair; do - start=$(echo "${pair}" | cut -d':' -f1) - end=$(echo "${pair}" | cut -d':' -f2) - - if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then - iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}') - BRIDGES="${iface_name}"$'\n'"${BRIDGES}" - fi - - done <"${iface_indexes_tmpfile}" - rm -f "${iface_indexes_tmpfile}" - fi - - done - IFS=$OLD_IFS - BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq) - if [[ -z "$BRIDGES" ]]; then - BRG="vmbr0" - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - # Build bridge menu with descriptions - BRIDGE_MENU_OPTIONS=() - while IFS= read -r bridge; do - if [[ -n "$bridge" ]]; then - # Get description from Proxmox built-in method - find comment for this specific bridge - description=$(grep -A 10 "iface $bridge" /etc/network/interfaces | grep '^#' | head -n1 | sed 's/^#\s*//') - if [[ -n "$description" ]]; then - BRIDGE_MENU_OPTIONS+=("$bridge" "${description}") - else - BRIDGE_MENU_OPTIONS+=("$bridge" " ") - fi - fi - done <<<"$BRIDGES" - - BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge: " 18 55 6 "${BRIDGE_MENU_OPTIONS[@]}" 3>&1 1>&2 2>&3) - if [[ -z "$BRG" ]]; then - exit_script - else - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - fi - fi - - # IPv4 methods: dhcp, static, none - while true; do - IPV4_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "IPv4 Address Management" \ - --menu "Select IPv4 Address Assignment Method:" 12 60 2 \ - "dhcp" "Automatic (DHCP, recommended)" \ - "static" "Static (manual entry)" \ - 3>&1 1>&2 2>&3) - - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit_script - fi - - case "$IPV4_METHOD" in - dhcp) - NET="dhcp" - GATE="" - echo -e "${NETWORK}${BOLD}${DGN}IPv4: DHCP${CL}" - break - ;; - static) - # Static: call and validate CIDR address - while true; do - NET=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Static IPv4 CIDR Address (e.g. 192.168.100.50/24)" 8 58 "" \ - --title "IPv4 ADDRESS" 3>&1 1>&2 2>&3) - if [ -z "$NET" ]; then - whiptail --msgbox "IPv4 address must not be empty." 8 58 - continue - elif [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv4 Address: ${BGN}$NET${CL}" - break - else - whiptail --msgbox "$NET is not a valid IPv4 CIDR address. Please enter a correct value!" 8 58 - fi - done - - # call and validate Gateway - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --inputbox "Enter Gateway IP address for static IPv4" 8 58 "" \ - --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --msgbox "Gateway IP address cannot be empty." 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --msgbox "Invalid Gateway IP address format." 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - break - ;; - esac - done - - # IPv6 Address Management selection - while true; do - IPV6_METHOD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --menu \ - "Select IPv6 Address Management Type:" 15 58 4 \ - "auto" "SLAAC/AUTO (recommended, default)" \ - "dhcp" "DHCPv6" \ - "static" "Static (manual entry)" \ - "none" "Disabled" \ - --default-item "auto" 3>&1 1>&2 2>&3) - [ $? -ne 0 ] && exit_script - - case "$IPV6_METHOD" in - auto) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}SLAAC/AUTO${CL}" - IPV6_ADDR="" - IPV6_GATE="" - break - ;; - dhcp) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}DHCPv6${CL}" - IPV6_ADDR="dhcp" - IPV6_GATE="" - break - ;; - static) - # Ask for static IPv6 address (CIDR notation, e.g., 2001:db8::1234/64) - while true; do - IPV6_ADDR=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Set a static IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 "" \ - --title "IPv6 STATIC ADDRESS" 3>&1 1>&2 2>&3) || exit_script - if [[ "$IPV6_ADDR" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}$IPV6_ADDR${CL}" - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "$IPV6_ADDR is an invalid IPv6 CIDR address. Please enter a valid IPv6 CIDR address (e.g., 2001:db8::1234/64)" 8 58 - fi - done - # Optional: ask for IPv6 gateway for static config - while true; do - IPV6_GATE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox \ - "Enter IPv6 gateway address (optional, leave blank for none)" 8 58 "" --title "IPv6 GATEWAY" 3>&1 1>&2 2>&3) - if [ -z "$IPV6_GATE" ]; then - IPV6_GATE="" - break - elif [[ "$IPV6_GATE" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$ ]]; then - break - else - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --msgbox \ - "Invalid IPv6 gateway format." 8 58 - fi - done - break - ;; - none) - echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}Disabled${CL}" - IPV6_ADDR="none" - IPV6_GATE="" - break - ;; - *) - exit_script - ;; - esac - done - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit_script - fi - fi - - # if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - # DISABLEIP6="yes" - # else - # DISABLEIP6="no" - # fi - # echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$MTU1" ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit_script - fi - - if SD=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z "$SD" ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" - else - exit_script - fi - - if NX=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z "$NX" ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" - else - exit_script - fi - - if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ "$NX" != "Host" ]; then - UDHCPC_FIX="yes" - else - UDHCPC_FIX="no" - fi - export UDHCPC_FIX - - if MAC1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z "$MAC1" ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit_script - fi - - if VLAN1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z "$VLAN1" ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" - else - exit_script - fi - - if ADV_TAGS=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then - if [ -n "${ADV_TAGS}" ]; then - ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') - TAGS="${ADV_TAGS}" - else - TAGS=";" - fi - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - else - exit_script - fi - - configure_ssh_settings - export SSH_KEYS_FILE - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - 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 "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - 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 - -# Note: _is_whitelisted_key() is defined above in default_var_settings section - -_sanitize_value() { - # Disallow Command-Substitution / Shell-Meta - case "$1" in - *'$('* | *'`'* | *';'* | *'&'* | *'<('*) - echo "" - return 0 - ;; - esac - echo "$1" -} - -# Map-Parser: read var_* from file into _VARS_IN associative array -# Note: Main _load_vars_file() with full validation is defined in default_var_settings section -# This simplified version is used specifically for diff operations via _VARS_IN array -declare -A _VARS_IN -_load_vars_file_to_map() { - local file="$1" - [ -f "$file" ] || return 0 - _VARS_IN=() # Clear array - local line key val - while IFS= read -r line || [ -n "$line" ]; do - line="${line#"${line%%[![:space:]]*}"}" - line="${line%"${line##*[![:space:]]}"}" - [ -z "$line" ] && continue - case "$line" in - \#*) continue ;; - esac - key=$(printf "%s" "$line" | cut -d= -f1) - val=$(printf "%s" "$line" | cut -d= -f2-) - case "$key" in - var_*) - if _is_whitelisted_key "$key"; then - _VARS_IN["$key"]="$val" - fi - ;; - esac - done <"$file" -} - -# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new) -_build_vars_diff() { - local oldf="$1" newf="$2" - local k - local -A OLD=() NEW=() - _load_vars_file_to_map "$oldf" - for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done - _load_vars_file_to_map "$newf" - for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done - - local out - out+="# Diff for ${APP} (${NSAPP})\n" - out+="# Old: ${oldf}\n# New: ${newf}\n\n" - - local found_change=0 - - # Changed & Removed - for k in "${!OLD[@]}"; do - if [[ -v NEW["$k"] ]]; then - if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then - out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - else - out+="- ${k}\n - old: ${OLD[$k]}\n" - found_change=1 - fi - done - - # Added - for k in "${!NEW[@]}"; do - if [[ ! -v OLD["$k"] ]]; then - out+="+ ${k}\n + new: ${NEW[$k]}\n" - found_change=1 - fi - done - - if [[ $found_change -eq 0 ]]; then - out+="(No differences)\n" - fi - - printf "%b" "$out" -} - -# Build a temporary .vars file from current advanced settings -_build_current_app_vars_tmp() { - tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)" - - # NET/GW - _net="${NET:-}" - _gate="" - case "${GATE:-}" in - ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;; - esac - - # IPv6 - _ipv6_method="${IPV6_METHOD:-auto}" - _ipv6_static="" - _ipv6_gateway="" - if [ "$_ipv6_method" = "static" ]; then - _ipv6_static="${IPV6_ADDR:-}" - _ipv6_gateway="${IPV6_GATE:-}" - fi - - # MTU/VLAN/MAC - _mtu="" - _vlan="" - _mac="" - case "${MTU:-}" in - ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;; - esac - case "${VLAN:-}" in - ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;; - esac - case "${MAC:-}" in - ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;; - esac - - # DNS / Searchdomain - _ns="" - _searchdomain="" - case "${NS:-}" in - -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;; - esac - case "${SD:-}" in - -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;; - esac - - # SSH / APT / Features - _ssh="${SSH:-no}" - _ssh_auth="${SSH_AUTHORIZED_KEY:-}" - _apt_cacher="${APT_CACHER:-}" - _apt_cacher_ip="${APT_CACHER_IP:-}" - _fuse="${ENABLE_FUSE:-no}" - _tun="${ENABLE_TUN:-no}" - _tags="${TAGS:-}" - _verbose="${VERBOSE:-no}" - - # Type / Resources / Identity - _unpriv="${CT_TYPE:-1}" - _cpu="${CORE_COUNT:-1}" - _ram="${RAM_SIZE:-1024}" - _disk="${DISK_SIZE:-4}" - _hostname="${HN:-$NSAPP}" - - # Storage - _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" - _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" - - { - echo "# App-specific defaults for ${APP} (${NSAPP})" - echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')" - echo - - echo "var_unprivileged=$(_sanitize_value "$_unpriv")" - echo "var_cpu=$(_sanitize_value "$_cpu")" - echo "var_ram=$(_sanitize_value "$_ram")" - echo "var_disk=$(_sanitize_value "$_disk")" - - [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")" - [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")" - [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")" - [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")" - [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")" - [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")" - [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")" - - [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")" - [ -n "$_ipv6_static" ] && echo "var_ipv6_static=$(_sanitize_value "$_ipv6_static")" - - [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")" - [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")" - - [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")" - [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")" - - [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" - [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" - [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")" - [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")" - - [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")" - [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")" - - [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")" - [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")" - } >"$tmpf" - - echo "$tmpf" -} - -# ------------------------------------------------------------------------------ -# maybe_offer_save_app_defaults() -# -# - Called after advanced_settings() -# - Offers to save current values as app defaults if not existing -# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel -# ------------------------------------------------------------------------------ -maybe_offer_save_app_defaults() { - local app_vars_path - app_vars_path="$(get_app_defaults_path)" - - # always build from current settings - local new_tmp diff_tmp - new_tmp="$(_build_current_app_vars_tmp)" - diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")" - - # 1) if no file → offer to create - if [[ ! -f "$app_vars_path" ]]; then - if whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then - mkdir -p "$(dirname "$app_vars_path")" - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Saved app defaults: ${app_vars_path}" - fi - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 2) if file exists → build diff - _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp" - - # if no differences → do nothing - if grep -q "^(No differences)$" "$diff_tmp"; then - rm -f "$new_tmp" "$diff_tmp" - return 0 - fi - - # 3) if file exists → show menu with default selection "Update Defaults" - local app_vars_file - app_vars_file="$(basename "$app_vars_path")" - - while true; do - local sel - sel="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "APP DEFAULTS – ${APP}" \ - --menu "Differences detected. What do you want to do?" 20 78 10 \ - "Update Defaults" "Write new values to ${app_vars_file}" \ - "Keep Current" "Keep existing defaults (no changes)" \ - "View Diff" "Show a detailed diff" \ - "Cancel" "Abort without changes" \ - --default-item "Update Defaults" \ - 3>&1 1>&2 2>&3)" || { sel="Cancel"; } - - case "$sel" in - "Update Defaults") - install -m 0644 "$new_tmp" "$app_vars_path" - msg_ok "Updated app defaults: ${app_vars_path}" - break - ;; - "Keep Current") - msg_info "Keeping current app defaults: ${app_vars_path}" - break - ;; - "View Diff") - whiptail --backtitle "[dev] Proxmox VE Helper Scripts" \ - --title "Diff – ${APP}" \ - --scrolltext --textbox "$diff_tmp" 25 100 - ;; - "Cancel" | *) - msg_info "Canceled. No changes to app defaults." - break - ;; - esac - done - - rm -f "$new_tmp" "$diff_tmp" -} - -ensure_storage_selection_for_vars_file() { - local vf="$1" - - # Read stored values (if any) - local tpl ct - tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2-) - ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2-) - - if [[ -n "$tpl" && -n "$ct" ]]; then - TEMPLATE_STORAGE="$tpl" - CONTAINER_STORAGE="$ct" - return 0 - fi - - choose_and_set_storage_for_file "$vf" template - choose_and_set_storage_for_file "$vf" container - - msg_ok "Storage configuration saved to $(basename "$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 -} - -ensure_global_default_vars_file() { - local vars_path="/usr/local/community-scripts/default.vars" - if [[ ! -f "$vars_path" ]]; then - mkdir -p "$(dirname "$vars_path")" - touch "$vars_path" - fi - echo "$vars_path" -} - -# ------------------------------------------------------------------------------ -# 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) - - # Show APP Header - header_info - - # --- Support CLI argument as direct preset (default, advanced, …) --- - CHOICE="${mode:-${1:-}}" - - # If no CLI argument → show whiptail menu - # Build menu dynamically based on available options - local appdefaults_option="" - local settings_option="" - local menu_items=( - "1" "Default Install" - "2" "Advanced Install" - "3" "My Defaults" - ) - - if [ -f "$(get_app_defaults_path)" ]; then - appdefaults_option="4" - menu_items+=("4" "App Defaults for ${APP}") - settings_option="5" - menu_items+=("5" "Settings") - else - settings_option="4" - menu_items+=("4" "Settings") - fi - - if [ -z "$CHOICE" ]; then - - TMP_CHOICE=$(whiptail \ - --backtitle "Proxmox VE Helper Scripts" \ - --title "Community-Scripts Options" \ - --ok-button "Select" --cancel-button "Exit Script" \ - --notags \ - --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \ - 20 60 9 \ - "${menu_items[@]}" \ - --default-item "1" \ - 3>&1 1>&2 2>&3) || exit_script - CHOICE="$TMP_CHOICE" - fi - - APPDEFAULTS_OPTION="$appdefaults_option" - SETTINGS_OPTION="$settings_option" - - # --- Main case --- - local defaults_target="" - local run_maybe_offer="no" - case "$CHOICE" in - 1 | default | DEFAULT) - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" - VERBOSE="no" - METHOD="default" - base_settings "$VERBOSE" - echo_default - defaults_target="$(ensure_global_default_vars_file)" - ;; - 2 | advanced | ADVANCED) - header_info - - echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" - METHOD="advanced" - base_settings - advanced_settings - defaults_target="$(ensure_global_default_vars_file)" - run_maybe_offer="yes" - ;; - 3 | mydefaults | MYDEFAULTS) - default_var_settings || { - msg_error "Failed to apply default.vars" - exit 1 - } - defaults_target="/usr/local/community-scripts/default.vars" - ;; - "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS) - if [ -f "$(get_app_defaults_path)" ]; then - header_info - echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}" - METHOD="appdefaults" - base_settings - _load_vars_file "$(get_app_defaults_path)" - echo_default - defaults_target="$(get_app_defaults_path)" - else - msg_error "No App Defaults available for ${APP}" - exit 1 - fi - ;; - "$SETTINGS_OPTION" | settings | SETTINGS) - settings_menu - defaults_target="" - ;; - *) - echo -e "${CROSS}${RD}Invalid option: $CHOICE${CL}" - exit 1 - ;; - esac - - if [[ -n "$defaults_target" ]]; then - ensure_storage_selection_for_vars_file "$defaults_target" - fi - - if [[ "$run_maybe_offer" == "yes" ]]; then - maybe_offer_save_app_defaults - fi -} - -edit_default_storage() { - local vf="/usr/local/community-scripts/default.vars" - - # Ensure file exists - if [[ ! -f "$vf" ]]; then - mkdir -p "$(dirname "$vf")" - touch "$vf" - fi - - # Let ensure_storage_selection_for_vars_file handle everything - ensure_storage_selection_for_vars_file "$vf" -} - -settings_menu() { - while true; do - local settings_items=( - "1" "Manage API-Diagnostic Setting" - "2" "Edit Default.vars" - "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}" -} - -# ------------------------------------------------------------------------------ -# 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[@]}" -} - -configure_ssh_settings() { - SSH_KEYS_FILE="$(mktemp)" - : >"$SSH_KEYS_FILE" - - IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0') - ssh_build_choices_from_files "${_def_files[@]}" - local default_key_count="$COUNT" - - local ssh_key_mode - if [[ "$default_key_count" -gt 0 ]]; then - ssh_key_mode=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "SSH KEY SOURCE" --menu \ - "Provision SSH keys for root:" 14 72 4 \ - "found" "Select from detected keys (${default_key_count})" \ - "manual" "Paste a single public key" \ - "folder" "Scan another folder (path or glob)" \ - "none" "No keys" 3>&1 1>&2 2>&3) || exit_script - else - ssh_key_mode=$(whiptail --backtitle "[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) - local selection - selection=$(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 $selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - ;; - manual) - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[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) - local glob_path - 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 - local folder_selection - folder_selection=$(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 $folder_selection; do - tag="${tag%\"}" - tag="${tag#\"}" - local line - line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2-) - [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE" - done - else - whiptail --backtitle "[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 - - if [[ -s "$SSH_KEYS_FILE" ]]; then - sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE" - printf '\n' >>"$SSH_KEYS_FILE" - fi - - if [[ -s "$SSH_KEYS_FILE" || "$PW" == -password* ]]; then - if (whiptail --backtitle "[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 -} - -# ------------------------------------------------------------------------------ -# 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" - "viseron" - ) - - # Check if app needs GPU - is_gpu_app() { - local app="${1,,}" - for gpu_app in "${GPU_APPS[@]}"; do - [[ "$app" == "${gpu_app,,}" ]] && return 0 - done - return 1 - } - - # Detect all available GPU devices - detect_gpu_devices() { - INTEL_DEVICES=() - AMD_DEVICES=() - NVIDIA_DEVICES=() - - # Store PCI info to avoid multiple calls - local pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D") - - # Check for Intel GPU - look for Intel vendor ID [8086] - if echo "$pci_vga_info" | grep -q "\[8086:"; then - msg_info "Detected Intel GPU" - if [[ -d /dev/dri ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && INTEL_DEVICES+=("$d") - done - fi - fi - - # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD) - if echo "$pci_vga_info" | grep -qE "\[1002:|\[1022:"; then - msg_info "Detected AMD GPU" - if [[ -d /dev/dri ]]; then - # Only add if not already claimed by Intel - if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then - for d in /dev/dri/renderD* /dev/dri/card*; do - [[ -e "$d" ]] && AMD_DEVICES+=("$d") - done - fi - fi - fi - - # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de] - if echo "$pci_vga_info" | grep -q "\[10de:"; then - msg_info "Detected NVIDIA GPU" - if ! check_nvidia_host_setup; then - msg_error "NVIDIA host setup incomplete. Skipping GPU passthrough." - msg_info "Fix NVIDIA drivers on host, then recreate container or passthrough manually." - return 0 - fi - - for d in /dev/nvidia* /dev/nvidiactl /dev/nvidia-modeset; do - [[ -e "$d" ]] && NVIDIA_DEVICES+=("$d") - done - - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_warn "NVIDIA GPU detected but no /dev/nvidia* devices found" - msg_warn "Please install NVIDIA drivers on host: apt install nvidia-driver" - else - if [[ "$CT_TYPE" == "0" ]]; then - cat <>"$LXC_CONFIG" - # NVIDIA GPU Passthrough (privileged) - lxc.cgroup2.devices.allow: c 195:* rwm - lxc.cgroup2.devices.allow: c 243:* rwm - lxc.mount.entry: /dev/nvidia0 dev/nvidia0 none bind,optional,create=file - lxc.mount.entry: /dev/nvidiactl dev/nvidiactl none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file - lxc.mount.entry: /dev/nvidia-uvm-tools dev/nvidia-uvm-tools none bind,optional,create=file -EOF - - if [[ -e /dev/dri/renderD128 ]]; then - echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - - export GPU_TYPE="NVIDIA" - export NVIDIA_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) - msg_ok "NVIDIA GPU passthrough configured (driver: ${NVIDIA_DRIVER_VERSION})" - else - msg_warn "NVIDIA passthrough only supported for privileged containers" - return 0 - fi - fi - fi - - # Debug output - msg_debug "Intel devices: ${INTEL_DEVICES[*]}" - msg_debug "AMD devices: ${AMD_DEVICES[*]}" - msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}" - } - - # Configure USB passthrough for privileged containers - configure_usb_passthrough() { - if [[ "$CT_TYPE" != "0" ]]; then - return 0 - fi - - msg_info "Configuring automatic USB passthrough (privileged container)" - cat <>"$LXC_CONFIG" -# Automatic USB passthrough (privileged container) -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -EOF - msg_ok "USB passthrough configured" - } - - # Configure GPU passthrough - configure_gpu_passthrough() { - # Skip if not a GPU app and not privileged - if [[ "$CT_TYPE" != "0" ]] && ! is_gpu_app "$APP"; then - return 0 - fi - - detect_gpu_devices - - # Count available GPU types - local gpu_count=0 - local available_gpus=() - - if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("INTEL") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("AMD") - gpu_count=$((gpu_count + 1)) - fi - - if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then - available_gpus+=("NVIDIA") - gpu_count=$((gpu_count + 1)) - fi - - if [[ $gpu_count -eq 0 ]]; then - msg_info "No GPU devices found for passthrough" - return 0 - fi - - local selected_gpu="" - - if [[ $gpu_count -eq 1 ]]; then - # Automatic selection for single GPU - selected_gpu="${available_gpus[0]}" - msg_info "Automatically configuring ${selected_gpu} GPU passthrough" - else - # Multiple GPUs - ask user - echo -e "\n${INFO} Multiple GPU types detected:" - for gpu in "${available_gpus[@]}"; do - echo " - $gpu" - done - read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu - selected_gpu="${selected_gpu^^}" - - # Validate selection - local valid=0 - for gpu in "${available_gpus[@]}"; do - [[ "$selected_gpu" == "$gpu" ]] && valid=1 - done - - if [[ $valid -eq 0 ]]; then - msg_warn "Invalid selection. Skipping GPU passthrough." - return 0 - fi - fi - - # Apply passthrough configuration based on selection - local dev_idx=0 - - case "$selected_gpu" in - INTEL | AMD) - local devices=() - [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}") - [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}") - - # For Proxmox WebUI visibility, add as dev0, dev1 etc. - for dev in "${devices[@]}"; do - if [[ "$CT_TYPE" == "0" ]]; then - # Privileged container - use dev entries for WebUI visibility - # Use initial GID 104 (render) for renderD*, 44 (video) for card* - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - - # Also add cgroup allows for privileged containers - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - else - # Unprivileged container - if [[ "$dev" =~ renderD ]]; then - echo "dev${dev_idx}: $dev,uid=0,gid=104" >>"$LXC_CONFIG" - else - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - fi - dev_idx=$((dev_idx + 1)) - fi - done - - export GPU_TYPE="$selected_gpu" - msg_ok "${selected_gpu} GPU passthrough configured (${dev_idx} devices)" - ;; - - NVIDIA) - if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then - msg_error "NVIDIA drivers not installed on host. Please install: apt install nvidia-driver" - return 1 - fi - - for dev in "${NVIDIA_DEVICES[@]}"; do - # NVIDIA devices typically need different handling - echo "dev${dev_idx}: $dev,uid=0,gid=44" >>"$LXC_CONFIG" - dev_idx=$((dev_idx + 1)) - - if [[ "$CT_TYPE" == "0" ]]; then - local major minor - major=$(stat -c '%t' "$dev" 2>/dev/null || echo "0") - minor=$(stat -c '%T' "$dev" 2>/dev/null || echo "0") - - if [[ "$major" != "0" && "$minor" != "0" ]]; then - echo "lxc.cgroup2.devices.allow: c $((0x$major)):$((0x$minor)) rwm" >>"$LXC_CONFIG" - fi - fi - done - - export GPU_TYPE="NVIDIA" - msg_ok "NVIDIA GPU passthrough configured (${dev_idx} devices)" - ;; - esac - } - - # Additional device passthrough - configure_additional_devices() { - # 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 [[ -e /dev/apex_0 ]]; then - msg_info "Detected Coral TPU - configuring passthrough" - echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG" - fi - } - - # Execute pre-start configurations - configure_usb_passthrough - configure_gpu_passthrough - configure_additional_devices - - # ============================================================================ - # START CONTAINER AND INSTALL USERLAND - # ============================================================================ - - msg_info "Starting LXC Container" - pct start "$CTID" - - # Wait for container to be running - for i in {1..10}; do - if pct status "$CTID" | grep -q "status: running"; then - msg_ok "Started LXC Container" - break - fi - sleep 1 - if [ "$i" -eq 10 ]; then - msg_error "LXC Container did not reach running state" - exit 1 - fi - done - - # Wait for network (skip for Alpine initially) - if [ "$var_os" != "alpine" ]; then - msg_info "Waiting for network in LXC container" - - # Wait for IP - for i in {1..20}; do - ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - [ -n "$ip_in_lxc" ] && break - sleep 1 - done - - if [ -z "$ip_in_lxc" ]; then - msg_error "No IP assigned to CT $CTID after 20s" - exit 1 - fi - - # Try to reach gateway - gw_ok=0 - for i in {1..10}; do - if pct exec "$CTID" -- ping -c1 -W1 "${GATEWAY:-8.8.8.8}" >/dev/null 2>&1; then - gw_ok=1 - break - fi - sleep 1 - done - - if [ "$gw_ok" -eq 1 ]; then - msg_ok "Network in LXC is reachable (IP $ip_in_lxc)" - else - msg_warn "Network reachable but gateway check failed" - fi - fi - # Function to get correct GID inside container - get_container_gid() { - local group="$1" - local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3) - echo "${gid:-44}" # Default to 44 if not found - } - - fix_gpu_gids - - # Continue with standard container setup - msg_info "Customizing LXC Container" - - # # Install GPU userland if configured - # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then - # install_gpu_userland "VAAPI" - # fi - - # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then - # install_gpu_userland "NVIDIA" - # fi - - # Continue with standard container setup - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </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 -} - -fix_gpu_gids() { - if [[ -z "${GPU_TYPE:-}" ]]; then - return 0 - fi - - msg_info "Detecting and setting correct GPU group IDs" - - # Ermittle die tatsächlichen GIDs aus dem Container - local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - - # Fallbacks wenn Gruppen nicht existieren - if [[ -z "$video_gid" ]]; then - # Versuche die video Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" - video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3") - [[ -z "$video_gid" ]] && video_gid="44" # Ultimate fallback - fi - - if [[ -z "$render_gid" ]]; then - # Versuche die render Gruppe zu erstellen - pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" - render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3") - [[ -z "$render_gid" ]] && render_gid="104" # Ultimate fallback - fi - - msg_info "Container GIDs detected - video:${video_gid}, render:${render_gid}" - - # Prüfe ob die GIDs von den Defaults abweichen - local need_update=0 - if [[ "$video_gid" != "44" ]] || [[ "$render_gid" != "104" ]]; then - need_update=1 - fi - - if [[ $need_update -eq 1 ]]; then - msg_info "Updating device GIDs in container config" - - # Stoppe Container für Config-Update - pct stop "$CTID" >/dev/null 2>&1 - - # Update die dev Einträge mit korrekten GIDs - # Backup der Config - cp "$LXC_CONFIG" "${LXC_CONFIG}.bak" - - # Parse und update jeden dev Eintrag - while IFS= read -r line; do - if [[ "$line" =~ ^dev[0-9]+: ]]; then - # Extract device path - local device_path=$(echo "$line" | sed -E 's/^dev[0-9]+: ([^,]+).*/\1/') - local dev_num=$(echo "$line" | sed -E 's/^(dev[0-9]+):.*/\1/') - - if [[ "$device_path" =~ renderD ]]; then - # RenderD device - use render GID - echo "${dev_num}: ${device_path},gid=${render_gid}" - elif [[ "$device_path" =~ card ]]; then - # Card device - use video GID - echo "${dev_num}: ${device_path},gid=${video_gid}" - else - # Keep original line - echo "$line" - fi - else - # Keep non-dev lines - echo "$line" - fi - done <"$LXC_CONFIG" >"${LXC_CONFIG}.new" - - mv "${LXC_CONFIG}.new" "$LXC_CONFIG" - - # Starte Container wieder - pct start "$CTID" >/dev/null 2>&1 - sleep 3 - - msg_ok "Device GIDs updated successfully" - else - msg_ok "Device GIDs are already correct" - fi - if [[ "$CT_TYPE" == "0" ]]; then - pct exec "$CTID" -- bash -c " - if [ -d /dev/dri ]; then - for dev in /dev/dri/*; do - if [ -e \"\$dev\" ]; then - if [[ \"\$dev\" =~ renderD ]]; then - chgrp ${render_gid} \"\$dev\" 2>/dev/null || true - else - chgrp ${video_gid} \"\$dev\" 2>/dev/null || true - fi - chmod 660 \"\$dev\" 2>/dev/null || true - fi - done - fi - " >/dev/null 2>&1 - fi -} - -# NVIDIA-spezific check on host -check_nvidia_host_setup() { - if ! command -v nvidia-smi >/dev/null 2>&1; then - msg_warn "NVIDIA GPU detected but nvidia-smi not found on host" - msg_warn "Please install NVIDIA drivers on host first." - #echo " 1. Download driver: wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run" - #echo " 2. Install: ./NVIDIA-Linux-x86_64-550.127.05.run --dkms" - #echo " 3. Verify: nvidia-smi" - return 1 - fi - - # check if nvidia-smi works - if ! nvidia-smi >/dev/null 2>&1; then - msg_warn "nvidia-smi installed but not working. Driver issue?" - return 1 - fi - - 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 0 - else - msg_error "pct create still failed after upgrade. See $LOGFILE" - return 3 - fi - fi - return 1 - else - msg_error "Upgrade failed. Please check APT output." - return 3 - fi - ;; - *) return 2 ;; - esac - } - - # ------------------------------------------------------------------------------ - # Required input variables - # ------------------------------------------------------------------------------ - [[ "${CTID:-}" ]] || { - msg_error "You need to set 'CTID' variable." - exit 203 - } - [[ "${PCT_OSTYPE:-}" ]] || { - msg_error "You need to set 'PCT_OSTYPE' variable." - exit 204 - } - - msg_debug "CTID=$CTID" - msg_debug "PCT_OSTYPE=$PCT_OSTYPE" - msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}" - - # ID checks - [[ "$CTID" -ge 100 ]] || { - msg_error "ID cannot be less than 100." - exit 205 - } - if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - msg_error "Cannot use ID that is already in use." - exit 206 - fi - - # Storage capability check - check_storage_support "rootdir" || { - msg_error "No valid storage found for 'rootdir' [Container]" - exit 1 - } - check_storage_support "vztmpl" || { - msg_error "No valid storage found for 'vztmpl' [Template]" - exit 1 - } - - # Template storage selection - if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - else - while true; do - if [[ -z "${var_template_storage:-}" ]]; then - if select_storage template; then - TEMPLATE_STORAGE="$STORAGE_RESULT" - TEMPLATE_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]" - break - fi - fi - done - fi - - # Container storage selection - if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - else - if [[ -z "${var_container_storage:-}" ]]; then - if select_storage container; then - CONTAINER_STORAGE="$STORAGE_RESULT" - CONTAINER_STORAGE_INFO="$STORAGE_INFO" - msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]" - fi - fi - fi - - # Validate content types - msg_info "Validating content types of storage '$CONTAINER_STORAGE'" - STORAGE_CONTENT=$(grep -A4 -E "^(zfspool|dir|lvmthin|lvm): $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'" - - # Build regex patterns outside awk/grep for clarity - SEARCH_PATTERN="^${TEMPLATE_SEARCH}" - - #echo "[DEBUG] TEMPLATE_SEARCH='$TEMPLATE_SEARCH'" - #echo "[DEBUG] SEARCH_PATTERN='$SEARCH_PATTERN'" - #echo "[DEBUG] TEMPLATE_PATTERN='$TEMPLATE_PATTERN'" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - #echo "[DEBUG] pveam available output (first 5 lines with .tar files):" - #pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | head -5 | sed 's/^/ /' - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - #echo "[DEBUG] After filtering: ${#ONLINE_TEMPLATES[@]} online templates found" - set -u - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #echo "[DEBUG] Online templates:" - for tmpl in "${ONLINE_TEMPLATES[@]}"; do - echo " - $tmpl" - done - fi - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - #msg_debug "SEARCH_PATTERN='${SEARCH_PATTERN}' TEMPLATE_PATTERN='${TEMPLATE_PATTERN}'" - #msg_debug "Found ${#LOCAL_TEMPLATES[@]} local templates, ${#ONLINE_TEMPLATES[@]} online templates" - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - #msg_debug "First 3 online templates:" - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - #msg_debug " [$idx]: ${ONLINE_TEMPLATES[$idx]}" - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - #msg_debug "ONLINE_TEMPLATE='$ONLINE_TEMPLATE'" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - # If still no template, try to find alternatives - if [[ -z "$TEMPLATE" ]]; then - echo "" - echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." - - # Get all available versions for this OS type - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep "^${PCT_OSTYPE}-" | - sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" | - sort -u -V 2>/dev/null - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo "" - echo "${BL}Available ${PCT_OSTYPE} versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - #echo "[DEBUG] Retrying with version: $PCT_OSVERSION" - - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${ONLINE_TEMPLATES[-1]}" - TEMPLATE_SOURCE="online" - #echo "[DEBUG] Found alternative: $TEMPLATE" - else - msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}" - exit 225 - fi - else - msg_info "Installation cancelled" - exit 0 - fi - else - msg_error "No ${PCT_OSTYPE} templates available at all" - exit 225 - fi - fi - - #echo "[DEBUG] Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - #msg_debug "Selected TEMPLATE='$TEMPLATE' SOURCE='$TEMPLATE_SOURCE'" - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - if [[ -z "$TEMPLATE" ]]; then - msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available" - - # Get available versions - mapfile -t AVAILABLE_VERSIONS < <( - pveam available -section system 2>/dev/null | - grep "^${PCT_OSTYPE}-" | - sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' | - grep -E '^[0-9]+\.[0-9]+$' | - sort -u -V 2>/dev/null || sort -u - ) - - if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then - echo -e "\n${BL}Available versions:${CL}" - for i in "${!AVAILABLE_VERSIONS[@]}"; do - echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}" - done - - echo "" - read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice - - if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then - export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}" - export PCT_OSVERSION="$var_version" - msg_ok "Switched to ${PCT_OSTYPE} ${var_version}" - - # Retry template search with new version - TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}" - SEARCH_PATTERN="^${TEMPLATE_SEARCH}-" - - mapfile -t LOCAL_TEMPLATES < <( - pveam list "$TEMPLATE_STORAGE" 2>/dev/null | - awk -v search="${SEARCH_PATTERN}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | - sed 's|.*/||' | sort -t - -k 2 -V - ) - mapfile -t ONLINE_TEMPLATES < <( - pveam available -section system 2>/dev/null | - grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | - awk -F'\t' '{print $1}' | - grep -E "${SEARCH_PATTERN}.*${TEMPLATE_PATTERN}" | - sort -t - -k 2 -V 2>/dev/null || true - ) - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then - TEMPLATE="${LOCAL_TEMPLATES[-1]}" - TEMPLATE_SOURCE="local" - else - TEMPLATE="$ONLINE_TEMPLATE" - TEMPLATE_SOURCE="online" - fi - - TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)" - if [[ -z "$TEMPLATE_PATH" ]]; then - TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg) - [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE" - fi - - # If we still don't have a path but have a valid template name, construct it - if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then - TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" - fi - - [[ -n "$TEMPLATE_PATH" ]] || { - msg_error "Template still not found after version change" - exit 220 - } - else - msg_info "Installation cancelled" - exit 1 - fi - else - msg_error "No ${PCT_OSTYPE} templates available" - exit 220 - fi - fi - } - - # Validate that we found a template - if [[ -z "$TEMPLATE" ]]; then - msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}" - msg_info "Please check:" - msg_info " - Is pveam catalog available? (run: pveam available -section system)" - msg_info " - Does the template exist for your OS version?" - exit 225 - fi - - msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]" - msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH" - - NEED_DOWNLOAD=0 - if [[ ! -f "$TEMPLATE_PATH" ]]; then - msg_info "Template not present locally – will download." - NEED_DOWNLOAD=1 - elif [[ ! -r "$TEMPLATE_PATH" ]]; then - msg_error "Template file exists but is not readable – check permissions." - exit 221 - elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template file too small (<1MB) – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template looks too small, but no online version exists. Keeping local file." - fi - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - NEED_DOWNLOAD=1 - else - msg_warn "Template appears corrupted, but no online version exists. Keeping local file." - fi - else - $STD msg_ok "Template $TEMPLATE is present and valid." - fi - - if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then - msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)" - if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then - TEMPLATE="$ONLINE_TEMPLATE" - NEED_DOWNLOAD=1 - else - msg_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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - 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." - offer_lxc_stack_upgrade_and_maybe_retry "yes" - rc=$? - case $rc in - 0) : ;; # success - container created, continue - 2) - echo "Upgrade was declined. Please update and re-run: - apt update && apt install --only-upgrade pve-container lxc-pve" - exit 231 - ;; - 3) - echo "Upgrade and/or retry failed. Please inspect: $LOGFILE" - exit 231 - ;; - esac - else - msg_error "Container creation failed. See $LOGFILE" - if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then - set -x - 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 diff --git a/bak/deferred/config-file.func b/bak/deferred/config-file.func deleted file mode 100644 index d7d26838c..000000000 --- a/bak/deferred/config-file.func +++ /dev/null @@ -1,648 +0,0 @@ -config_file() { - CONFIG_FILE="/opt/community-scripts/.settings" - - if [[ -f "/opt/community-scripts/${NSAPP}.conf" ]]; then - CONFIG_FILE="/opt/community-scripts/${NSAPP}.conf" - fi - - if CONFIG_FILE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Set absolute path to config file" 8 58 "$CONFIG_FILE" --title "CONFIG FILE" 3>&1 1>&2 2>&3); then - if [[ ! -f "$CONFIG_FILE" ]]; then - echo -e "${CROSS}${RD}Config file not found, exiting script!.${CL}" - exit - else - echo -e "${INFO}${BOLD}${DGN}Using config File: ${BGN}$CONFIG_FILE${CL}" - source "$CONFIG_FILE" - fi - fi - if [[ -n "${CT_ID-}" ]]; then - if [[ "$CT_ID" =~ ^([0-9]{3,4})-([0-9]{3,4})$ ]]; then - MIN_ID=${BASH_REMATCH[1]} - MAX_ID=${BASH_REMATCH[2]} - if ((MIN_ID >= MAX_ID)); then - msg_error "Invalid Container ID range. The first number must be smaller than the second number, was ${CT_ID}" - exit - fi - - LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K\d+') || true - if [[ -n "$LIST_OF_IDS" ]]; then - for ((ID = MIN_ID; ID <= MAX_ID; ID++)); do - if ! grep -q "^$ID$" <<<"$LIST_OF_IDS"; then - CT_ID=$ID - break - fi - done - fi - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - - elif [[ "$CT_ID" =~ ^[0-9]+$ ]]; then - LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K\d+') || true - if [[ -n "$LIST_OF_IDS" ]]; then - - if ! grep -q "^$CT_ID$" <<<"$LIST_OF_IDS"; then - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - else - msg_error "Container ID $CT_ID already exists" - exit - fi - else - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" - fi - else - msg_error "Invalid Container ID format. Needs to be 0000-9999 or 0-9999, was ${CT_ID}" - exit - fi - else - if CT_ID=$(whiptail --backtitle "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 - - fi - if [[ -n "${CT_TYPE-}" ]]; then - if [[ "$CT_TYPE" -eq 0 ]]; then - CT_TYPE_DESC="Privileged" - elif [[ "$CT_TYPE" -eq 1 ]]; then - CT_TYPE_DESC="Unprivileged" - else - msg_error "Unknown setting for CT_TYPE, should be 1 or 0, was ${CT_TYPE}" - exit - fi - echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - else - if CT_TYPE=$(whiptail --backtitle "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 "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" - fi - else - exit_script - fi - fi - - - if [[ -n "${PW-}" ]]; then - if [[ "$PW" == "none" ]]; then - PW="" - else - if [[ "$PW" == *" "* ]]; then - msg_error "Password cannot be empty" - exit - elif [[ ${#PW} -lt 5 ]]; then - msg_error "Password must be at least 5 characters long" - exit - else - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" - fi - PW="-password $PW" - fi - else - while true; do - if PW1=$(whiptail --backtitle "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 - if [[ -n "$PW1" ]]; then - if [[ "$PW1" == *" "* ]]; then - whiptail --msgbox "Password cannot contain spaces. Please try again." 8 58 - elif [ ${#PW1} -lt 5 ]; then - whiptail --msgbox "Password must be at least 5 characters long. Please try again." 8 58 - else - if PW2=$(whiptail --backtitle "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 - fi - else - PW1="Automatic Login" - PW="" - echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}" - break - fi - else - exit_script - fi - done - fi - - if [[ -n "${HN-}" ]]; then - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - else - if CT_NAME=$(whiptail --backtitle "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 - echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" - else - exit_script - fi - fi - - if [[ -n "${DISK_SIZE-}" ]]; then - if [[ "$DISK_SIZE" =~ ^-?[0-9]+$ ]]; then - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - else - msg_error "DISK_SIZE must be an integer, was ${DISK_SIZE}" - exit - fi - else - if 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); then - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - else - if ! [[ $DISK_SIZE =~ $INTEGER ]]; then - echo -e "{INFO}${HOLD}${RD} DISK SIZE MUST BE AN INTEGER NUMBER!${CL}" - advanced_settings - fi - echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" - fi - else - exit_script - fi - fi - - if [[ -n "${CORE_COUNT-}" ]]; then - if [[ "$CORE_COUNT" =~ ^-?[0-9]+$ ]]; then - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" - else - msg_error "CORE_COUNT must be an integer, was ${CORE_COUNT}" - exit - fi - else - if 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); then - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - else - echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" - fi - else - exit_script - fi - fi - - if [[ -n "${RAM_SIZE-}" ]]; then - if [[ "$RAM_SIZE" =~ ^-?[0-9]+$ ]]; then - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - else - msg_error "RAM_SIZE must be an integer, was ${RAM_SIZE}" - exit - fi - else - if 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); then - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - else - echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - fi - else - exit_script - fi - fi - - 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 [[ -n "${BRG-}" ]]; then - if echo "$BRIDGES" | grep -q "${BRG}"; then - echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" - else - msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces or /etc/network/interfaces.d/sdn" - exit - fi - 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 - - local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$' - local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' - - if [[ -n ${NET-} ]]; then - if [ "$NET" == "dhcp" ]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}DHCP${CL}" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}" - elif [[ "$NET" =~ $ip_cidr_regex ]]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" - if [ ! -z "$GATE" ]; then - if [[ "$GATE" =~ $ip_regex ]]; then - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}" - GATE=",gw=$GATE" - else - msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}" - exit - fi - - else - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - fi - elif [[ "$NET" == *-* ]]; then - IFS="-" read -r ip_start ip_end <<< "$NET" - - if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then - msg_error "Invalid IP range format, was $NET should be 0.0.0.0/0-0.0.0.0/0" - exit 1 - fi - - ip1="${ip_start%%/*}" - ip2="${ip_end%%/*}" - cidr="${ip_start##*/}" - - ip_to_int() { - local IFS=. - read -r i1 i2 i3 i4 <<< "$1" - echo $(( (i1 << 24) + (i2 << 16) + (i3 << 8) + i4 )) - } - - int_to_ip() { - local ip=$1 - echo "$(( (ip >> 24) & 0xFF )).$(( (ip >> 16) & 0xFF )).$(( (ip >> 8) & 0xFF )).$(( ip & 0xFF ))" - } - - start_int=$(ip_to_int "$ip1") - end_int=$(ip_to_int "$ip2") - - for ((ip_int=start_int; ip_int<=end_int; ip_int++)); do - ip=$(int_to_ip $ip_int) - msg_info "Checking IP: $ip" - if ! ping -c 2 -W 1 "$ip" >/dev/null 2>&1; then - NET="$ip/$cidr" - msg_ok "Using free IP Address: ${BGN}$NET${CL}" - sleep 3 - break - fi - done - if [[ "$NET" == *-* ]]; then - msg_error "No free IP found in range" - exit 1 - fi - if [ -n "$GATE" ]; then - if [[ "$GATE" =~ $ip_regex ]]; then - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}" - GATE=",gw=$GATE" - else - msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}" - exit - fi - else - while true; do - GATE1=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - fi - else - msg_error "Invalid IP Address format. Needs to be 0.0.0.0/0 or a range like 10.0.0.1/24-10.0.0.10/24, was ${NET}" - exit - fi - else - while true; do - NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Static IPv4 CIDR Address (/24)" 8 58 dhcp --title "IP ADDRESS" 3>&1 1>&2 2>&3) - exit_status=$? - if [ $exit_status -eq 0 ]; then - if [ "$NET" = "dhcp" ]; then - echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" - break - else - if [[ "$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}IP Address: ${BGN}$NET${CL}" - break - else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "$NET is an invalid IPv4 CIDR address. Please enter a valid IPv4 CIDR address or 'dhcp'" 8 58 - fi - fi - else - exit_script - fi - done - if [ "$NET" != "dhcp" ]; then - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - else - GATE="" - echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}" - fi - fi - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if [[ -n "${APT_CACHER_IP-}" ]]; then - if [[ ! $APT_CACHER_IP == "none" ]]; then - APT_CACHER="yes" - echo -e "${NETWORK}${BOLD}${DGN}APT-CACHER IP Address: ${BGN}$APT_CACHER_IP${CL}" - else - APT_CACHER="" - echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}No${CL}" - fi - else - if APT_CACHER_IP=$(whiptail --backtitle "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}" - if [[ -n $APT_CACHER_IP ]]; then - APT_CACHER_IP="none" - fi - else - exit_script - fi - fi - fi - - if [[ "${DISABLEIP6-}" == "yes" ]]; then - echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}Yes${CL}" - elif [[ "${DISABLEIP6-}" == "no" ]]; then - echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}No${CL}" - else - if (whiptail --backtitle "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}" - fi - - if [[ -n "${MTU-}" ]]; then - if [[ "$MTU" =~ ^-?[0-9]+$ ]]; then - echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU${CL}" - MTU=",mtu=$MTU" - else - msg_error "MTU must be an integer, was ${MTU}" - exit - fi - else - if MTU1=$(whiptail --backtitle "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 - fi - - if [[ -n "${SD-}" ]]; then - if [[ "$SD" == "none" ]]; then - SD="" - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}Host${CL}" - else - echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SD${CL}" - SD="-searchdomain=$SD" - fi - else - if SD=$(whiptail --backtitle "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 - fi - - if [[ -n "${NS-}" ]]; then - if [[ $NS == "none" ]]; then - NS="" - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}Host${CL}" - else - if [[ "$NS" =~ $ip_regex ]]; then - echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NS${CL}" - NS="-nameserver=$NS" - else - msg_error "Invalid IP Address format for DNS Server. Needs to be 0.0.0.0, was ${NS}" - exit - fi - fi - else - if NX=$(whiptail --backtitle "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 - fi - - if [[ -n "${MAC-}" ]]; then - if [[ "$MAC" == "none" ]]; then - MAC="" - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}Host${CL}" - else - if [[ "$MAC" =~ ^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$ ]]; then - echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}" - MAC=",hwaddr=$MAC" - else - msg_error "MAC Address must be in the format xx:xx:xx:xx:xx:xx, was ${MAC}" - exit - fi - fi - else - if MAC1=$(whiptail --backtitle "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 - fi - - if [[ -n "${VLAN-}" ]]; then - if [[ "$VLAN" == "none" ]]; then - VLAN="" - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}Host${CL}" - else - if [[ "$VLAN" =~ ^-?[0-9]+$ ]]; then - echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN${CL}" - VLAN=",tag=$VLAN" - else - msg_error "VLAN must be an integer, was ${VLAN}" - exit - fi - fi - else - if VLAN1=$(whiptail --backtitle "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 - fi - - if [[ -n "${TAGS-}" ]]; then - if [[ "$TAGS" == *"DEFAULT"* ]]; then - TAGS="${TAGS//DEFAULT/}" - TAGS="${TAGS//;/}" - TAGS="$TAGS;${var_tags:-}" - echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" - fi - else - TAGS="community-scripts;" - if ADV_TAGS=$(whiptail --backtitle "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 - fi - - if [[ -n "${SSH-}" ]]; then - if [[ "$SSH" == "yes" ]]; then - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - if [[ ! -z "$SSH_AUTHORIZED_KEY" ]]; then - echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}********************${CL}" - else - echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}None${CL}" - fi - elif [[ "$SSH" == "no" ]]; then - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - else - msg_error "SSH needs to be 'yes' or 'no', was ${SSH}" - exit - fi - else - SSH_AUTHORIZED_KEY="$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)" - if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then - SSH_AUTHORIZED_KEY="" - fi - if [[ "$PW" == -password* || -n "$SSH_AUTHORIZED_KEY" ]]; 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 - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - else - SSH="no" - echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" - fi - fi - - if [[ -n "${VERBOSE-}" ]]; then - if [[ "$VERBOSE" == "yes" ]]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}" - elif [[ "$VERBOSE" == "no" ]]; then - echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}No${CL}" - else - msg_error "Verbose Mode needs to be 'yes' or 'no', was ${VERBOSE}" - exit - fi - else - if (whiptail --backtitle "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}" - fi - - if (whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS WITH CONFIG FILE COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then - echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above settings${CL}" - else - clear - header_info - echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}" - config_file - fi - -} diff --git a/bak/deferred/create_lxc.sh b/bak/deferred/create_lxc.sh deleted file mode 100644 index 31fb86c94..000000000 --- a/bak/deferred/create_lxc.sh +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2021-2026 tteck -# Author: tteck (tteckster) -# Co-Author: MickLesk -# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE - -# ------------------------------------------------------------------------------ -# Optional verbose mode (debug tracing) -# ------------------------------------------------------------------------------ -if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi - -# ------------------------------------------------------------------------------ -# Load core functions (msg_info/msg_ok/msg_error/…) -# ------------------------------------------------------------------------------ -if command -v curl >/dev/null 2>&1; then - # Achtung: bewusst exakt diese URL-Struktur - source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/core.func) - load_functions -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) - load_functions -fi - -# ------------------------------------------------------------------------------ -# Strict error handling -# ------------------------------------------------------------------------------ -# set -Eeuo pipefail -# trap 'error_handler $? $LINENO "$BASH_COMMAND"' ERR -# trap on_exit EXIT -# trap on_interrupt INT -# trap on_terminate TERM - -# error_handler() { -# local exit_code="$1" -# local line_number="$2" -# local command="${3:-}" - -# if [[ "$exit_code" -eq 0 ]]; then -# return 0 -# fi - -# printf "\e[?25h" -# echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL}: while executing command ${YW}${command}${CL}\n" -# exit "$exit_code" -# } - -# on_exit() { -# local exit_code="$?" -# [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" -# exit "$exit_code" -# } - -# on_interrupt() { -# echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" -# exit 130 -# } - -# on_terminate() { -# echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" -# exit 143 -# } - -exit_script() { - clear - printf "\e[?25h" - echo -e "\n${CROSS}${RD}User exited script${CL}\n" - kill 0 - exit 1 -} - -# ------------------------------------------------------------------------------ -# 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 -} - -# ------------------------------------------------------------------------------ -# Storage discovery / selection helpers -# ------------------------------------------------------------------------------ -resolve_storage_preselect() { - local class="$1" preselect="$2" required_content="" - case "$class" in - template) required_content="vztmpl" ;; - container) required_content="rootdir" ;; - *) return 1 ;; - esac - [[ -z "$preselect" ]] && return 1 - if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then - msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)" - return 1 - fi - - local line total used free - line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')" - if [[ -z "$line" ]]; then - STORAGE_INFO="n/a" - else - total="$(awk '{print $4}' <<<"$line")" - used="$(awk '{print $5}' <<<"$line")" - free="$(awk '{print $6}' <<<"$line")" - local total_h used_h free_h - if command -v numfmt >/dev/null 2>&1; then - total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")" - used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")" - free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")" - STORAGE_INFO="Free: ${free_h} Used: ${used_h}" - else - STORAGE_INFO="Free: ${free} Used: ${used}" - fi - fi - STORAGE_RESULT="$preselect" - return 0 -} - -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 - - if [[ "$CONTENT" == "rootdir" && -n "${STORAGE:-}" ]]; then - if pvesm status -content "$CONTENT" | awk 'NR>1 {print $1}' | grep -qx "$STORAGE"; then - STORAGE_RESULT="$STORAGE" - msg_info "Using preset storage: $STORAGE_RESULT for $CONTENT_LABEL" - return 0 - else - msg_error "Preset storage '$STORAGE' is not valid for content type '$CONTENT'." - return 2 - fi - fi - - declare -A STORAGE_MAP - local -a MENU=() - local COL_WIDTH=0 - - while read -r TAG TYPE _ TOTAL USED FREE _; do - [[ -n "$TAG" && -n "$TYPE" ]] || continue - local DISPLAY="${TAG} (${TYPE})" - local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") - local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") - local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" - STORAGE_MAP["$DISPLAY"]="$TAG" - MENU+=("$DISPLAY" "$INFO" "OFF") - ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} - done < <(pvesm status -content "$CONTENT" | awk 'NR>1') - - if [[ ${#MENU[@]} -eq 0 ]]; then - msg_error "No storage found for content type '$CONTENT'." - return 2 - fi - - if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then - STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" - STORAGE_INFO="${MENU[1]}" - return 0 - fi - - local WIDTH=$((COL_WIDTH + 42)) - while true; do - local DISPLAY_SELECTED - DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "Storage Pools" \ - --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \ - 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || exit_script - - DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED") - if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then - whiptail --msgbox "No valid storage selected. Please try again." 8 58 - continue - fi - STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}" - for ((i = 0; i < ${#MENU[@]}; i += 3)); do - if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then - STORAGE_INFO="${MENU[$i + 1]}" - break - fi - done - return 0 - done -} - -# ------------------------------------------------------------------------------ -# 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 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 - 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 - while true; do - 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]" - break - fi - done -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." diff --git a/bak/deferred/github.func b/bak/deferred/github.func deleted file mode 100644 index 9732e2583..000000000 --- a/bak/deferred/github.func +++ /dev/null @@ -1,79 +0,0 @@ -#source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func) - -get_gh_release() { - local repo="$1" - local app="${repo##*/}" - local api_url="https://api.github.com/repos/$repo/releases/latest" - local header=() - local attempt=0 - local max_attempts=3 - local api_response tag - - echo "🔍 Checking latest release for: $repo" - - [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN") - - until [[ $attempt -ge $max_attempts ]]; do - ((attempt++)) - $STD msg_info "[$attempt/$max_attempts] Fetching GitHub release for $repo...\n" - - if ! api_response=$(curl -fsSL "${header[@]}" "$api_url"); then - $STD msg_info "Request failed, retrying...\n" - sleep 2 - continue - fi - - if echo "$api_response" | grep -q "API rate limit exceeded"; then - msg_error "GitHub API rate limit exceeded." - return 1 - fi - - if echo "$api_response" | jq -e '.message == "Not Found"' &>/dev/null; then - msg_error "Repository not found: $repo" - return 1 - fi - - tag=$(echo "$api_response" | jq -r '.tag_name // .name // empty') - [[ "$tag" =~ ^v[0-9] ]] && tag="${tag:1}" - - if [[ -z "$tag" ]]; then - $STD msg_info "Empty tag received, retrying...\n" - sleep 2 - continue - fi - - $STD msg_ok "Found release: $tag for $repo" - echo "$tag" - return 0 - done - - msg_error "Failed to fetch release for $repo after $max_attempts attempts." - return 1 -} - -fetch_and_extract_gh_release() { - set -Eeuo pipefail - trap 'echo -e "\n❌ [fetch_and_extract_gh_release] Error on line $LINENO: $BASH_COMMAND"' ERR - - local repo="$1" - local tag="$2" - local app="${repo##*/}" - - local temp_file - temp_file=$(mktemp) - local tarball_url="https://github.com/$repo/archive/refs/tags/v$tag.tar.gz" - - msg_info "Downloading tarball for $app from $tarball_url..." - if ! curl -fsSL "$tarball_url" -o "$temp_file"; then - msg_error "Failed to download tarball: $tarball_url" - return 1 - fi - - mkdir -p "/opt/$app" - echo "📦 Extracting tarball to /opt/$app..." - tar -xzf "$temp_file" -C /opt - mv "/opt/${app}-${tag}"/* "/opt/$app/" 2>/dev/null || msg_warn "Could not move extracted files." - rm -rf "/opt/${app}-${tag}" - - msg_ok "Extracted $app to /opt/$app" -}