Introduces a SESSION_ID variable for log file naming and tracking, updates log file paths to include timestamps and session IDs, and enhances error handling output to use custom message functions when available. Also improves log file management and user guidance for viewing logs, and refactors error handler to better support containerized environments.
507 lines
14 KiB
Bash
507 lines
14 KiB
Bash
#!/usr/bin/env bash
|
||
# Copyright (c) 2021-2025 community-scripts ORG
|
||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Loads core utility groups once (colors, formatting, icons, defaults).
|
||
# ------------------------------------------------------------------------------
|
||
|
||
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
|
||
_CORE_FUNC_LOADED=1
|
||
|
||
load_functions() {
|
||
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
||
__FUNCTIONS_LOADED=1
|
||
color
|
||
formatting
|
||
icons
|
||
default_vars
|
||
set_std_mode
|
||
# add more
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Sets ANSI color codes used for styled terminal output.
|
||
# ------------------------------------------------------------------------------
|
||
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")
|
||
}
|
||
|
||
# Special for spinner and colorized output via printf
|
||
color_spinner() {
|
||
CS_YW=$'\033[33m'
|
||
CS_YWB=$'\033[93m'
|
||
CS_CL=$'\033[m'
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Defines formatting helpers like tab, bold, and line reset sequences.
|
||
# ------------------------------------------------------------------------------
|
||
formatting() {
|
||
BFR="\\r\\033[K"
|
||
BOLD=$(echo "\033[1m")
|
||
HOLD=" "
|
||
TAB=" "
|
||
TAB3=" "
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Sets symbolic icons used throughout user feedback and prompts.
|
||
# ------------------------------------------------------------------------------
|
||
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}"
|
||
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Sets default retry and wait variables used for system actions.
|
||
# ------------------------------------------------------------------------------
|
||
default_vars() {
|
||
RETRY_NUM=10
|
||
RETRY_EVERY=3
|
||
i=$RETRY_NUM
|
||
#[[ "${VAR_OS:-}" == "unknown" ]]
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Sets default verbose mode for script and os execution.
|
||
# ------------------------------------------------------------------------------
|
||
set_std_mode() {
|
||
if [ "${VERBOSE:-no}" = "yes" ]; then
|
||
STD=""
|
||
else
|
||
STD="silent"
|
||
fi
|
||
}
|
||
|
||
SILENT_LOGFILE="/tmp/install-$(date +%Y%m%d_%H%M%S)_${SESSION_ID:-$(date +%s)}.log"
|
||
|
||
silent() {
|
||
local cmd="$*"
|
||
local caller_line="${BASH_LINENO[0]:-unknown}"
|
||
|
||
set +Eeuo pipefail
|
||
trap - ERR
|
||
|
||
"$@" >>"$SILENT_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 "$SILENT_LOGFILE" ]]; then
|
||
local log_lines=$(wc -l <"$SILENT_LOGFILE")
|
||
echo "--- Last 10 lines of silent log ---"
|
||
tail -n 10 "$SILENT_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): /tmp/install-*_${SESSION_ID:-*}.log"
|
||
fi
|
||
fi
|
||
|
||
exit "$rc"
|
||
fi
|
||
}
|
||
|
||
# Check if the shell is using bash
|
||
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
|
||
}
|
||
|
||
# Run as root only
|
||
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
|
||
}
|
||
|
||
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
|
||
# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+)
|
||
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
|
||
}
|
||
|
||
# This function checks the system architecture and exits if it's not "amd64".
|
||
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
|
||
# - Warns user and recommends using Proxmox shell
|
||
# - User can choose to continue or abort
|
||
# ------------------------------------------------------------------------------
|
||
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
|
||
}
|
||
|
||
# Function to download & save header files
|
||
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() {
|
||
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() {
|
||
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() {
|
||
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() {
|
||
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 ]]
|
||
}
|
||
|
||
fatal() {
|
||
msg_error "$1"
|
||
kill -INT $$
|
||
}
|
||
|
||
spinner() {
|
||
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||
local i=0
|
||
while true; do
|
||
local index=$((i++ % ${#chars[@]}))
|
||
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}"
|
||
sleep 0.1
|
||
done
|
||
}
|
||
|
||
clear_line() {
|
||
tput cr 2>/dev/null || echo -en "\r"
|
||
tput el 2>/dev/null || echo -en "\033[K"
|
||
}
|
||
|
||
stop_spinner() {
|
||
local pid="${SPINNER_PID:-}"
|
||
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(</tmp/.spinner.pid)
|
||
|
||
if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then
|
||
if kill "$pid" 2>/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
|
||
}
|
||
|
||
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() {
|
||
local msg="$1"
|
||
[[ -z "$msg" ]] && return
|
||
stop_spinner
|
||
clear_line
|
||
printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2
|
||
unset MSG_INFO_SHOWN["$msg"]
|
||
}
|
||
|
||
msg_error() {
|
||
stop_spinner
|
||
local msg="$1"
|
||
echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}"
|
||
}
|
||
|
||
msg_warn() {
|
||
stop_spinner
|
||
local msg="$1"
|
||
echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}"
|
||
}
|
||
|
||
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}"
|
||
}
|
||
|
||
function 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
|
||
}
|
||
|
||
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
|
||
|
||
rm -rf /tmp/* /var/tmp/*
|
||
|
||
# Remove temp files created by mktemp/tempfile
|
||
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
|
||
|
||
find /var/log -type f -exec truncate -s 0 {} +
|
||
|
||
# Python pip
|
||
if command -v pip &>/dev/null; then pip cache purge || true; fi
|
||
# Python uv
|
||
if command -v uv &>/dev/null; then uv cache clear || true; fi
|
||
# Node.js npm
|
||
if command -v npm &>/dev/null; then npm cache clean --force || true; fi
|
||
# Node.js yarn
|
||
if command -v yarn &>/dev/null; then yarn cache clean || true; fi
|
||
# Node.js pnpm
|
||
if command -v pnpm &>/dev/null; then pnpm store prune || true; fi
|
||
# Go
|
||
if command -v go &>/dev/null; then go clean -cache -modcache || true; fi
|
||
# Rust cargo
|
||
if command -v cargo &>/dev/null; then cargo clean || true; fi
|
||
# Ruby gem
|
||
if command -v gem &>/dev/null; then gem cleanup || true; fi
|
||
# Composer (PHP)
|
||
if command -v composer &>/dev/null; then composer clear-cache || true; fi
|
||
|
||
if command -v journalctl &>/dev/null; then
|
||
$STD journalctl --rotate
|
||
$STD journalctl --vacuum-time=10m
|
||
fi
|
||
msg_ok "Cleaned"
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
trap 'stop_spinner' EXIT INT TERM
|