From 3dc76ce5d2c4927831b9ba1670e764c70d6cc606 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:24:32 +0100 Subject: [PATCH 1/4] cleanup --- bak/core_bak/build copy 2.func | 3549 ---------------- bak/core_bak/build refactor_menu.func | 3768 ---------------- ...unc - advanced-backup-20251127-154005.func | 3778 ----------------- bak/core_bak/test-tools-func.sh | 355 -- bak/core_bak/tools.func.bak | 2208 ---------- bak/core_ref/newtemplateavailable.png | Bin 41282 -> 0 bytes bak/core_ref/pve-container-upgrader.png | Bin 70668 -> 0 bytes .../build.func.backup-20251029-123804 | 3516 --------------- .../build.func.backup-20251029-124205 | 3516 --------------- .../build.func.backup-20251029-124307 | 3517 --------------- .../build.func.backup-20251029-124334 | 3517 --------------- ...ld.func.backup-refactoring-20251029-125644 | 3517 --------------- bak/deferred/config-file.func | 648 --- bak/deferred/create_lxc.sh | 633 --- bak/deferred/github.func | 79 - 15 files changed, 32601 deletions(-) delete mode 100644 bak/core_bak/build copy 2.func delete mode 100644 bak/core_bak/build refactor_menu.func delete mode 100644 bak/core_bak/build.func - advanced-backup-20251127-154005.func delete mode 100644 bak/core_bak/test-tools-func.sh delete mode 100644 bak/core_bak/tools.func.bak delete mode 100644 bak/core_ref/newtemplateavailable.png delete mode 100644 bak/core_ref/pve-container-upgrader.png delete mode 100644 bak/deferred/build.func.backup-20251029-123804 delete mode 100644 bak/deferred/build.func.backup-20251029-124205 delete mode 100644 bak/deferred/build.func.backup-20251029-124307 delete mode 100644 bak/deferred/build.func.backup-20251029-124334 delete mode 100644 bak/deferred/build.func.backup-refactoring-20251029-125644 delete mode 100644 bak/deferred/config-file.func delete mode 100644 bak/deferred/create_lxc.sh delete mode 100644 bak/deferred/github.func 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 14ca28639d712d1182d57a74f9db22e64ce7853e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41282 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 ZV!B};i*&mn9CrW! diff --git a/bak/core_ref/pve-container-upgrader.png b/bak/core_ref/pve-container-upgrader.png deleted file mode 100644 index efabf8a8159c013757e0ae8fb6342ee0fd758fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70668 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 pXcsdFC05Lf*GX z%<^BrA^HKE_~rMSL8^Xhi5HdSMBUe3$X$l7q>hZ8#ovTg&LU&@#te#^K2E2eJFkk- zcP~9WhGzS-iN%dsrI1C8^^xuv0Y_2Z-;97nrj%jtSdEfOLlfiARpgPJ!R8m+; ztsZ+oYHe%1F*=A+IDx!}pH_8;S*kjWN6gNH9CpNn7If1<3ylVv1_a`8sEZv5fr+rD|WJVmeXM4R(pM><&@8`mv*hhE; zTfwI(x*PKQ(`1w1GB1ZBybqp~i>k`wF!KkTW>iKnoR;sIOTgM#dKOIY=oj5+%Iq^!_nVnXYLcou4YYy?Wif4BI}PU=f(B5u<8t4SF1L=REqJunY1F550>*Cqp;% z?T+|s{PgwS;|9L1Ma}UL)Q;RBoN1xl@0|kU^EWR}0Y)ix=ETNId0_LL3fy}i5~fNq zcfULFGK?Rmy1xrLJ&_h6a0c+Y60&`wqUyoTGDIwM-AnEV$=pJmcKNZo>-l>%c0e4d z>Dv_g9@$q#Ik^%CiHz zk;IX2gJ#e_w{}{+*%Hz1@($n>YnAk-MNS8mVhg_k=#4hdl{&w8Q^sQ5BDm4%L|5La zd4$k9(#=0+T|#30wSX(5M~9yo?#{=$3eAs22I(t)ZB*q746`=+4Pi@@YPnuIQi)5R z@n+ky<&o~~NC5t5h#;hd@egrQ35=h?vok9;8Gv`THfRx%gmpb{!RWCnC7}X{ip^H< z26E44k-lImp4qz7k_+*VfM`Bd)Y1pk=@ZN z4_%yw2hs;WTa&@^`@mi6L!H;+AnzgSJnDqcuSh>UVy2(^xMJ9n zC@t^yQ}M+g6K^#$3^-JE#lC{XUY3X@UdWO6ZF*L{#BYpgeeftf7n2zeXkd1C;5VKk zm65n)o~;ZI<)-jectGxgfoBtAC1u+r7-=0|_)D$95v*@^l$xisCnz^CkyvnBIrG=W zLPa$&kz`6BX|uZM=wLx<8Ham;3rD#1N`4!!Es!G@A#bXn*FR|gmUb^pbp&{QH-Dg` zyS8-N`9Ra}C{90!=t}N-+pw{nPhIr6%=>?Euqg^7&A)u?#!p3iA}0&noDQtV>^_S* zq04To2iu0ey5aUAps-?Po~aA7=OYtYCx)iTv|}<>2Sp5mDSDvBs;+C9{~086T$Qq3 zlez-Z5xlYGZZ6Lx?wLv`lZ*lQ7hwQ9df2HZ|b((dARIQ(e%bk(rG(dM9tG*-HkWu zRUe{<&&Y)zkba=cPS!A8Uhh#XdmaML&Z*-g6m;drYF>x5NyK@0lg5mfOotgi;qMRp zj{(n;XfbKA4+9Mz4w9w!&f^YpaV@-j<+df>mWfhw^+RuUqx}1C^IB`{Sk97my4^ud zjBv^}bOj@!VPemR1|Eid2w)b{Si6wBKAa7e>*t6tlvPU-F!p~(yj+gF#{KM?aMW2O zjM|jW1%4jk-Qt)T`MfC#Xx-&B{`0B@tO9S5rg!S3&6oqcNxc!g38!6^j}(rg$`Gon z%4{R4v=pW5<5knS5RzNxQWK(bUdh4+A!|ul`AHu zyjumgDK73f61WBT^iUy4dyQ90Og~%`rolH|CA=el9}1h8gy;r z*|S9|MS<8sM^%`gdcQ_h7p>rW}bewb}-$Nt6f zxAbb$svw_AvM~QIFt(J@=j8#HQo)Qw$oE(--vO>5%}g(vWk}e#uopz>>&kRlTRgft zY}$NIIbs%U@q5d{;xU~5xZrgat`oSa9IYBuIX1VKA!D~Ks!ZP;v_U?9;~e|A|U25TI5X-YFI(yA0cy={nV73GO-X z06i>q`AWyk1n_$A)1CE=j2%taol^Eb3@V6(i72Up?Y*%_(`r$0s&%$&L(CCksD#(N zxpO<MvOz}_LJ;tLa=pV3D64ZdmB;|M-x#vJ!5}vXMG1AT9&BRt zMkB?xC(rH$S6U-C1wnp;7Pq64ru5yXU#stGSb~@rn(`W+odemlKLLR?XB2#K8Gf?G z-zukJCQVXU!71w)voth(86k#U9x-X@xWHrGYlgsZ8o712-yj%O+t1~! zqGfP0$VY}Dn%`&Yj@48XJ~_uv4wh|7vy+sgDEJ2wTuCL`cn8pg4y5@co_4tuOO`I- z9Xbei1dsm+>aqXqNghj6g0CRNoL6Y&PVq*iDB84!cW>%FN)La$M*}De?W1vFa{1O% z)qjJF-cNA^!nIi)k-%yrM#IR9p#4_Z3T9Vb52L7kJ9%jam!AVo_NOat^KS28D5m7<&OUm7W95o1VbH?Rk^T`0e{i&Hz#Ej_bg=BK zMx`R(Jd>|%flgpupbqpf%>DPN zF^!yhl;Ea^mT072holz4$8h*FT;kqfHOSXBx|Bq%#02vFQS`hy&S&FQ3Fo_m-}c5Vh+x-}mV!uCG?6vw`-GdpDNHR^zz0Cp|Y2?`<*@cte+JoK@~x zTUURb9q6dy6}ja$_QN#& zfJG7Rqg|6pnJk+gTRAt9Te0tef}E8B<7@UAFbt8_RO>$ocHd=hF5e3|ZTk!ujz{C( zCHthW-*58T2cG|47Y7lpiuc&jpE+#77b_84e6~wTHxAV3$EV;XTeIQhb^OQqZC{wqx$#PEvtn$I6f*VEX%lk z*h#9E8&FX2jbXhE621|o2|i;NOxSPoV}=%C0mbY{UxZwL>(i#NF&Fyy_9L300NhD) z@HlBL$8Xe10?o+G2s3-&jz1HgGl>9R+VJ4sGOn=B9q+~t!t%O=t!HCQ6)kR#OERD6 zvKG2C4SOg8UrrsFeKst()5%}Jo4(HQMH{@lNb0HtvmFTJ?6{{nv=#Xj3P6?lr!aof8Mi#|7^{SEf}|MI@zCmCg#Lh=d!}>e|47is;!~No zCwJCsLdGbhQP@S;N2puQT#T@gqRAe3qG^5S9z2L2^GvN)QM=drM-Cz>1+R_m$WcUm zK%~JY*Sk=35{!ZSq^N@;Hu#8aC zZYWo=KUrwI`e{UkU+P5yW9*h@z|v`Q!@N@bSWQ9RLvxhd+H~D}w`g-WkDkT?-Cc!GJ2wm85Xt{h-N*(z{RX6@i zD(u-TUdmNPRenK^Rq4K5qo7U&L zQYEGWdrLTKWe@r%pt(D-e*?OoU0YLpae8a4jf?aqqH_ncP5uR8Kc+=TXFF%ayMLB4 z%O13L+R1bMI#fm8DqCfL(qenK1y8>hws;z!t46>kc5O0DOWcRo3}AVmigTZEb*`(%3;Btp3LYBX-nsd+}`fQ+fH;Z1beYetC^IQ=tl- zhuE(|D~E*Nk*0&h3w{A%BWbZ8Zh+(J$lFOlnoj}6+{_{8FE>TG{)vV4CrYSNI{gl5 zl||LLfPkbrva}dva4S_VpP1c>UOW$?ggPrAUr)miLbiWIDt$l0mA<~yQl-3y>#$@r zG2U76BaVYE7=ZU#Md__;bC<>>70W)2ytTISlTNO-4W6#sJPit0cuciC#T1SCmf@K- z^ESLu7E4bm(jtDFq4!LtDWtQYp$X)=%(tQ5l-@b{rR1N3?f@GGLgQa*Ou^dYG2Z*y zFrkJW+rlwITPl*`8uV`ZnqR1hXY2L8$Jrg}3r>1|Kb9p

