#!/usr/bin/env bash # Copyright (c) 2021-2025 community-scripts ORG # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE # ============================================================================== # CORE FUNCTIONS - LXC CONTAINER UTILITIES # ============================================================================== # # This file provides core utility functions for LXC container management # including colors, formatting, validation checks, message output, and # execution helpers used throughout the Community-Scripts ecosystem. # # Usage: # source <(curl -fsSL https://git.community-scripts.org/.../core.func) # load_functions # # ============================================================================== [[ -n "${_CORE_FUNC_LOADED:-}" ]] && return _CORE_FUNC_LOADED=1 # ============================================================================== # SECTION 1: INITIALIZATION & SETUP # ============================================================================== # ------------------------------------------------------------------------------ # load_functions() # # - Initializes all core utility groups (colors, formatting, icons, defaults) # - Ensures functions are loaded only once via __FUNCTIONS_LOADED flag # - Must be called at start of any script using these utilities # ------------------------------------------------------------------------------ load_functions() { [[ -n "${__FUNCTIONS_LOADED:-}" ]] && return __FUNCTIONS_LOADED=1 color formatting icons default_vars set_std_mode } # ------------------------------------------------------------------------------ # color() # # - Sets ANSI color codes for styled terminal output # - Variables: YW (yellow), YWB (yellow bright), BL (blue), RD (red) # GN (green), DGN (dark green), BGN (background green), CL (clear) # ------------------------------------------------------------------------------ color() { YW=$(echo "\033[33m") YWB=$'\e[93m' BL=$(echo "\033[36m") RD=$(echo "\033[01;31m") BGN=$(echo "\033[4;92m") GN=$(echo "\033[1;92m") DGN=$(echo "\033[32m") CL=$(echo "\033[m") } # ------------------------------------------------------------------------------ # color_spinner() # # - Sets ANSI color codes specifically for spinner animation # - Variables: CS_YW (spinner yellow), CS_YWB (spinner yellow bright), # CS_CL (spinner clear) # - Used by spinner() function to avoid color conflicts # ------------------------------------------------------------------------------ color_spinner() { CS_YW=$'\033[33m' CS_YWB=$'\033[93m' CS_CL=$'\033[m' } # ------------------------------------------------------------------------------ # formatting() # # - Defines formatting helpers for terminal output # - BFR: Backspace and clear line sequence # - BOLD: Bold text escape code # - TAB/TAB3: Indentation spacing # ------------------------------------------------------------------------------ formatting() { BFR="\\r\\033[K" BOLD=$(echo "\033[1m") HOLD=" " TAB=" " TAB3=" " } # ------------------------------------------------------------------------------ # icons() # # - Sets symbolic emoji icons used throughout user feedback # - Provides consistent visual indicators for success, error, info, etc. # - Icons: CM (checkmark), CROSS (error), INFO (info), HOURGLASS (wait), etc. # ------------------------------------------------------------------------------ icons() { CM="${TAB}✔️${TAB}" CROSS="${TAB}✖️${TAB}" DNSOK="✔️ " DNSFAIL="${TAB}✖️${TAB}" INFO="${TAB}💡${TAB}${CL}" OS="${TAB}🖥️${TAB}${CL}" OSVERSION="${TAB}🌟${TAB}${CL}" CONTAINERTYPE="${TAB}📦${TAB}${CL}" DISKSIZE="${TAB}💾${TAB}${CL}" CPUCORE="${TAB}🧠${TAB}${CL}" RAMSIZE="${TAB}🛠️${TAB}${CL}" SEARCH="${TAB}🔍${TAB}${CL}" VERBOSE_CROPPED="🔍${TAB}" VERIFYPW="${TAB}🔐${TAB}${CL}" CONTAINERID="${TAB}🆔${TAB}${CL}" HOSTNAME="${TAB}🏠${TAB}${CL}" BRIDGE="${TAB}🌉${TAB}${CL}" NETWORK="${TAB}📡${TAB}${CL}" GATEWAY="${TAB}🌐${TAB}${CL}" DISABLEIPV6="${TAB}🚫${TAB}${CL}" DEFAULT="${TAB}⚙️${TAB}${CL}" MACADDRESS="${TAB}🔗${TAB}${CL}" VLANTAG="${TAB}🏷️${TAB}${CL}" ROOTSSH="${TAB}🔑${TAB}${CL}" CREATING="${TAB}🚀${TAB}${CL}" ADVANCED="${TAB}🧩${TAB}${CL}" FUSE="${TAB}🗂️${TAB}${CL}" HOURGLASS="${TAB}⏳${TAB}" } # ------------------------------------------------------------------------------ # default_vars() # # - Sets default retry and wait variables used for system actions # - RETRY_NUM: Maximum number of retry attempts (default: 10) # - RETRY_EVERY: Seconds to wait between retries (default: 3) # - i: Counter variable initialized to RETRY_NUM # ------------------------------------------------------------------------------ default_vars() { RETRY_NUM=10 RETRY_EVERY=3 i=$RETRY_NUM } # ------------------------------------------------------------------------------ # set_std_mode() # # - Sets default verbose mode for script and OS execution # - If VERBOSE=yes: STD="" (show all output) # - If VERBOSE=no: STD="silent" (suppress output via silent() wrapper) # ------------------------------------------------------------------------------ set_std_mode() { if [ "${VERBOSE:-no}" = "yes" ]; then STD="" else STD="silent" fi } # ============================================================================== # SECTION 2: VALIDATION CHECKS # ============================================================================== # ------------------------------------------------------------------------------ # shell_check() # # - Verifies that the script is running under Bash shell # - Exits with error message if different shell is detected # - Required because scripts use Bash-specific features # ------------------------------------------------------------------------------ shell_check() { if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then clear msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." echo -e "\nExiting..." sleep 2 exit fi } # ------------------------------------------------------------------------------ # root_check() # # - Verifies script is running with root privileges # - Detects if executed via sudo (which can cause issues) # - Exits with error if not running as root directly # ------------------------------------------------------------------------------ root_check() { if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then clear msg_error "Please run this script as root." echo -e "\nExiting..." sleep 2 exit fi } # ------------------------------------------------------------------------------ # pve_check() # # - Validates Proxmox VE version compatibility # - Supported: PVE 8.0-8.9 and PVE 9.0 only # - Exits with error message if unsupported version detected # ------------------------------------------------------------------------------ pve_check() { local PVE_VER PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')" # Check for Proxmox VE 8.x: allow 8.0–8.9 if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then local MINOR="${BASH_REMATCH[1]}" if ((MINOR < 0 || MINOR > 9)); then msg_error "This version of Proxmox VE is not supported." msg_error "Supported: Proxmox VE version 8.0 – 8.9" exit 1 fi return 0 fi # Check for Proxmox VE 9.x: allow ONLY 9.0 if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then local MINOR="${BASH_REMATCH[1]}" if ((MINOR != 0)); then msg_error "This version of Proxmox VE is not yet supported." msg_error "Supported: Proxmox VE version 9.0" exit 1 fi return 0 fi # All other unsupported versions msg_error "This version of Proxmox VE is not supported." msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0" exit 1 } # ------------------------------------------------------------------------------ # arch_check() # # - Validates system architecture is amd64/x86_64 # - Exits with error message for unsupported architectures (e.g., ARM/PiMox) # - Provides link to ARM64-compatible scripts # ------------------------------------------------------------------------------ arch_check() { if [ "$(dpkg --print-architecture)" != "amd64" ]; then echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n" echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" echo -e "Exiting..." sleep 2 exit fi } # ------------------------------------------------------------------------------ # ssh_check() # # - Detects if script is running over SSH connection # - Warns user for external SSH connections (recommends Proxmox shell) # - Skips warning for local/same-subnet connections # - Does not abort execution, only warns # ------------------------------------------------------------------------------ ssh_check() { if [ -n "$SSH_CLIENT" ]; then local client_ip=$(awk '{print $1}' <<<"$SSH_CLIENT") local host_ip=$(hostname -I | awk '{print $1}') # Check if connection is local (Proxmox WebUI or same machine) # - localhost (127.0.0.1, ::1) # - same IP as host # - local network range (10.x, 172.16-31.x, 192.168.x) if [[ "$client_ip" == "127.0.0.1" || "$client_ip" == "::1" || "$client_ip" == "$host_ip" ]]; then return fi # Check if client is in same local network (optional, safer approach) local host_subnet=$(echo "$host_ip" | cut -d. -f1-3) local client_subnet=$(echo "$client_ip" | cut -d. -f1-3) if [[ "$host_subnet" == "$client_subnet" ]]; then return fi # Only warn for truly external connections msg_warn "Running via external SSH (client: $client_ip)." msg_warn "For better stability, consider using the Proxmox Shell (Console) instead." fi } # ============================================================================== # SECTION 3: EXECUTION HELPERS # ============================================================================== # ------------------------------------------------------------------------------ # get_active_logfile() # # - Returns the appropriate log file based on execution context # - BUILD_LOG: Host operations (container creation) # - INSTALL_LOG: Container operations (application installation) # - Fallback to BUILD_LOG if neither is set # ------------------------------------------------------------------------------ get_active_logfile() { if [[ -n "${INSTALL_LOG:-}" ]]; then echo "$INSTALL_LOG" elif [[ -n "${BUILD_LOG:-}" ]]; then echo "$BUILD_LOG" else # Fallback for legacy scripts echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log" fi } # Legacy compatibility: SILENT_LOGFILE points to active log SILENT_LOGFILE="$(get_active_logfile)" # ------------------------------------------------------------------------------ # silent() # # - Executes command with output redirected to active log file # - On error: displays last 10 lines of log and exits with original exit code # - Temporarily disables error trap to capture exit code correctly # - Sources explain_exit_code() for detailed error messages # ------------------------------------------------------------------------------ silent() { local cmd="$*" local caller_line="${BASH_LINENO[0]:-unknown}" local logfile="$(get_active_logfile)" set +Eeuo pipefail trap - ERR "$@" >>"$logfile" 2>&1 local rc=$? set -Eeuo pipefail trap 'error_handler' ERR if [[ $rc -ne 0 ]]; then # Source explain_exit_code if needed if ! declare -f explain_exit_code >/dev/null 2>&1; then source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) fi local explanation explanation="$(explain_exit_code "$rc")" printf "\e[?25h" msg_error "in line ${caller_line}: exit code ${rc} (${explanation})" msg_custom "→" "${YWB}" "${cmd}" if [[ -s "$logfile" ]]; then local log_lines=$(wc -l <"$logfile") echo "--- Last 10 lines of silent log ---" tail -n 10 "$logfile" echo "-----------------------------------" # Show how to view full log if there are more lines if [[ $log_lines -gt 10 ]]; then msg_custom "📋" "${YW}" "View full log (${log_lines} lines): ${logfile}" fi fi exit "$rc" fi } # ------------------------------------------------------------------------------ # spinner() # # - Displays animated spinner with rotating characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) # - Shows SPINNER_MSG alongside animation # - Runs in infinite loop until killed by stop_spinner() # - Uses color_spinner() colors for output # ------------------------------------------------------------------------------ spinner() { local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) local msg="${SPINNER_MSG:-Processing...}" local i=0 while true; do local index=$((i++ % ${#chars[@]})) printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}" sleep 0.1 done } # ------------------------------------------------------------------------------ # clear_line() # # - Clears current terminal line using tput or ANSI escape codes # - Moves cursor to beginning of line (carriage return) # - Erases from cursor to end of line # - Fallback to ANSI codes if tput not available # ------------------------------------------------------------------------------ clear_line() { tput cr 2>/dev/null || echo -en "\r" tput el 2>/dev/null || echo -en "\033[K" } # ------------------------------------------------------------------------------ # stop_spinner() # # - Stops running spinner process by PID # - Reads PID from SPINNER_PID variable or /tmp/.spinner.pid file # - Attempts graceful kill, then forced kill if needed # - Cleans up temp file and resets terminal state # - Unsets SPINNER_PID and SPINNER_MSG variables # ------------------------------------------------------------------------------ stop_spinner() { local pid="${SPINNER_PID:-}" [[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(/dev/null; then sleep 0.05 kill -9 "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true fi rm -f /tmp/.spinner.pid fi unset SPINNER_PID SPINNER_MSG stty sane 2>/dev/null || true } # ============================================================================== # SECTION 4: MESSAGE OUTPUT # ============================================================================== # ------------------------------------------------------------------------------ # msg_info() # # - Displays informational message with spinner animation # - Shows each unique message only once (tracked via MSG_INFO_SHOWN) # - In verbose/Alpine mode: shows hourglass icon instead of spinner # - Stops any existing spinner before starting new one # - Backgrounds spinner process and stores PID for later cleanup # ------------------------------------------------------------------------------ msg_info() { local msg="$1" [[ -z "$msg" ]] && return if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then declare -gA MSG_INFO_SHOWN=() fi [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return MSG_INFO_SHOWN["$msg"]=1 stop_spinner SPINNER_MSG="$msg" if is_verbose_mode || is_alpine; then local HOURGLASS="${TAB}⏳${TAB}" printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2 return fi color_spinner spinner & SPINNER_PID=$! echo "$SPINNER_PID" >/tmp/.spinner.pid disown "$SPINNER_PID" 2>/dev/null || true } # ------------------------------------------------------------------------------ # msg_ok() # # - Displays success message with checkmark icon # - Stops spinner and clears line before output # - Removes message from MSG_INFO_SHOWN to allow re-display # - Uses green color for success indication # ------------------------------------------------------------------------------ msg_ok() { local msg="$1" [[ -z "$msg" ]] && return stop_spinner clear_line echo -e "$CM ${GN}${msg}${CL}" unset MSG_INFO_SHOWN["$msg"] } # ------------------------------------------------------------------------------ # msg_error() # # - Displays error message with cross/X icon # - Stops spinner before output # - Uses red color for error indication # - Outputs to stderr # ------------------------------------------------------------------------------ msg_error() { stop_spinner local msg="$1" echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2 } # ------------------------------------------------------------------------------ # msg_warn() # # - Displays warning message with info/lightbulb icon # - Stops spinner before output # - Uses bright yellow color for warning indication # - Outputs to stderr # ------------------------------------------------------------------------------ msg_warn() { stop_spinner local msg="$1" echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2 } # ------------------------------------------------------------------------------ # msg_custom() # # - Displays custom message with user-defined symbol and color # - Arguments: symbol, color code, message text # - Stops spinner before output # - Useful for specialized status messages # ------------------------------------------------------------------------------ msg_custom() { local symbol="${1:-"[*]"}" local color="${2:-"\e[36m"}" local msg="${3:-}" [[ -z "$msg" ]] && return stop_spinner echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}" } # ------------------------------------------------------------------------------ # msg_debug() # # - Displays debug message with timestamp when var_full_verbose=1 # - Automatically enables var_verbose if not already set # - Shows date/time prefix for log correlation # - Uses bright yellow color for debug output # ------------------------------------------------------------------------------ msg_debug() { if [[ "${var_full_verbose:-0}" == "1" ]]; then [[ "${var_verbose:-0}" != "1" ]] && var_verbose=1 echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*" fi } # ------------------------------------------------------------------------------ # fatal() # # - Displays error message and immediately terminates script # - Sends SIGINT to current process to trigger error handler # - Use for unrecoverable errors that require immediate exit # ------------------------------------------------------------------------------ fatal() { msg_error "$1" kill -INT $$ } # ============================================================================== # SECTION 5: UTILITY FUNCTIONS # ============================================================================== # ------------------------------------------------------------------------------ # exit_script() # # - Called when user cancels an action # - Clears screen and displays exit message # - Exits with default exit code # ------------------------------------------------------------------------------ exit_script() { clear echo -e "\n${CROSS}${RD}User exited script${CL}\n" exit } # ------------------------------------------------------------------------------ # get_header() # # - Downloads and caches application header ASCII art # - Falls back to local cache if already downloaded # - Determines app type (ct/vm) from APP_TYPE variable # - Returns header content or empty string on failure # ------------------------------------------------------------------------------ get_header() { local app_name=$(echo "${APP,,}" | tr -d ' ') local app_type=${APP_TYPE:-ct} # Default zu 'ct' falls nicht gesetzt local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/${app_type}/headers/${app_name}" local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" mkdir -p "$(dirname "$local_header_path")" if [ ! -s "$local_header_path" ]; then if ! curl -fsSL "$header_url" -o "$local_header_path"; then return 1 fi fi cat "$local_header_path" 2>/dev/null || true } # ------------------------------------------------------------------------------ # header_info() # # - Displays application header ASCII art at top of screen # - Clears screen before displaying header # - Detects terminal width for formatting # - Returns silently if header not available # ------------------------------------------------------------------------------ header_info() { local app_name=$(echo "${APP,,}" | tr -d ' ') local header_content header_content=$(get_header "$app_name") || header_content="" clear local term_width term_width=$(tput cols 2>/dev/null || echo 120) if [ -n "$header_content" ]; then echo "$header_content" fi } # ------------------------------------------------------------------------------ # ensure_tput() # # - Ensures tput command is available for terminal control # - Installs ncurses-bin on Debian/Ubuntu or ncurses on Alpine # - Required for clear_line() and terminal width detection # ------------------------------------------------------------------------------ ensure_tput() { if ! command -v tput >/dev/null 2>&1; then if grep -qi 'alpine' /etc/os-release; then apk add --no-cache ncurses >/dev/null 2>&1 elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq >/dev/null apt-get install -y -qq ncurses-bin >/dev/null 2>&1 fi fi } # ------------------------------------------------------------------------------ # is_alpine() # # - Detects if running on Alpine Linux # - Checks var_os, PCT_OSTYPE, or /etc/os-release # - Returns 0 if Alpine, 1 otherwise # - Used to adjust behavior for Alpine-specific commands # ------------------------------------------------------------------------------ is_alpine() { local os_id="${var_os:-${PCT_OSTYPE:-}}" if [[ -z "$os_id" && -f /etc/os-release ]]; then os_id="$( . /etc/os-release 2>/dev/null echo "${ID:-}" )" fi [[ "$os_id" == "alpine" ]] } # ------------------------------------------------------------------------------ # is_verbose_mode() # # - Determines if script should run in verbose mode # - Checks VERBOSE and var_verbose variables # - Also returns true if not running in TTY (pipe/redirect scenario) # - Used by msg_info() to decide between spinner and static output # ------------------------------------------------------------------------------ is_verbose_mode() { local verbose="${VERBOSE:-${var_verbose:-no}}" local tty_status if [[ -t 2 ]]; then tty_status="interactive" else tty_status="not-a-tty" fi [[ "$verbose" != "no" || ! -t 2 ]] } # ============================================================================== # SECTION 6: CLEANUP & MAINTENANCE # ============================================================================== # ------------------------------------------------------------------------------ # cleanup_lxc() # # - Comprehensive cleanup of package managers, caches, and logs # - Supports Alpine (apk), Debian/Ubuntu (apt), and language package managers # - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP # - Truncates log files and vacuums systemd journal # - Run at end of container creation to minimize disk usage # ------------------------------------------------------------------------------ cleanup_lxc() { msg_info "Cleaning up" if is_alpine; then $STD apk cache clean || true rm -rf /var/cache/apk/* else $STD apt -y autoremove || true $STD apt -y autoclean || true $STD apt -y clean || true fi # Clear temp artifacts (keep sockets/FIFOs; ignore errors) find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true # Truncate writable log files silently (permission errors ignored) if command -v truncate >/dev/null 2>&1; then find /var/log -type f -writable -print0 2>/dev/null | xargs -0 -n1 truncate -s 0 2>/dev/null || true fi # Python pip if command -v pip &>/dev/null; then $STD pip cache purge || true; fi # Python uv if command -v uv &>/dev/null; then $STD uv cache clear || true; fi # Node.js npm if command -v npm &>/dev/null; then $STD npm cache clean --force || true; fi # Node.js yarn if command -v yarn &>/dev/null; then $STD yarn cache clean || true; fi # Node.js pnpm if command -v pnpm &>/dev/null; then $STD pnpm store prune || true; fi # Go if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi # Rust cargo if command -v cargo &>/dev/null; then $STD cargo clean || true; fi # Ruby gem if command -v gem &>/dev/null; then $STD gem cleanup || true; fi # Composer (PHP) if command -v composer &>/dev/null; then $STD composer clear-cache || true; fi if command -v journalctl &>/dev/null; then $STD journalctl --rotate || true $STD journalctl --vacuum-time=10m || true fi msg_ok "Cleaned" } # ------------------------------------------------------------------------------ # check_or_create_swap() # # - Checks if swap is active on system # - Offers to create swap file if none exists # - Prompts user for swap size in MB # - Creates /swapfile with specified size # - Activates swap immediately # - Returns 0 if swap active or successfully created, 1 if declined/failed # ------------------------------------------------------------------------------ check_or_create_swap() { msg_info "Checking for active swap" if swapon --noheadings --show | grep -q 'swap'; then msg_ok "Swap is active" return 0 fi msg_error "No active swap detected" read -p "Do you want to create a swap file? [y/N]: " create_swap create_swap="${create_swap,,}" # to lowercase if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then msg_info "Skipping swap file creation" return 1 fi read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then msg_error "Invalid size input. Aborting." return 1 fi local swap_file="/swapfile" msg_info "Creating ${swap_size_mb}MB swap file at $swap_file" if dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress && chmod 600 "$swap_file" && mkswap "$swap_file" && swapon "$swap_file"; then msg_ok "Swap file created and activated successfully" else msg_error "Failed to create or activate swap" return 1 fi } # ============================================================================== # SIGNAL TRAPS # ============================================================================== trap 'stop_spinner' EXIT INT TERM