YD07q;Vq%06^1}u0dkRrwr}2@<89PF*JPB~1;-=WdkMA)KB9s$Hg^-k= z*i0lhn!Me|-ie`cTp9v$$}E&}*(WQ^<&>J)iR$|O`Ab0hwTL(L^X}pV-6aG?yMgEW z7!NV)GWRc*)%zp18@KYxx=X1K7&<5htGxmKz1m?6TllC5VXl$GZCO4_`$^U|OS9U* z8I?8D$ZCv1hOkn{mVp{fFT?0=)cTs{RP_CJUfuLU7kU481iv84$7^m6{2+Rpa_<@d#>EB_ z%Og=j(8~W$q)rNy7{ucHd)+5_uDTs2=KS{{>GhGG)$&@x3_c98rC9%PCudYi6`~&N zVu!r~Yqj zfs8-f<^>)b2h6m9J@58FgGV%C2Ejyo?r zz+*UQuD9bvOY(X)oaE}C=!On>$XRuVTrf-PI`yFvq*c2W_MW1%NNHHHy5QIE+}+;z z3m)@1arG+a$nimP2-~>EEAol0=1&ak?dF@bLwO=leV)t%CZ@gH4g1c8QX(@Uzrlw8 zBRrP7l$Kg5BlX|O8uvrr;`mZ}uSG$>-HMf)jGRkz-T6&74w{1Y{)WnkWCc&*Ic#Rw zUQK35t&V(w^Ybj|csxvC|JzCgyv* zB9mzv9PhvUS+IJw3v>CT(pmzQXG|K;{$Y(S>q+uDAc~NpvLVNVC^YNHT1zi6ZtLpE z>9p-2w9!FJ_zfVA=6h(qcJ$tCkdorYTdrj=wx-U&E;>#d%lP`gxQ@QPCfp=r|AdcQ zAVF^Gf5~;IXr`by$X;z6wU!`!dsWMI;GM}a;F-9CwFcyFu+$a;5V$snXmNN$MHZFyA4!YjX0u7*Z=7&bi5Xk|hvAr*HxhTe42&;BiU_EmO1)e@82Ba$oi?Bd_ z&f4)!4S;y?UJ%NA1o zKz&sMZr1?m|J}7dsg5u9Fa>yvue%4MUq{x}Jm|XmLVwCbUht!E<32`9p;`gY4b1XH zm0qKHlS^65y!*-k+TR(}80-zygEx1u4q7;K+9P9n!`HX#90QX_qa1;?YNMG}?mnY1 zJ~90ADz9BiB+N1J*5m?diz@{m;->QBdEW8-JoV+Lyi{fY!IM1duoic=RNV#?bn27> zRvRS&V=gm13uAxwBxvYBdpS~n_I|b8UiFLku}tqH3bByugzG>c*?Qwr=SQr2Vw?(4 z+AkGSHQb2>c3)47>)1Y!adppK0!Qmp62EpN_Urqjk#AKGFzU6JEgG;UJ9U8?OQ869 z{Fgr<;oPw&|F3Bl4;upzP{AyTmgpz$aJgss2_fE(BzEGq7bE1}xevepTy0Lf{Fc>u z1;_msKMv3m6J=j}K}h@E-eTx0h@*}FxLUl$=s;0XaFnM0_^g)Mq3hW6U6s0z7H>RA z`XKjxHjpijRspFanJdSl&FvS-UW@;Vr>`m@CYiXH^c|xyQMF z;|0xYMK!)L;JM<8u|1OU#j~ zWlx>o&lpGd?zH6nfy4mD2Eg6q9*Rqpgen>n9@COkLI&$X$|L9WKThhy&@*CW`^5}m{`)-t zzu0^4pr+ct-52#yR6r0!svt@S=|yT3q)YG8doO~3v;?IJBGNkn>Am-+ARxW>5~-m_ zYDhxhtnfU~`+Mii>@$1L+5eo``~Jg;%$k+8zPZcyF4yOZx}TMPxOR=)b8tJspIBc^ zXcTEV{+ZMnM&Z?}h(^jI(Zfl`&})wqfPHT{VsNO6I0|_@M&=>v(XjlNgEREK)~}vs zryVmE+oRfH>ZRTE9qK*~%{fz*J>%i-)uQ;Pl#y zyVEuSKmXq@)aU`eP=E4n*n0kCU=X@)8s(=r%u0!`McD7s;*Ur28%;Pu1}r7v%z)@H zKSg|YE#RBeF4&L2xqzspQ(6h+JSZ<848P1Z&_n{^Q1-G$n6grQ(h+W%*dlzr;R5d`YYZe|rnUuG zv)Dd5Vbsi;7WWnlBXRCymMJwRsj0?CgPP3_Wdw#-e`?Pz^nGjjEj zq{sOA>lL9#FU&>&gPApZq1oEBulIUC=Sj}qtcZfR9tyf$P948ZoAKvLl(;_3Lmmet?Yk_B39{($~4eQk7*&cuu!iN4tQ(`d3bM;rpxhD zij(J{{2fH410_M5W#h49p+eo_Hy^8nJpRSu^*&_)WYLP)1Blr!lij7_fpIy3&l}KG z!vTZ8PrU}Vj6-wKexT02G4_Zpf&c}Kf}7+Qnpo^32Z>rpt&Aop8$f;jg`d+ds|oeL z2&^u7L6qMq02XE>se3+0^Ma%dL=f^L(mXset;!)z)ni;wEpM@G-0|^XG@CkbOrGtj zbI+_!Wfry-E0D5NI@b_y^EKuj3BE4q_RMGUz@4uJ!$^LKc&nP%b3i}#b^-VPG-{~^ z0w?_Jj!6wy&^qtf#b2*+D!W$id*Eo2Xis!_%`}OkE(t-mjxp`q!*+qc1@Xrxajz`kbMIxU>mTD z4LzW~wfTwov(`tn{Uyu=Cwc{Rc~;Y`iNHX<*CPNSnVWyYs0H?7zl~|8@&WLL!&H-i z`r?mWI(HFvg9;u57TvXsUCVA6h zB1h!m{DKD~aO8c6$59^sq6S;t@4)yaF-$d)uZ^ z0f6LPywF=VDWZf^l>CE^yOGxSdfc&g{aih65$^!<2Q%DCIrSgXUEiRy)O_UkCx-hi zvd#Kgvs<526Rtpcjlh&j`hD{*u{Dma@l7oDgj(hmc5PNU%KgK=oa%`8i%*pF@tf9> z?b7v<%1B|Q+&pES(itkcC6PLv*f;)Xfn-{?KZ!+jWoO7Ki1VG_C|AuWqBQ8!7jPQY@;jku6G@@aY6` zGgSnwP;6Ypy5Sw1B39NlF4!CEwN;Nle#G?3=gVJ= zw!^?y{>PrV8wy{t$-b}3iZK?bR_~;$wqqGdQHysAsmzhZj_LSWrJ@Tey_x0Q3PTi& zJ5CSkQAb;?u=e#CSf-AQWK$=}ymPbq=J~vc4FCfyJKu7O85T94ySY>GD=>9Q{(7&? zEVGpdy@$kB&q0hk0^ibsn^y&Ub00s(8; zcvbi06DQc=OX&-TpMU0Gc+L}4UP{Yn|Awym?=Z5ticv(GR~YN}6f2)3yBb%ciTNCe znc+%d$Fy6DV;S*I{2R@;vcz_uuaKwo#Rd7r5f$T01|;TORdV1aoQKWp3(<_b6mq;t zaC6O-TY4LK<3}>Hc|`J27OF^>o9sf4RWvdvBKPYZ;KLsMO;^{mbB0}nTm*a3$)^X8 zB<)(e{1PkZ{Z|)>3-Z#GEAJC~q?}x&pQBx5fyI+@+y?VW3pPxM=NOaxtEwRefiNwH zH|>Q20~__af~&t!ZgYDcyu88)3<=)J*T0nd?g9%qeBO5|{i6n)KynE)fmeLc9?;|yU(tq+u31p1WhgaMUqmd;3FuDZ%e@sNx9 zoHbP|nmXo-Q4CrOEE`i_+;d7Mhgx)e5V__^cdY(T@rqv|4{c3yXUSi0t8EaKd@>y#M{yw=)Zv@ zBgg0Db^VBPSFAvD#_h8`B}%Ns;cZNo=^Kzz7!ojLbE?~4IA_EJTYn`l^!o7jsq6Ob z^k1A$dQaUc?>jA`1?FK4mr|L%#~({eXk@^-S|6yL4rWs0drAOqnVhe|6RAgTUO_U& zoI^07;>*X?r;l@=n@Hur<#l6DOB_>qVYL;(a+c91gU%3KHqTqk?{jS0J#y5z%pz*pX{Dk^d8jXhmfNXWEpH^XzVBqy)GNcrglPKuwF`-fb z{UwsEUsN-9-(6O)4>4M%+KGL%O>3xJL{MihUtQ&#LqUTGF%d;XV?U5{J6o^wIs(;x zbg9N_hI>zia7m+VnP=cUZ{enuO-nwCbz_@)3Xd&KL!GZ_ciONV1j{Wb#dF%Lr1@Yqw8ODH(4OHgWCGyA?g#Smy=2 zTD3Pc%cw>|G4)~j&dH>G!~aCleZpa;;xyB^Q?{gj6G3{~*>mYHu(3qES!NcCYoNaW zHqa)Zfqo}E0yCF!e))+@8yY<^3hyPi;R>EW18r>&`L70QcKNZ#1^P6O<;4Y(b;Dg^ zN_8X?F)YVnKDmj54q642((dP1YVRm{)RESlPbX>tFpN`&P~-z*oAqP2MemqUZFRYT zlPwd|H0^N%96@ncTqIx*fs@zH5XZ*{emF1rP+zc7T2oeCRM(8%55FMv<-2AuX(I7c zrTu*9lyDnp>!A&Ot;~?DLc7bK%Btgzxe5tl-73Gz#ZkEwG|20+I6>Z$;u+_nz6{LM zIquA)6zBxzz^{+8Tet2?N~z6BhNQugreBsTWc>BOQU|byJ2eHj1<4Qp;2A%4^lam1 z-e;wmubX|Tz4S9_Du4E5Fn=CQszKN9`n}tz9uSTY5eUr0uxLNuTED*1z1~x@G0CooAIIf?<=1ITI(fm@y?UFgoLM2_WkkebM_>Nig`P+xM4goYwbAN)< z3rgB`8tZa%=;w0nC!KB@P#-ofn`q5j4 zoz2is+z}kJxN7e_298OKLVL#;L6wtt%K_ah7%PA<3Hmb+XGHn_BrF1dyak3xDyGA$ zIPPVDR7)`$Acw`oa)BGe{>Tgu(0o6AwN(x0#>@RdAZ>JHeU~*KwxBz2_t%95@U%1? zI8_C`5vb!-J3^0%6J((Q2qfY#J4Pjd>vO|G&vl=6#Y){_0-Kayq7jcgjg5`+X1hAQ zQPLr^<)J{gd6rudJL!Ehl(O1{faY&m@IbTU61eGs@^{v7|mczv?eveAMAqs zM^J;_szMB1w&8etXklmK5O@SXH7lA49GY`JcAB>4XJVQ5IAdFPjH?xWOl1iBJRg<( z=dD(2O8^mU>iRSxnAbMT@QPaKMHJo!9kqDh_SdG7OSM)(?RC`5vb~x};{H_n8igko zQ~zx~YuVWiz&Wm+Z#z$Wqte4wvH)ge54|~}EzP2v-lA8@$5Gk?Q3rnjL;bFQR3Mzb z^J(c|Y`Sz+a)G#@>{5bf)+9?jdYb;|Jg=sHxc*MBlC8^)R_OBO##`>*#vxk-BA^`p zvT`r9+&BFw`ttJ{`BLcGw2fvZhUPIX_wu>&d#u2o3FWj+Rl6LvoPP8-(3}+I^}cnq z|Gt*Q>jd;9*Gvn|g&B%y9^-|5#|KOn9||6G zNiIPd;5#k5X~L-?)j=A3>s~9*-#1}%quZmaZNtZtx3Vd=Jh5B0#0v<%hK-N`zRm5- z8!@yiyhVnO_X;;r0^xB!D`L8plIt7Pg7=$rDAhBfh<{N=4{)jG2fkeJpJf+do#X%G z8i%PCpq3XSUa2ljNNfQS=I&Rkrvgm-itp=%)?)a&hfCKhlIGx#hrAZi%g_%kYV(~9 zqookamX~QJc+!H*5a1lRanc5UfbIFy`S}wnUNYA8zgH64)Hsh1xm9+Fk8iv190{5u z@f&&{wJoPbS-9Oun)v1b^_PUjP7o9DaZa4_ILAzY2-Tp&<=3c(P9V!~Hcw(p&MnT6 zbH|8az^3XepfWR(ca=)+7o+&y^7WrvKW+Q*I=*oI-E&|$0$A|)@B>v~Hd;F@2}LMu z;|otY7B9gMp(T6@*;CZ}~E?RkjO*}q8_&YWKD)Raz^H04^ir>M;7hQG} z!bvynpFjG-93O-<{pu>Yt>?H`4TJlBME@Z2pl2$8P-cRmCV_!FkeXm)5JBttqdW*V zU{Vyox{AdYg9Z^Ou*8`217a7qfYbcCqB;g;1^&aPQL#>_R}3zE zP`^lJe!y~Sj){9VQ80=6tXyi37;ih<(I>#vefC`#i%pZ_1y}IKIX(eZ!;GpId!|AzK)K&Z z5dJH@)v=1&sxOxb7VA0hUXKFMdmdA?6$PSuaA7gy&tS}PPTtjYUv#k!s>I>U6Y472 z!6COxWqFY=OfohZ&5 z^+>j-|NMMA+!?B{HElD7<`svLDXD>Yg(nu>Go0#G_>Fxw&xx?rZ2<5BmTh4MHMvu!duvb5_S=F|%SCFu%?L8~^XY+L3(R6*r-vMN z5wYen=d4w@tZmY)owucxQ3URE5H5^UZfIBNABM25pm+HjmjE#!$q`bDBo;!sM`rqi zBXIYa^^Zj!kq$e*a`j|_`;YtA z<&(wN#U9z$i=*!L&wu0d;13&T;^j0jlxfgjG*LW%E@t{VJ`F#k=o<*MaIczRRZ<*yIS)i zwhklwKc!3nvSw_cK5;a-5V>NQu#@{h6^F;zc4b*N=GYpJh3e$68)(GD}9 zQD_&d%`x__8+&8c4g<{LJBT)Z5BFqBb&4ni9dmV}ZG@i@$@mUcu>JLn-R9#~Ak1WZ z2biy`x?y=m=m+O$eV9=u^A@~iTpm+PpS=Tt`@ZYr z55g#Pt~z+O!}$v7V^4?c`maAMRs1qm$i`kjH{o30TSj5a9-o)ov_?1T3c(^qpF!kb zrJsE+ufNj=%TlMSK9{zI;t?46Ct!OxC+D5qF6ZOf>eE>HA!HlB{w7-{f|iELS$Crf z#8Y;RXO6)iELfU$I}nCJOwk8%m}?9W(hUkzB(XQZ%lhp2Kz`(P`W$1S4g41Wb81GB zy~AqIEuHv0)H}KEMt%DSYdso?v-GZ>om{A=YrR!NGLNZ;FMju3Qog_MXNHjsT+My# z0Np*WB8LQ24Gj}5(+vE8M&%7`!+y-UH)D2MhEZKD_b)uae=rX3u?iy>v4)VE;hx)7 zr_5h7G|iaTRRMAhsQs3*HgFQy0uTqJ%lew;rJ07&*BpEl6r3&32!SV727|G@He-N@ zL3c>#jg^A2Zr;jqo~`1carwh3R4%ZIF$W$fzw}vuOSlZykoE8z+(#PDomk9V%zDm$ zU}8uUves_uiE>iqL%)`-ZNEU;#zQI8S;{VqT(bLM_YE@sLdi`?UVBK0T>0S)?tqLe z94Ubh`FAODW@1Idy^l(#(JHWU{CG|s(PnOBI7mvkPU&Xx@{13B;dhY$6y z2%6^~cHR;(PQHz!=bo~D`p{zl@b*C6=)WHGtSAWlaxZ2P&oh~pTjXs!z>&hKxxIeD zvw36D$i^R)vb+uSyRzB91o{s=5}D}#c!Rvp1qj*LMLayAMxw{BJ78vs5r^Q9&9UeiKDv@+~Ropi_5f6L3y; z8-}+CMm^!CeOx?!5f&d2Isj%frk7q%VM0VZN0vT&uxUN2H4K@h5)Wfo6lyk$dNaX$ z27Yb4b?T4qQEK<^cGC2N{Dx;&CW&gd41>Kb+t<(zHn|JlcRRSvZopu_EVwl42E+E) zhMy@c%?!(L)(e074=s~)9$f$=&0y@$J&Og5^y4^@kNj2vk48iXi@=k@v&K&jca#<| z7VcPu7wxS~jmDM1F|Bx%1fJ^iC@9S@H@Ua3Za#j;`Q=E-#Xw;`$_*NS+&9kq9iuS5 zTI`tJaj-X5O21y`zuzyGUmA$YEGOa6%x-^i#AQ`P?W2#txi-+K1ae|tI#g_3+Hl}xT7QdBb| zP#~^MPBZj|cdU>SXnBn{!GA}n*|yGx=DS)72YCU{M zvaUj?XbKe5?qcIe9{fQrlXBcNpwvd}E{hj=+OU#@QIYaOFb0~aVQ@a6oaqIMc z^_=>KsgJ)AO0IkT zl1x2&zXd3-@)oAUaH}vHpl(3hqtM6c3Q(#<=N925WHp7FFZFW90Mv8cNI7WZ^_)S> zj`q+jxEJNS`I_bkAvSZ`NPqmAt-tN}5clAYb_XtC7vK`7Ui-;~i zfb|u!XEMVD>uv~${0e$e+a3*F?H^w4f6CLm#NK~j^Ww|Q;qg~kg}?R810$RFUvmvR z@lDlD;n7DiX|IRacS=R$t?zkelyq`&Ug_sQ4EMcU=4a*$4yi5@F_>LaRkE=u(6YJV z=4Ah%nPW>#I(bi&Jipb}6+k`$9&Q**Al5RyYb*DkYTK%%MlSuyF#lcBXAF1^@Kvx^ z-G+PwRTxjq);Z~Hbqilpf8B?u3uPZf_k8b#deIs6mN!9z+SOn+m!TXI%GL4A`umj1 zT%cw%7xh73(igj$Qfc|GX8)dO&QW7J_a=$~>cY|ip9^$DwU+W2W!qhG5!eFyRhtxl ziQn4JJWbzSgJo&F*vyF9*i&vN1cWjyV#Yr3=}e|V)oL^^2o|dNW&`gu%%I;h1DK^G zKAY&RqS1Ol=j-4JPS!u0rq{II8$9=^9ndcbQ4TtUa;=r2Ix`ZU&bNK)atCjjl*iQ2 zFYZ7R!QM@12q<|C_bW8M-tNi45;#S=SQvh1?!6F&xu{)`w26~4PtWJ9BSu(N56`Xl z_|J~rw@$8`R=VZ|KHur@buk1i*J5Yd!x`|b&KMGbl!)cJG|#K{UL;Y{ z1G2m=v#6vJp+bZ7{ipWsb}kddzJi?*rFTwX@FR7Tk7&f7jDbo0r!SpGFvSq;TL9Wx zcezvs@_}cY`MS%Jy=rfA67@y}qtzdAuO2y!yx!(F!!kaH;dd8EJV+nqOxMj^C2ed6 zoCN(NR?*okX1=lUCO)l)&o!YP$d5yDf8}o-0Y3u4`MKizR#HH0gS9ofBumK8Mj&IE zd+IS%SWGKdDf-~{33m;otF}}#yWTl=xBMk4vTWE0J#El=2%L`<01apQ zn+FkbGDuzj%;2f6fxV`evKFDp+_9ZNJoDnSyMjAr!Vmh#lpZRN4@z@xyb${7? zo1#Vo$@K~qGMfGL8ugp1the(JS zF!g(X{OjU*(v~V9f=}u^!GC;bLAB$#H0CcgQc_dGf;?7XCFE`IL~4k!|KBu&g6Y5K zrCX1Uxlf4kMhJih8Muv+HcVk~<&CkfDcVRxC+ zV9c7tNE!4SysP4a%&<9J2{2T{Ql2ZG>DbsQ-fg@mW{JTyT0AJFa7_)@h9 z$0CaSE@XF_f`3Aa#e<68Y1Rky1Z{w$yH%_IM@u~e5|fA8`CTpjdBtm9Y_kB=xQ$|) z|BZIahGZ&`7Mb1r@@_uQy7t8^faQ`K`5_=;62gFp!@rg~s;CCq@6vj$TJlPHJV03u z){ld9RJ?dLnEDMMbA*h)fI)HU>(#v`x{^bp}8pVFwU-e^?|&* zrQ$?Qs+NW!hh|sEDe^S7sx|UgLp;6!RhqER-~J(+5k+0MEG!#@tqbvy>Sl$)1e=QG8@gOFh)s?lsT$=`E z?^hf?O0qmD^lfQ^6ZUK7Q*lcOcogUpmP?EkXmn!Gp2e@n3&pjuWbKPN8VRucmvM6E z_W<6QgihIzZAzHBFemi9;Z(P5A#?w8vCyneAbYN;zGtM{2R~qwFm@-ETTnamP5 z_7J3*Clxsv`aTVHO3#&V9-hJl&KLYr?d;E-Wd!wWym3Nef4NKk9KWW%98DLA-WqxC zGSIhbf*2dREwGFbq$gRoi(}b35%2t1rQ#e>IX%hZ zyaGj&#c9~S@8_O9juKhFedo#?%~8F2%ahW&YDrn2UE14I+BO)%W>Uxb&o+2++t;gY zcNwQ*g=Y%it@W8zQKq3EotgnO8BH+bRtcXUzRaMJ?%%B5;%2RoJR1&`(E9RmtDF;- zzG{_3vhR0usvqkDyX=js&K1S~fXALIQ|TZ$Z%;cPw-i)@{hRjk7o9`KbzYjlKb8xYaDvVgE>|2ObUO z_xpM;`0nY8f0%olMgpQ~IT)Y$pg;BDrG8A$L>8CQpl0QP%6P}U(%=w}U}i^xPWoWG z+FF}>2b;RCMZ2RBm6O{i3AgQnEbT5{ra+!xi`@777krKPCE!Ep$vvlD-&BK!(|q9z z?!&Fa67UR)?OKj#1`y|B$@L8Z#kk=?jYXUm%1UsuhYZu^l z?R6sCK6eVQ@;;K(5|0r8<}AP&5!}dL>vJQ4hn=HtG(niWA47xqAh4<4ETk%fpv6b3 z4X$;V`y3uAyQMuGET*22Gjq)tU&|-&KetS;&?2(iXWS0YC`-UZ@%*CSIn&vCdui+? z>QDV#`Aw4hr7=9>$oXl+NVRfIQpPdz$hA44h!EGAaB^3d)JLqCjh6ppTPlHXt8{e6 zQ8_Wmqbj=T@J@*JWOS$;iG42~Z12JdG{or=b7AK3tVq{&SJ{%x&5Gj+V}d?KyGoUz zOzFu6d_9`JbFzxQBy$aI-4DW^9_&TJ`TK!~nbQMpq}I_;Lb2D{hT5n!t&8F@leBVm zIfG9;1H*A}hXi43bR?tks=U%ZL(iDRrq)Nuk@&7)2x^^}K1Iz(gP#raZ9%;9ly%Cr zl#`z7Ue@PM8cHoY)<*C0?fc)@UFs%~!c+Yki}PV`A;N8;L@t@`)V-~2V?57??GE_~ zw$BY;a%{}|HKfnj_WMFT27D9kko4AHK~KAg)$@C>zNIMy;mgR>5txI5l#gs9G&>Y$ zlV@mcjY_rh*3)`z^?2E=FU~(uIVz<({ERU5DPw_Jl|^{5>9hCo_jwJKQ->PFHTI>A zP-g6pbg~jVHPtsv9w>R0i*N*E-Umx)eW0gyS)UXDQSEuF(_ZEr!uASDu|K`Wh5Lt( z;r*$Jxzl+M5enbR7!^=l8(AZ!{QJi_%z5(x^RYoEtT8nCMoii6aW^IX7zNfWGpWos zVuuXw_U-&~_?oKyGio(2UuseXicT+Tl1jhXdE}T1)mPhKf!_<<)#^ktOz{Y1p0-fx zsB>mGWWs0O1a7CN*X1YzgF;sn7Mj04f37NnnQa zo-n@4Z#CY~24asTt1jG2 z^0JOe7M|BDwD)s@YJ0XdY25>e-@3uS=#Lz)M2|h&SGBF6^77Fbzu?-D$!>rIZIr&H ziJ52(A&v4fJaqRP;+iDkmlbv4E!lUO^ZN`J(rfD6nvm$@i{bNEK-qP&gCt69o_F7R-^BJwq2?pa?8sAbny_9~i#pU+ zzjh(nu{hhG5G8HI)C2}OyP>M#w{uK+T=6MYj`hF2+wNtHnR?`AV0Inf*FQW~in_}> zgD?5^MI38-iXLS{D~!rs1iQ+-Ya4{SQ_hoo;E6pV6T?@C)-BR@|C%JklVg1!dU>!%2s^wf&5yUMAQypO*dH5iUA@|(-IZUdsmd#857C*{l^YzAG zh@T}*f_!`4r@8&Anx`m{ni0B&qxwISq?7KYpQ|8TypUZ$EMn8*KJ(cP)I*Civ2{&; zdzt;+IQCBGw;EzQ1!B#R^G`S6=ixHoFAu4Wwj2FRg3orZjbtU%^C?dizd9(}#tLb- zPIBt3MF^NY;?m~?eQk1!zejZIo?wf1&6&%@ydP7k@zc2*AIghXYYK(hhmG$1Qo58B zVteMoGtaUIBJVs}&9i9$Eb=(wQ0#PQh8rP7glw~G`PzYlL5C*3gSN@={7IC#k{c53 zH_7rA)OFWjszG{E?dhjDXI|p2TdBPHEP}j?A8Z37#atq-hp4AUMs!R3Iy-&+n7Bco z_C?4i$TluQpee9#)4&l%e^@}g~b5NxXws*oF zD6WUW^%J-Ia-njW62oopP-cYbY;VWQ&vbh8i*DsK6IM3!5nsN;PKtzU*@>^Xgpz*yZ@F!>(UcVgSXeE%b)!8*j7F)Gf3- zUCeaX%MbraGdORpQ;AY)x};nf9__>>Ph9*O-}#t`?SPM0r9n#JTK@xTV9)ca{4Qd+ z$*F0Jvp2_;fZ~t`6}w?aihoAogUhdy{{AZ-o-QTZzaR1PlyGitS0!L8=D((BrcF8j z{uwWc>wkFp?;_1S9cWmSV3(Kc7Sn#+uxwhX zP5!9%;ek^3@AHHrOSTCqml3ho_0M?b*pmoJ>m2NyF2+c*T>?b9|xtqoiD zb>4lWzBBiz-03{J;waiE__?mLh6~T*e4}iXxiK32U|lE5rxf)Y)@2`xZhaW$lL@1GvM4XOq@&Rg7d>7Ovb`>{$0{)e^h0jTD_I8PcrG z@N0i#TkTM+T~tBOf<#r*BLKW80o zrE}z~f9>#=R%(}F;$0vuetvI`K%*fQWK{HtNaS~K*?ECg{pBYkLoJ)n6z$Ow*=TQSmTTr~NY+F?N8UUs5P^exGP0jQoi~*$c}#9# z8r+2}r=N)l8)PMrQCF+n*!J4dvO9@08n@w4-oA%_NOs~oQ5bQsQMe~#Mhxpxj7VF~ zeD9Xh$@N>3Eg~ptJJKR+>>C%23w?@iCd^4g1}UUo(mF}dM(+TnyFy8D=2RZ(dZk29*9I?QKiF}jlBL$d!hoAN4hzHH;*uGvd zh{t~jNnjszw#*>(e3o!Xr5)y8QvV{N&-}fty^#^M!-N%5k!?Z6FK{qmj==ovY<2-O z*l*Lf(_eQUMxk%+T{qv3I>>8YDlO+WNJO2Gnz^-WxQ4!pEFZ-+oEI_FEspLN6PKutx4DD?_W9C6Svp;wai5E5S{?gr=9c8zfzoI5Z*el%l zdp49e)nvlyOBX2N@udStHa7=_d;~UY3*Jy~$e5KWmM#T63ac`&N$U$V@|!?73I_LaRJF^o*R!E}DmIdDN4`Y>lFfo$LKc?3mo?9zp3{lSD0O+xVYX8j@0QP~6K8jSqugmFy%iJ@0bG|j z_8w0DlA~~qtumCoOS&j;zrX(Hb|Ai9 z>Ty2E#JMYQ(8~199ZCb@)QZnXq02-h&D(;|OJ3A3{E%*?RGvPIL%L#~^ zk=6#1dh>?-k;ih|g1_$tZfSKAgLZ#FUhJVZPxp`FFBl5P%v|nEn>hvssWP@rq=k#>K*%-P+uq_DO7Gx=;6X*|MGoayFYg0-)5%Jem7U>7V_oaetwdA zeh{yRs2(Bt^ubL&{fCR*uOji8pY-pF#T_L1dY$Nfo&l zA(hUH_l%ta1sagyCBhiZ{EtXPnwW~B52g9At^$25zQMD8{Dy{5 z$Hj{pVpw>?r+L;jC6Db25mLZ)#})oMOqqi$_7+s=S-8_Uw7j_Zhir$Bf^EnI?;K2x z(#yNzlZ!50Okcp6e-i^;i@NqW*++kylwnb{ZxGrzDgC-&+MQvuSl0_}$Nv1fU8VMC z3B>AS$Rhta$MTD%)AztUfnc|c+mQPJF8pcwu`X#RZe;0}x+~A7OiXhc(x90B`$xt1 z=1?(@x02T{iI!Pi{*YE2pdPw}B^u!_NJURLajNw~L1A92OLF_{IpIzn=3~Fd^JFTW z&_}styRXw%C}o6DWE{F#LofWF*+HZQKHw@_nfT9EC!1X#9@{~o0=bls$T!oRPzzhc z{Z+ja=8|6<9_qtL0igusu)w;e!D(();!9NYDOG~`CEJ(8QM&5y`)I{2#U^9(VHZ+_ z%il?WDIZXmGzQF8^=*l5zqeVC^~YG3VEt0j;bk+k2O;KmI*{LF_8N&rV7*;hA(@L1 z1HsTpK8QITUG4nZU9H=L2j3zSdE}%XGZRVfxfIkX`?u}VI3|qK3U#Aa+XZ#vJ&c#D z9V?Qhyeq<8^y#Y7o-%&%MMOhFm06bxe;p*ddXEpzc2r`L$8Y-p)_Al=eZ5mo$nrIn z5DwK9ac9q~cWI9Nwi{^(et;pYUThcIaNwg-XvMT$xY)qtTjk&#llow_1s z9Aqdn)Qb4z-2SVKbo(08wY+=<0{3O%JS)lkAba_U(`B)ZWnSH_hFDsk;%|TA<+q*M z_!P@|{b+ha6Hex}PR3)n$NW4R)*8yBOk-|AozUay+?ZRbR<(+8+sOF&CZeB_ga-IsbM%5)BF!V40{}IkZBbk7^%*p-ze-5w z;;bp4@ZzqSg|x|6V?tW*qwZR$K+{6N_j2A=HA3>(fNpa-Y+bH#_!y(=-r@%hnHIC~ z^3Q!IV#00q37&Z3wyRmMo$Y-|Z2|u*(NB_a0_27PpJJ|S9QGu`?b?3O*?*K{1=44& zaPQxTEcDi~W#O?z5!M`?p@vBQ-^7I%eC64PaH_D~_W7K?67)4?#VEHiz0*^Xn*Nbg z>(hnj=ePR{eEOo#Y1P`Nv${Xdc(edH8(DZy%-9{~$Lr8P(t?P=2bWtqa;oidpr#{o z@V1U$h+TAKZs_35QK+_`s20a`!6eb31Mg<+c+rUCzzHAr@avPdo(6HAl7k|u^tfcf zTZxDJist9zk!8$@vd(piPIZ8XiNc$_nf*v`bW!@qtlJx{QO;$@)=RfD+T+4$r+YMi z<^4Rru+x+rH#A9DXHoX9n{A&wkWDs1a24-xc2lBuo81ZKudw% zS{U0vi6nIoxC~v(H=2*@5J{RJAAF-RPSb`k|3Kn&E!x3y7iu5@sU;u zu-$09AJYdfK*r`^Q&VTKKd+a`2^vNL-)Sm;_gD8T4nMrae7W)JJv^jz0`){Ir%1)JZV z&StoJwoI0rmQRU&+kO@3Km4XH^h&dyWbF&& z_C|>F(}LueOw5lngq=`n`E|PA_15$C?4rpYG)RgJno;&?X8i5*D_{x={C?poXP=s02J{p4bh;a;s}=lS$Vufm}|Zn&T$#lq#tK)_KVj}=E&La9=Pug&SuK(&54t@-ik9c`|>blKPND|Ro{}> z=AK9MzVthuDM{T-x7K1>3X?rsx*!4%w_jO447B@0?jr~xTeBfwjwFte1*yO$>@^Pu z8E=qoq3gOujXQN`rr%-xq)MVs38|*%uzuFU@A!6SWpK6F{N`xp^f!CE_gn<-+V&^6 zc3;GiQxprY1oOqWhP?a|KX{7+b8j-1c~0QDar0T-da}{9|8{MWfUYY?+K<|KTy|Qa zQyc3q3+fEfaL!V`k+AV?{`PMOS|xU)lc=n4K%-jt(_QnZxk%UCHdPg+Hi|Et2j^d) zC)kCm^Le9op><6LUXbQ4?C?F+5$4n2NyN^9eFGiPYnaZ)T(2@a!{h6xkv^BDDX(M} z`1G5^*P)MYWUW5pGbS^oEHtv;=)M%P!oV_8$%(KnmwT+llxk;4n=cRl?{ORA3LFfCV!6eU&wc8BMx z4YVq=13V2&@?+!FHzi&XRh8W|iAxS+C6r~IV9+v||E32lPtb-OV}6&&EytMDeXVXV zh~rVo6TZw-zjwaEViWOXc|c++xb8|x7I;`#dj8YVz?|r776V}*I7-yRze^KC^YH4e zcL9jHfQ^B8YJ zBh<99!B}NaxKxEp;v6d+;d87JE+qaT7VN1CI zC?R7StWxO!Q@3v7>!i@09@nk_D?YWWZbkdf;~to^q8a>FLbh;tTHgFW51S+zu%S@IH>e}#S4O#h<|#xcnJrvpZ|5^pajaWNCSLIHeP^sD1zheAOz+E~?0r5fHOX4s zGe31B-zJ=}Qo_(YnR@!+pH$T3({NyA6g96j-H{-SZ3(~4%+J`SO_OSrvGrUxXL;fR5pq|WZ@5xS88f8psII=k+R z;rhbzP3n79>jI&XRpPAmCrjH!LlWg*^@V0|$)}*9{VLOd-F>w@5m^nt{=>9&TDrFj z4(b=_ZpCux;k3{@iNcF%?6l2o>9QlKLp>WM8Y+^`wU^W$`1b@A50U$8Esu)i=RZH* zI{%Wsh!s~xO*>~lKaK9sH;0LnFcU&XY@sh#e|-!sw9(S9WP`9J{=a3T3D=~1-OCm^I3JfHQ@KxN7` zz5p-PFG+M_Ogs%eBW(|}({uvUlb`#q->q?JlTD|i*=|>z+pw=G(fOo-ZF4iI{}F{Y zvgaHK9zt+x&-u5#3D>JM#jOqNb$vJJ3Mr@6bUBK5^od-aw~#z8gXL|f>2e*(q?8Mx z!nt;{i@)EDv-@I30wkFEdQ2>lPl|1c3Jj3@_Z{WmM3gQu5o;dL#?=GLR89gTJzI6| z{Uy^%NS!@ha}bnH0&ALYe)fjQ;8tb5zJl`~af%UGY@1nVOU}+;{u2I? z3id}?lE>bg>;rtXq>oex7pzQeEc#SMG?s2nAUp~xAA6o~Yvis6t=x~&*x`|8|@R@E`S4r`YMf0-grDVu&f zHlG`LFi>;5%waVw*II^?U@M5D=+Gu^>C;v+Y}ly}lCm>h<(JoJ?0D+p(V}jRBukt; zNhK|lt6hID0VIvzs~B$@=5|Y{GXNC^R|0t1ezON(EtcW_t0$BbQ2d`|Sku|B*ULm5 zweK8|)%X2{y8tNPlN4~N=vCn{()jly-s{8%|MhvC>E^$mzrXuGm;X;j#*hn0ok=jq zh1xoP#H;E%hlXAt3Hu?o6-&mRp||A${{?!u+R^#W@sArHvx{;;Yo zjtUA@-b&>XoZ(gK%q8sWd^Ls}8u9R+9$sXRJPsArB41h?(n%R*xP_(j;G;DE&i(|z z{t)>K%PrGgnkRy@j|T*MVKVg(@0N#bq*G>9kuDLjv44z46e?+Rx2V~v*H7oa{Ca-v zq#1Dc40rhzDovxeDqlJFgk>gARYj9-g9NLL$}T%jR%(W1A6fMup5F`o`mQh`HP^mb z4*S^rs)slXhxH~FEbaNa|Ew5a&y%1SFd@xoW;Aqe?Q^S?8Z5h1lCake>3l zP$PFX*UYo(jB_vLuFsG%;Vtzbl;hUt(n}TmD z+|>{o?JR_M2I31RX}*rL;&(c@1@j4}NuK!lcU|~9TQwTF#Eg3tXrF|@D`c64YM5xJ zi^LxkPVH%yYv(q8oX?ECS~kvlS+4X&>!r=s<83dR8XkB&xzPD&>T0e*gb-b>nSrEJ zpLEfVl11InVZqDlaD;|tLM0CdliDDGxwCk>EX&%6Ke|TaC<`*aii;ZM#Z!%I*5pqL zi(8|#nbF~mWSm<|k14j-!Z^xv&kW{koV{1$ivS*28Pqr*$%|27NB7`Jl&pP%cvNIujwjn!{-Pi^*jA54N zqweo>e&_to^PJ~*p83Nc&iMGe*Xz1o*Xwm%@q%f^%0HGD^UucnQm2v$Q_}f4HjOmq z|BSju{Vg-1ok0+MbuDa`TFLv*S`Tu}_U~Xk_U&t3NT+ z@pM`{kKEqi>nh%M1^H9$J`UZ5K~ECGgiFiF=J8MZU(z*!ALY=MUli#5^lh1XYo5s< z!u8agrON#G@54Z1f5-3lalC7H5aWLO)1!`nK2r(LmnQklc-3)zRQwW4i*Bu1a6MuC z(W*%NlM$XOr&qKo$a#(`r`e-sv<=P3q+GS$`#GojQ*_zPExtSeh-r4g?&_pLi~pI* zUM{|_)E4?TT@y2FKEyp=|KayxnFoI}3ih5|TzJ}qncwe5b5dq!!kX_Dqivd87Tq}~ zzkKdOtnZ4i?X{!7@n0LIEk9uHye)-7g%@%KFJuWR{OCTcBS$XA_;BHm zEx1X8W~pQ?YGz+6qKEI4xRh#gwNH&pV~5ow^!9R1_ggIWt3NJMp3V>ePPY`DxZ}2oXFcMNi0noQkmV&_ zr;J^qa5wBZY8DEd-1v{3?B1fc8M88GH`Qjk?qIYLP4Fe`#7n5 zVJYLKmgcaisgPTF_xMg7&nZoMsb%djS-M@KFlh$U`WTnI*zFIbz_-N(LnIwj;b!}t z=YDVNCtHZOUVh&PtnTLz(@PCH+ICtEIsl#FqgUZ;b{w>w^u^8>UYXq1u{pyFG~)_Zwv!j- zky&baah}td^+;C*5~ib8)%jU4L!c+5Z%@Xf&~`+k4n=wwFeV;BmtuXJrML36q+=Z1 z4E*m41_b3Mg+7hHYRS8hX?V^{ zyIU3e(-Qm!LC!(fnZOU*tSFsn?_Yc#hiaYs4Pd7K1~9)@Jig~pEcl(f`6=T%*L=xj z+W@So&twZLLx5b>p4hvdQl;UhKUS_R48Wa_5Q13FHptrtj?Gdc{F|4;4G?~3Ok49h3PM)Ad zNm%J<%vdjg4;US<5o$j?&c!5fU?q6QttejvG5;aFD>53l9RBMeeChl@Snu}4{3k<{HGi{&72*tV{x&p$g z-+S;z>|c3V-vlo_J;R2bh!3cpKgULy=-mb;byn~-9~gdFT;E5 zr0fEO2q)gDfSJJUZ*J`!kgw&xv;2A87B$cLBw>J}bEOBIN0+kX^~V$@LIkX@dmF;N zor#(rC)MT#0xwbXOFPK$gOc8J^;}$`RCoNmH(dz0>6h1{P@3s zqc8Ov{pf@Du}X(~NDz7gkc|fRi*9CHWuWdc(7#;5s=zI9*;=^b*k(>-rB>i-TjG*? zZO|OGY0FFpbClpHcCAWi9!~ zZx<0}T|I;!5dF!Fu^N@^;;F4SwTM{&FYVqjb26yG<-J626@FL-0 zSsmjQlYWHi4J~4GwbbF~J*;j3-i_FR=Lu^g4mseg_+1e<6&k$X$c*1T_ZLGoh@ zer%=H5Q9<6(8+96+UXaqC-mh<;=C3ic)E;xftHhS^XC>o-ol_rk7BIBFjf(TBC{0C zU67B;npb3*I?zsfLeUp;mszt6X9AnEl1`Z*rNf=WDug-AOgfLRnir-M0dEoV9_Jl)z9l3G$HaDN1ZrdUsV;?kE%#GKO@VCnE%?a z8W+POaWwO;pl3>2M*&jY0+bnpY0Zgq$0?yc+XQ5U`034PW(3#A%|>t98D<$nn>}93 zxIC=6^(MwJnJz^V_EG|Iw=kwQV|9C4-G_|@9^^=mT71KoCTvsfg`YsQ%F-#bYo62uy0r#;=7-06-^snjta5)mB!Y8`vTrhq4qV|Ov^L*gv3j?E zMxe1aUdcS@z0%P*4|w;Cg#uIs*_`aHdE(KJJR$^kJ zgTw0lha(D5gDyuUWo$5z$FB{c)T|{xinxkp54TfYen`=3o0XPma(jGd#=j4JxoqZM zhTqCdE3ru5!6|4%ypm~hCcpB|b(I=sqb6OnIjRm;oL$LtN3PLcNo3dgU_?#ZZZ`Wd$-zB*xr5}ct3n* z3*r_Meb`d-t-iFuwje)gPNiwe+Av46Hxn*?t2fhFbxT*2Y5kJYyvi4yAcW}ON-LVj z>4aHv$OzIBshiuF3ePjbJ%oMoJLVJRe=Qh;>1$lf&`8U`yK+1XE6jtz;^tTE3(y;A zcZP5v*4lV1!%_<*9A<3#w?z&u3KMsCn?vYQ?!>QI$U}XcBwJ_u0|TAo3yxC_S5-1n z^QfDRat?IIE2SuW>Zr&YAU|n~R;TU=bQ&j7F-vw)-Td zlsxRk1}-H9Pu~I3AfBf@BFGhX@!Z+u;tv_l6x}!l%Uyciv}I4b=ZB{Uo?VIA4`}r4 z@nSuo$Sp73%a!{4iWqFn1a3o}6b4O6nKfQY)EaT|^gmwkcGZRgDO+&v_Pt;P|@)I!PIVWWCHnK84E zbx3_E9Ti*dGdm z#93KXfnn)V-}fMXqV$cEx41fFpthOcQF(gshK)DNX~4Sc-oZ<8J3Z+}Ui_MPzC{ zwhy|G6;%j)u#?t_Q|_l(xcRdK#~+_qa9erj!sa6SC!n{7eEm_1t2#-PG`> zX;=Fm3Pj7QDU57$bpF8Mx;2>q%3+?n)l(Aqvt@&_YCBj(V`WT^*3L9s2`e9h-q^GU z*=D*mLg6&ty|yS?p7`=yw9fXy#Ju*J=Fb$Ku6R>0yQ5sNO@mbfzeoJ-IC=wO->1eN zm1sbt`{Es0@^F-ooX$<7GRgnI9=r^9wFwiF1hQdW+lGY+R zIE>NJmi=pM{SO`vgn{00Fm+xg4DxA80z#LD&pg=c|=9la=UJ2%(;5|Ki4#wz0O!_n?i{3Y<{;j5C^Ct z-t|#N3mbFGUCKIP(_Q?TF+y?jqb5@Ycv`w0>@wFV&F1dr>3zt$#{#WJ*eB-w-}RP| zJ0;0IF`K9A{ewooebxmTJ_F%f8yOl~H%FG}Nd}RJ$=sK;3(1R7E)p=9>e-!hJ}>T^ zGmtZUMiMLLXqy*`iJyxX-l>@;U-fqGaUJ*9NJd^iTXtV40DQX_)C=0|5h?!i6RB%g ztI&6V@BBi3oR^`2u~rF+Px03&lVn?GY2Cfslob;=!xR{h)6Jv!0$b6iXKAU@q~yoG zE)&|>wvTjt7D=}JvA0U=STavnH!jvslpS5?2$@!k-41ReQ{C(nWc!bzz?QZ;y}y*= z-Qgm;R?+Vhf%Q+Qs;4w`o z&NdlPdXU&(YH0HL%Qq9dNGLy;sD}xRj2Tq53E#%^K<}?)M1%_9g%?LmM|=60Ss5Ef zBEQ%Cph=HqS8sebSDADbzOlr`_Ak3D4sNJm#T!?@UvfKIzjGDtY&u)qi}!5cK3yy! zuoY;jvA_~xF5V9xw{8_2o97(e(K5`e3cf2FGqC$vU1#R~m~zNf)i>`z#IrsgJl~l# zuH2K6%n>mSBAY(npYNA-{uT+xrC;)~`()F`oV9}P)tgD#H669h);agI_Vi}mqmMkS zHDjsq3juz)a{2G|t|QFzmO4c#rP5=GJ3+TBLt22WXe=1<&NQzR49)#=YuQFQM2EbW zPh!eAFdM~YnEUIBR8(}pn6U;W;*7(~6K4+A9yNi#K89(UI8=A;nEnVXIp!H0n&56B-syB_Fl5;fc$PmSw z8X1mC{7WWiVt3UFP>iF>PNyy^h^)U$+$Uvc3P4|A^~TjM`aVzs=Z*(=N%r!jF=O?Y zqwEm|rk4?7r7gF10!GN8dSsDx0)Ltz>8K*3OgyfGqwjU|k_JiE*Nc9j>VdVj?E z!I#KUBulbnQ>`U8+XVVUt1lRqGrw=)XX~)h_G#!8c0tM;I~=y!COeA0oVl}7cVvv7 ziV)pOGz6vO09UJyA!8g_4^KDKxq|g~VB+#40r87p(HCdbvGPJ{!^ZYQ`p9?huzbP4 zx5zSW3#_!wg|_(bqDPm#cWn2X?kd0(08^eZ*0LPc5Qbc#tlzHjX98^fd(q}Qd1{;? zHjH56z_(=|?^xHBCIL7;1VR{c_QciZ$nNu1{?}u3zj{fB`QKxsE^l> zQ;9eC_ebpy9(o29$u_4RsaeoEEbIr{I;5d8zt^K#!pS~IYgSL+b-I#k8lwR45OTjZKg8=CiT#- zzvcbbE;T+s)-E*&!yP|&lfL#G$B*MT0(hEuO{_i1HX3l}XHv_GZqck@I_TD1|Kcm7 zBlqH@IBNs1@!P{Wf2`b}rAW@6-vmXVud=ww6?Fj?GzD6PQA57Ygc{Vh->FJ%vmZH@ zuqobmDWCJ=y&W);`tygZ_Y3`|f0*MCYsU3Cju*bP3?q{q=7IoFPC5T6E1RAVMKWl$ z#X~-I?(#=?09%vbYjrrk{m&BN8+r;Za2lf`2|nbSYJ2%jrL9rgC<~jmqmVPvUEs6E=Z}8e0zC zDCoTv#KcXV`I)7_=^x6ww34z@;=W_Tc7qCf^fpp?R4p|)?fPT9Nw)fM;o*VR zQn97f+b7!q5Xpftg{T<}KluCGNNypIeHv87eyK$gl0XuOm#KJ%<#1svQ(uA|e8)T$ z9)?nPUBpc6+pZt={Or_r!V7;dCLct}6!W^&qOQZL0F747U!UekU%L?U%Fi4B!e6i; zPr&FGxKT!K6D&4%wu10koEwu*d?xKE;0L{cdTu zR?Scmm4R`}1RZ0=Ey`OIa3Z5iP`{A=ZeO(%zB3f8BcZ*UF<100Nc^(t>5vld_5{gC zOi4CPspExuPY<@(VA*;JTc1r-K%2)a&&=s^?8yKp@inK*G>g8p*J$~=HnDX+!Kn!VYYf~U>GJI+D(sG^4(WI89(^IODuE&}^HWVz%+ zTUo`J+fwgU#BN9VE6Kh+Gryhl&Cj(D{3c}ZhewAC@jVv@D!B-GSrtx3^?=^KnmBMG zWrHx>u)1*sYTqyP?4^|u4h@C)R@T>DKJ2tZdOG|$t4Xjxh~`QUn-^15Wn)rai$lsW zeV;(xXjo-x(ck;K``z-DHWyq?^-|xcXA`%nyr!EuK4JG3nBjTt{DZ+gi{cBfbIg9; zPAXL{C|RRn*P zH(SkaaT(_}Pclh^4h5HYqD(~2Za;EL1^-+ozKO26d$jC4VuODyyZ>gs0pc=VXf`jX zFT*NtAAft8JY+m7z0|2g6Bp4geYjj?9Io>IRvh%jvDIt~w_PEJc9XA)NY{^WZrg?B z9Ec9nrLY*~dmj{PyWA9jzRSRX&Ehq-WNZ?lDtSx176CtY4F#Q_vJXp-f3k{+z-))3 zSiZ~Z(GGemZzfn3>l^%h*%}bSv7dlmHhrs4veBvn1aK_kI}J~Bb(#QXWP}_X1C9U{X_{FJS?5r~%=PA#NXms^&waYR9-%3d;gl6$rWMA6 z@w9SjWI(O0Z41jnqebAVicJ=Dc|=iB1v{>i$cp0^QkHAHT%&S8EJFEm z)x98j^@xBDgW2!S1|M1|R`CG8H-4A8u|Il61;^1#)(Av>6@h^h*7v&hxyCLm?lyc> z$4uk>T^{Sb{S>BK#Lau_z^r?apO7c#c-Is4@O*)Q>|-lln$%}_nM%~@x{40bW++lK zeVK3PXk>k);D(H7xj)+kSU7jcvH)UVaj3Yb>v(qRe)W#&!d*ZGSkj>F z?s)J@e8jpMz1Vwr9(u~M0H4TfzdSDZY^#HnB>$1Dmb#aB=0z8x6DJ>Q1kWKj-y$t_ ze1w*IFR4!=%nwa~tistSzMc8y5N(Dfy;|d<1Mi~wW($%*nnKg|W0nMUAfa8@pK_ft zNoLm+1+nL;_Y4$Hm9qxnWP1MgUwufiMEDgPo!#v@2xz?2Q<=N^s+&i3!Q_*XxoznE zn~6Mq^13~U1^l{17wre9i_2RTIfLG;d-Iy1pH668qW9E4Y zOM6{`xQ(2e_SuitP6Q42O7Ot!ECc?-_lhXciG`5$9RR7v3!T>1HU`NB8CdlFUH%ey zl=)7kZ1OCl=?wIw_D=e>+}>b`?@oy>T7P4h0N4`k}#gt$^IcbSW= zBFf;~_h&mVD~}EsgR_$O=-)$6BsNrPt?>$)PD4++1RV&Nm7HmEHcFV;I{*;%a5rwL z@~Y`$ap5mPzSHk4yl3eXOQ%q)k32+UVu1e7$?~41YF#`pg{E!;BkE!;?G&1F?~k8D zj$PF?2gFh0C;vCg<>gJA*4gg^k3%n7X#9WW4Xm?_)9Pb&scVshFGz3A=u_J^qTodGh{Sa2)&O=pJ(<}zbzcmSBDq%4%E=b@=M)6}o6fdI zDZuUamU7uAO|NuVA(rjS^l#~@>y>56Oigb;YA_i(wNjnkLdOKYEVbpOv;|ZXI{uj7 z1X4#w^RAdI+y==|;czBn7zCoxEA0D4!U<`pOe8l+YtjpM&VcM8=~ixY9`@PR68Fg# zR+9AoCd6Yt5Zjs^P$@0%^OJ_nR>wa;7kgZ<1;AtXHj*;a6vK9m z(mIG_Ys0FBp_{9Mu)WX zB}Cn~0~EYH0^N(f!T;I}2hY|JKlr8SOCLvb?V;lH-3?_nS-<9cM{y3Bg?s!B{gaJ8qu+qb$3OthNoD zaD=<-GbQ%;2O&LkJ_K!ZawMmtR_##73)=5hJYlWH(DgmN?2Z;ak#cs%zn2@UhzTJo zX`oOzt0B;N%4y2b9C1~C^f7)MGgJZLK~vLM1gJO0!Qi(0;yFTWdf;Be&IiLhqkpga&%ly0{A!^eCy(-tYr+w(bG~b zm3GDmNHNRLN4UdMW~3htgl)k1#i!3l$>4^)^vE(?w0_J`3~lcEW|PCc2&lj)6F9|~ zNvO>7rV{${D|sK=s(+xj+Rnz{y*FB;ymRVay-0hav(CaKqf|tf(ljs50bH0+`BZA2w2)W7b{i;egi9U(x*KhLyFnyyOb#)ks!RpT#Po?>3VceCTC zw5|pUU$p7Hdeg`9v^E|CyJkMA>@NYy^tznt1*K?^xt~`5JF1t`IWJ>_X+=;ZPw+~a zsLU=PVP>sYy-j-$umC_Q?sgH&iXa+%6S8H&9HAs!$y7XUF24}`3BILSr}uR>{{ zWyWi@^j5H41&%NfVhI_?21Z{jn-^o6*$L|vp1nq~%wrfEq@HV&HwxX#TS*z~*>UA? z`4CDz!vwAmddCMIPi?H#brWX4mrTcL(d+`NUr9qCo_FX}`pqV}js<#+ccGkj6;Gy+ zW3*z{WGM&Xjke`8{~n|q-qY_kK+R+&N@de|%Hkk>tXj$p5vqy_T8(kqN+}x}_M)Yy z+eOJ>Xq*bvKw1d=;AWYnR4ccWnwp*- zn34|J<-ykBGLQ?Ew77Hus%*foYUulYDW(o4Fnr-%c@WY4(){XTTbo_ejKA_fHkwn$ zXtw_8Mc2xxeV&iQ&lX=RYQHgA^#utLgw7SM&YjyTXMc1$pd_R8+`411W5Es=&MlWs z84~di$>aPXKAz0lmpXYjR~3e~%xtlzB$-Wb=azW?GV(s368~mEpDN-@o1-gU71SRa zj(~oI7}waeSbf~;U*tOSXCx;Cu1U42 zrO-M#T`iLV!?3uD$U_7j{KZAv7sD1{D5q!k6fqyFQ)&U@FYX*ulC8qlhW|0GAtu~E zID7f*<^Eek+$!nOX=Zv;wgLIY@)hZtk+X|mgRW1RttR=78P4MR<|e44I+>aZQ#iUw zkG0T;s#b0fljMPpp%y>XIV^QWU6BqG)w9&EOkjTSxy~Cn%xJ&YR3oN7MOSoIJIu09 z)KdxIMcLvk)j`rQ)@ZZ14EnN|RI`K_b<2s*scmY)D%UGoLM!@dA!BGNaEbMBXdSi1 zF14vJ2?m-Bi@qq<$_%Z}ox+kLr3m9m&RG0hd^36j6UKd9Bn9}~%6X~%1{l8F0SX&~ z!Xs7>YS;?Q$){(M10O_y%CCMp|46e+0FJi#^Ys%<0@u8V!E?7Mi@oP5-ng5GD^|3R z>9dq`ll9n>0^Hv`V}Xlu-?wQFntV3k-&qDOy^7l4Uie{~g-$@$Hg^dvkaqWw+uC%n zZ|O^w#3z)h(3Lg54~ZfFUi}ZQojzgS1b8d*N$uqOfBvz%%>TH|#lLU--&icAl8OWV zy${9Q-ZzI7jnA%QSJCd_ySA`k-RM)jt@~0^-Y(d!!4JFs0X80?+;YO5GCtP&=Ckn7 z7lQM-QK-}?KB!Gn3+1@VlPJjGo003`QO!^@hJqvIK^0?U#LKqfd{+yYGw~gkLYnko?EYo~C&r%r7+xcR8welP!al|yf#Yw)F$ptx_^*>|)TWLu_r;jsF&^Jxz-a&bA&{ArcI zaK;z;OQU*u`gwuz2zvqJnQs32pfZO1k-GgWg%A>gxCbRu8wdnngl-sdB&T+vocg1$ z#cU)SX)=`T>UK~#Q?&ZZYih_Em^|qopl#pfA39kc-}hx*7R30$PMu#pNNw@}pZ=wt zF2ko7e-ll2?FC$qaK;!#-j3!4q=W z%CUEe3O@EOHK|Nme;nF$-9yw@?{o3}<|Whj{f>Zt4qB7| zw_-XVpcV^$Y_Ff0B;VH)jcBSx?U1jpPk>bved|45+z+T%?OiNA*XFOPiCm_y~2-bFqExHM$Q^DER-kAqu<0}Rw-fJ-hka~sYe z7)|5D+BjDq%W>f>k+tf@*-8J96@|`Q>V=8Afl5VWabv?ngH+x0hUiWmuSR2_*m)Dm zH1(Lt_EO@*|BDwq|C#u(zACo)8XQ^a!~zM@hrV80Z1<^ZN90FJ`#GAMvrQn98Co(2 z{eq6Y;0V2XI!-2DiqQG8Y=@_4{BAo_8(Cke+)BkCcTZtsRj9j-+1uzSl7k!N6CfOO zJZ+;(Eno=!nA)<}!&pJ-gzpgnBj8C~E~ZeK{ZQ8jo|SdFDHml}1-*uL5@DFb z&AJ?dss{F(*C+VYf=mq0jBY=FtZ2F*RJGpK=fiwwysa$6GsKwnL#fy;flV8a&Wj0o zaRZ))N0FJuUEX+HE9cg60IvlEIo-%2~G!zO>0SXz7o>r5`WBt7N`6FapH}UZts-bIo$d4rq$?;;v;N1n`*wA-R5D4LELRNB#A$N z!uxCAQYCO;=F;Z%El}A5Ik3f~_WT1Zbk4-@^6da=CLy{h?4Z`#oRiZ={V(dW6!Pbh zcUWV$iv2ru+`gY-s#x}}bqY7C#nb8B5(_Mf(iln6IR&yJAQbgFMW-LACv!TE75uE4 z{lcsKD6G11c4 zx=z|TE*jvFug+Pa$C?lHawt)0nBc%2I+7ww$ibF-_Nv*_6#8cw`fpxpto_4nc+bY$ zNh+K#mp1Z_e5ASC{4Qq9V6tXk4XfeRC@?Vg7`@OrX7nKnK74E&>4rpbDfiBx*Zwlf>H$QL5C z>3kh*p{0Is>Qi&C3?kpGKK4a<%}ffprmbdhR(+{fh}%MM;1J}UDH{L=b;V+2{9f)6 zFLIZDFP5L%+Yc}JEM8xg^gR~MLkyL9h25Fl4Lr%^uR#@!)(scVT5gTofkIU-l?yifkk6uKM=J!OZ4Qk6o2A36=C+?UMzM{?RyH$~&B0OcMV zX*G!cM{-CJ_En92Be$k!vNq4SD+z|5ASt*DDmZlaw65HyC8T6L4N3cVKJARSU4}0w zd$p!XwH`IO3i&r5K$M>1uZ7Ec$dp-ca_2eN*#K6kg{$KlKo`n(1^jtgYBGOW_9?c; zy6o5M?fc{>?KOP^(w)wnRz3WrdS;;JUKauIK8}bpaVu`yJ)iR~P8&?FJJ-5;P9^~- zl2?tG@zJG6WzZU5B!)i6HeUE}3mhx&LgrOx|6vp74H(U_eJZ$>G!Mi*0kp1jzk#-# z|7p}STrTw@o^}5&+Y@8&;lj_E%1$19uEZ7l_r|6@!?wlOYw-#Sm+pK!MDgCqSE}z{ zFUgj4DZgl9sowygl$)jLJKx?c9`K8qXDCrLEJl-Z`>JLXb^Y55^RJ0GRp8tCVev!= zbMpOhFeIVVIk5S!&||CWY&rR%Tr(P`$mM4ci_*9h#@^Ltt-b5`xEqiQjP8iwhY$Tk z79xw*y3~60ijGtw4qiR9OYXeLQ8z3pKf1jae+ybCVG%P*%wX2u?Ggjc(Q(QE{as>Y zC#`{d@0JrN!)pD=?CIf~b;ylW6Ew!__^<{V+_evl0@HwRHA>UY9d-}Vv}p(1rxmE1 zR%k=i(Yg9{-{zaHvO50>p&tAvgjzd$!9f?6=fKt!etsfkX}-^UmGJxt=ayz#UJKu| zzVJOG23iMNgz%$bT>yu&Ah;mb^9EI%fRJnIuA=VDg@El;>ky_&UzyG4v)Th}2>ZO!+b}kCYU9d3 zRr$UAzT1ag7=r|{KLOmWzMaWbD_KYaQ8p6K^6Gb3_FhEbn&F&U3fp-q>~H}`<^d*B zRgevFuV$J1!~Cfe-+uC4VU5?vadf3)ALiaqndj#^E;D!{ocVp-QM3C;_^w?TyqfxxTU1ta{nE`~o^g4j;uI|p3H$MAmT}iQk)xNj?%li0F-#<8M>v5DoA%ry~ z6YkGJp3sQYR|E(EFOvvuF@&~gYfW#jI$>9%e$1trQ67#P41JrwkHzoAz{`reK!L6s z3v?-Q68M(msPE;6|Fb^Ugn25a7I^gW=Nt2blPCV?zv!JzzRbD0I-fQxnK^}BH5+^2 z@k=tXNWTV?M`97mAHBIY%IqOL4*$OSZwQ)qm*G?KWlroHt7BNfQUJlP(D*8vBKR9$ zf0SZ8*uxJ1ykiZUWv)Z7KW+S+o{Kl-iOb!5FP4()zY95ckKd<~!005Jp_nJ-l z!IF-H#zWmI;yfGn8GiD?INNMfnx*CB1FMJM!+JkXm4q4>-!?45AdO8gcK)-eKF$5- z5!MU@S-!PL!==n%4pm*?a`7gt`<+l7o zhwxVhX!(UgfPFmk9KOsY&mlzK_IH3eoM}Rc63W{#Nt>Lfl=h2M5#1d*XT`lPZ%zu3 z^Tw^)!gQF3{~d1UtC*UO;1(U;57%JBBJV5 e&i^uT8b`F1g^DeHn$7QPLqk>fK2WFWpZ^6a9zF^H 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" -}