mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-08-30 08:11:00 +02:00
Merge remote-tracking branch 'origin/main' into refactor/tools-func-stability
# Conflicts: # misc/tools.func
This commit is contained in:
+48
-39
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: tteck (tteckster)
|
||||
# Co-Author: MickLesk
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
@@ -11,6 +11,39 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
|
||||
load_functions
|
||||
catch_errors
|
||||
|
||||
# Persist diagnostics setting inside container (exported from build.func)
|
||||
# so addon scripts running later can find the user's choice
|
||||
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
|
||||
mkdir -p /usr/local/community-scripts
|
||||
echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics
|
||||
fi
|
||||
|
||||
# Get LXC IP address (must be called INSIDE container, after network is up)
|
||||
get_lxc_ip
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# post_progress_to_api()
|
||||
#
|
||||
# - Lightweight progress ping from inside the container
|
||||
# - Updates the existing telemetry record status
|
||||
# - Arguments:
|
||||
# * $1: status (optional, default: "configuring")
|
||||
# - Signals that the installation is actively progressing (not stuck)
|
||||
# - Fire-and-forget: never blocks or fails the script
|
||||
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
|
||||
# ------------------------------------------------------------------------------
|
||||
post_progress_to_api() {
|
||||
command -v curl &>/dev/null || return 0
|
||||
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
||||
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
||||
|
||||
local progress_status="${1:-configuring}"
|
||||
|
||||
curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\"}" &>/dev/null || true
|
||||
}
|
||||
|
||||
# This function enables IPv6 if it's not disabled and sets verbose mode
|
||||
verb_ip6() {
|
||||
set_std_mode # Set STD mode based on VERBOSE
|
||||
@@ -31,42 +64,6 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
|
||||
setting_up_container() {
|
||||
msg_info "Setting up Container OS"
|
||||
@@ -86,6 +83,7 @@ setting_up_container() {
|
||||
fi
|
||||
msg_ok "Set up Container OS"
|
||||
msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}"
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
|
||||
@@ -118,8 +116,18 @@ network_check() {
|
||||
update_os() {
|
||||
msg_info "Updating Container OS"
|
||||
$STD apk -U upgrade
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
||||
local tools_content
|
||||
tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) || {
|
||||
msg_error "Failed to download tools.func"
|
||||
exit 6
|
||||
}
|
||||
source /dev/stdin <<<"$tools_content"
|
||||
if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then
|
||||
msg_error "tools.func loaded but incomplete — missing expected functions"
|
||||
exit 6
|
||||
fi
|
||||
msg_ok "Updated Container OS"
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# This function modifies the message of the day (motd) and SSH settings
|
||||
@@ -144,6 +152,7 @@ motd_ssh() {
|
||||
# Start the sshd service
|
||||
$STD /etc/init.d/sshd start
|
||||
fi
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# Validate Timezone for some LXC's
|
||||
@@ -180,5 +189,5 @@ EOF
|
||||
|
||||
echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
@@ -34,11 +34,19 @@ net_resolves() {
|
||||
}
|
||||
|
||||
ensure_usr_local_bin_persist() {
|
||||
# Login shells: /etc/profile.d/
|
||||
local PROFILE_FILE="/etc/profile.d/10-localbin.sh"
|
||||
if [ ! -f "$PROFILE_FILE" ]; then
|
||||
echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE"
|
||||
chmod +x "$PROFILE_FILE"
|
||||
fi
|
||||
|
||||
# Non-login shells (pct enter): /root/.profile and /root/.bashrc
|
||||
for rc_file in /root/.profile /root/.bashrc; do
|
||||
if [ -f "$rc_file" ] && ! grep -q '/usr/local/bin' "$rc_file"; then
|
||||
echo 'export PATH="/usr/local/bin:$PATH"' >>"$rc_file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
download_with_progress() {
|
||||
|
||||
+1231
-167
File diff suppressed because it is too large
Load Diff
+1803
-310
File diff suppressed because it is too large
Load Diff
+208
-5
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: community-scripts ORG
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
|
||||
# Revision: 1
|
||||
@@ -28,13 +28,210 @@
|
||||
# ==============================================================================
|
||||
# These can be overridden before sourcing this library
|
||||
|
||||
# Disable 'unbound variable' errors for this library (restored at end)
|
||||
_OLD_SET_STATE=$(set +o | grep -E 'set -(e|u|o)')
|
||||
set +u
|
||||
|
||||
CLOUDINIT_DEFAULT_USER="${CLOUDINIT_DEFAULT_USER:-root}"
|
||||
CLOUDINIT_DNS_SERVERS="${CLOUDINIT_DNS_SERVERS:-1.1.1.1 8.8.8.8}"
|
||||
CLOUDINIT_SEARCH_DOMAIN="${CLOUDINIT_SEARCH_DOMAIN:-local}"
|
||||
CLOUDINIT_SSH_KEYS="${CLOUDINIT_SSH_KEYS:-/root/.ssh/authorized_keys}"
|
||||
CLOUDINIT_SSH_KEYS="${CLOUDINIT_SSH_KEYS:-}" # Empty by default - user must explicitly provide keys
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: HELPER FUNCTIONS
|
||||
# SECTION 2: SSH KEY DISCOVERY AND SELECTION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_ssh_extract_keys_from_file - Extracts valid SSH public keys from a file
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_ssh_extract_keys_from_file() {
|
||||
local file="$1"
|
||||
[[ -f "$file" && -r "$file" ]] || return 0
|
||||
grep -E '^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-)' "$file" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_ssh_discover_files - Scans standard paths for SSH keys
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_ssh_discover_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[@]}"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _ci_ssh_build_choices - Builds whiptail checklist from SSH key files
|
||||
#
|
||||
# Sets: CI_SSH_CHOICES (array), CI_SSH_COUNT (int), CI_SSH_MAPFILE (path)
|
||||
# ------------------------------------------------------------------------------
|
||||
function _ci_ssh_build_choices() {
|
||||
local -a files=("$@")
|
||||
CI_SSH_CHOICES=()
|
||||
CI_SSH_COUNT=0
|
||||
CI_SSH_MAPFILE="$(mktemp)"
|
||||
local id key typ fp cmt base
|
||||
|
||||
for f in "${files[@]}"; do
|
||||
[[ -f "$f" && -r "$f" ]] || continue
|
||||
base="$(basename -- "$f")"
|
||||
# Skip known_hosts and private keys
|
||||
case "$base" in
|
||||
known_hosts | known_hosts.* | config) continue ;;
|
||||
id_*) [[ "$f" != *.pub ]] && continue ;;
|
||||
esac
|
||||
|
||||
while IFS= read -r key; do
|
||||
[[ -n "$key" ]] || continue
|
||||
|
||||
typ=""
|
||||
fp=""
|
||||
cmt=""
|
||||
read -r _typ _b64 _cmt <<<"$key"
|
||||
typ="${_typ:-key}"
|
||||
cmt="${_cmt:-}"
|
||||
|
||||
# Get 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
|
||||
|
||||
# Shorten long comments
|
||||
[[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
|
||||
|
||||
CI_SSH_COUNT=$((CI_SSH_COUNT + 1))
|
||||
id="K${CI_SSH_COUNT}"
|
||||
echo "${id}|${key}" >>"$CI_SSH_MAPFILE"
|
||||
CI_SSH_CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
|
||||
done < <(_ci_ssh_extract_keys_from_file "$f")
|
||||
done
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# configure_cloudinit_ssh_keys - Interactive SSH key selection for Cloud-Init
|
||||
#
|
||||
# Usage: configure_cloudinit_ssh_keys
|
||||
# Sets: CLOUDINIT_SSH_KEYS (path to temporary file with selected keys)
|
||||
# ------------------------------------------------------------------------------
|
||||
function configure_cloudinit_ssh_keys() {
|
||||
local backtitle="Proxmox VE Helper Scripts"
|
||||
local ssh_key_mode
|
||||
|
||||
# Create temp file for selected keys
|
||||
CLOUDINIT_SSH_KEYS_TEMP="$(mktemp)"
|
||||
: >"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
|
||||
# Discover keys and build choices
|
||||
IFS=$'\0' read -r -d '' -a _def_files < <(_ci_ssh_discover_files && printf '\0')
|
||||
_ci_ssh_build_choices "${_def_files[@]}"
|
||||
local default_key_count="$CI_SSH_COUNT"
|
||||
|
||||
if [[ "$default_key_count" -gt 0 ]]; then
|
||||
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
|
||||
"Provision SSH keys for Cloud-Init VM:" 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 SSH keys (password auth only)" 3>&1 1>&2 2>&3) || return 1
|
||||
else
|
||||
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
|
||||
"No host keys detected. Choose:" 12 72 3 \
|
||||
"manual" "Paste a single public key" \
|
||||
"folder" "Scan another folder (path or glob)" \
|
||||
"none" "No SSH keys (password auth only)" 3>&1 1>&2 2>&3) || return 1
|
||||
fi
|
||||
|
||||
case "$ssh_key_mode" in
|
||||
found)
|
||||
# Show checklist with individual keys
|
||||
local selection
|
||||
selection=$(whiptail --backtitle "$backtitle" --title "SELECT SSH KEYS" \
|
||||
--checklist "Select one or more keys to import:" 20 140 10 "${CI_SSH_CHOICES[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
|
||||
for tag in $selection; do
|
||||
tag="${tag%\"}"
|
||||
tag="${tag#\"}"
|
||||
local line
|
||||
line=$(grep -E "^${tag}\|" "$CI_SSH_MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
done
|
||||
local imported
|
||||
imported=$(wc -l <"$CLOUDINIT_SSH_KEYS_TEMP")
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}${imported} key(s) selected${CL}"
|
||||
;;
|
||||
manual)
|
||||
local pubkey
|
||||
pubkey=$(whiptail --backtitle "$backtitle" --title "PASTE SSH PUBLIC KEY" \
|
||||
--inputbox "Paste your SSH public key (ssh-rsa, ssh-ed25519, etc.):" 10 76 3>&1 1>&2 2>&3) || return 1
|
||||
if [[ -n "$pubkey" ]]; then
|
||||
echo "$pubkey" >"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}1 key added manually${CL}"
|
||||
else
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}none (empty input)${CL}"
|
||||
CLOUDINIT_SSH_KEYS=""
|
||||
rm -f "$CLOUDINIT_SSH_KEYS_TEMP" "$CI_SSH_MAPFILE" 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
folder)
|
||||
local glob_path
|
||||
glob_path=$(whiptail --backtitle "$backtitle" --title "SCAN FOLDER/GLOB" \
|
||||
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub):" 10 72 3>&1 1>&2 2>&3) || return 1
|
||||
if [[ -n "$glob_path" ]]; then
|
||||
shopt -s nullglob
|
||||
local -a _scan_files=($glob_path)
|
||||
shopt -u nullglob
|
||||
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
|
||||
_ci_ssh_build_choices "${_scan_files[@]}"
|
||||
if [[ "$CI_SSH_COUNT" -gt 0 ]]; then
|
||||
local folder_selection
|
||||
folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \
|
||||
--checklist "Select key(s) to import:" 20 140 10 "${CI_SSH_CHOICES[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||
for tag in $folder_selection; do
|
||||
tag="${tag%\"}"
|
||||
tag="${tag#\"}"
|
||||
local line
|
||||
line=$(grep -E "^${tag}\|" "$CI_SSH_MAPFILE" | head -n1 | cut -d'|' -f2-)
|
||||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
done
|
||||
local imported
|
||||
imported=$(wc -l <"$CLOUDINIT_SSH_KEYS_TEMP")
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}${imported} key(s) from folder${CL}"
|
||||
else
|
||||
whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
|
||||
fi
|
||||
else
|
||||
whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
none | *)
|
||||
echo -e "${ROOTSSH:- 🔑 }${BOLD}${DGN}SSH Keys: ${BGN}none (password auth only)${CL}"
|
||||
CLOUDINIT_SSH_KEYS=""
|
||||
rm -f "$CLOUDINIT_SSH_KEYS_TEMP" "$CI_SSH_MAPFILE" 2>/dev/null
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Cleanup mapfile
|
||||
rm -f "$CI_SSH_MAPFILE" 2>/dev/null
|
||||
|
||||
# Set the variable for setup_cloud_init to use
|
||||
if [[ -s "$CLOUDINIT_SSH_KEYS_TEMP" ]]; then
|
||||
CLOUDINIT_SSH_KEYS="$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
else
|
||||
CLOUDINIT_SSH_KEYS=""
|
||||
rm -f "$CLOUDINIT_SSH_KEYS_TEMP"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: HELPER FUNCTIONS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -144,9 +341,10 @@ function setup_cloud_init() {
|
||||
local cipassword=$(openssl rand -base64 16)
|
||||
qm set "$vmid" --cipassword "$cipassword" >/dev/null
|
||||
|
||||
# Add SSH keys if available
|
||||
if [ -f "$CLOUDINIT_SSH_KEYS" ]; then
|
||||
# Add SSH keys only if explicitly provided (not auto-imported from host)
|
||||
if [ -n "${CLOUDINIT_SSH_KEYS:-}" ] && [ -f "$CLOUDINIT_SSH_KEYS" ]; then
|
||||
qm set "$vmid" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null 2>&1 || true
|
||||
_ci_msg_info "SSH keys imported from: $CLOUDINIT_SSH_KEYS"
|
||||
fi
|
||||
|
||||
# Configure network
|
||||
@@ -459,6 +657,11 @@ export -f wait_for_cloud_init 2>/dev/null || true
|
||||
export -f validate_ip_cidr 2>/dev/null || true
|
||||
export -f validate_ip 2>/dev/null || true
|
||||
|
||||
# Restore previous shell options if they were saved
|
||||
if [ -n "${_OLD_SET_STATE:-}" ]; then
|
||||
eval "$_OLD_SET_STATE"
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 7: EXAMPLES & DOCUMENTATION
|
||||
# ==============================================================================
|
||||
|
||||
+899
-71
File diff suppressed because it is too large
Load Diff
+390
-143
@@ -2,7 +2,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -27,100 +27,146 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# explain_exit_code()
|
||||
#
|
||||
# - Maps numeric exit codes to human-readable error descriptions
|
||||
# - Supports:
|
||||
# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
|
||||
# * Package manager errors (APT, DPKG: 100, 101, 255)
|
||||
# * Node.js/npm errors (243-249, 254)
|
||||
# * Python/pip/uv errors (210-212)
|
||||
# * PostgreSQL errors (231-234)
|
||||
# * MySQL/MariaDB errors (241-244)
|
||||
# * MongoDB errors (251-254)
|
||||
# * Proxmox custom codes (200-231)
|
||||
# - Returns description string for given exit code
|
||||
# - Canonical version is defined in api.func (sourced before this file)
|
||||
# - This section only provides a fallback if api.func was not loaded
|
||||
# - See api.func SECTION 1 for the authoritative exit code mappings
|
||||
# ------------------------------------------------------------------------------
|
||||
explain_exit_code() {
|
||||
local code="$1"
|
||||
case "$code" in
|
||||
# --- Generic / Shell ---
|
||||
1) echo "General error / Operation not permitted" ;;
|
||||
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
|
||||
126) echo "Command invoked cannot execute (permission problem?)" ;;
|
||||
127) echo "Command not found" ;;
|
||||
128) echo "Invalid argument to exit" ;;
|
||||
130) echo "Terminated by Ctrl+C (SIGINT)" ;;
|
||||
137) echo "Killed (SIGKILL / Out of memory?)" ;;
|
||||
139) echo "Segmentation fault (core dumped)" ;;
|
||||
143) echo "Terminated (SIGTERM)" ;;
|
||||
|
||||
# --- Package manager / APT / DPKG ---
|
||||
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
|
||||
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
|
||||
255) echo "DPKG: Fatal internal error" ;;
|
||||
|
||||
# --- Node.js / npm / pnpm / yarn ---
|
||||
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
|
||||
245) echo "Node.js: Invalid command-line option" ;;
|
||||
246) echo "Node.js: Internal JavaScript Parse Error" ;;
|
||||
247) echo "Node.js: Fatal internal error" ;;
|
||||
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||
249) echo "Node.js: Inspector error" ;;
|
||||
254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||
|
||||
# --- Python / pip / uv ---
|
||||
210) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
||||
211) echo "Python: Dependency resolution failed" ;;
|
||||
212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
||||
|
||||
# --- PostgreSQL ---
|
||||
231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
|
||||
232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
|
||||
233) echo "PostgreSQL: Database does not exist" ;;
|
||||
234) echo "PostgreSQL: Fatal error in query / syntax" ;;
|
||||
|
||||
# --- MySQL / MariaDB ---
|
||||
241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
|
||||
242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
|
||||
243) echo "MySQL/MariaDB: Database does not exist" ;;
|
||||
244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
|
||||
|
||||
# --- MongoDB ---
|
||||
251) echo "MongoDB: Connection failed (server not running)" ;;
|
||||
252) echo "MongoDB: Authentication failed (bad user/password)" ;;
|
||||
253) echo "MongoDB: Database not found" ;;
|
||||
254) echo "MongoDB: Fatal query error" ;;
|
||||
|
||||
# --- Proxmox Custom Codes ---
|
||||
200) echo "Proxmox: Failed to create lock file" ;;
|
||||
203) echo "Proxmox: Missing CTID variable" ;;
|
||||
204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
|
||||
205) echo "Proxmox: Invalid CTID (<100)" ;;
|
||||
206) echo "Proxmox: CTID already in use" ;;
|
||||
207) echo "Proxmox: Password contains unescaped special characters" ;;
|
||||
208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;;
|
||||
209) echo "Proxmox: Container creation failed" ;;
|
||||
210) echo "Proxmox: Cluster not quorate" ;;
|
||||
211) echo "Proxmox: Timeout waiting for template lock" ;;
|
||||
212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;;
|
||||
213) echo "Proxmox: Storage type does not support 'rootdir' content" ;;
|
||||
214) echo "Proxmox: Not enough storage space" ;;
|
||||
215) echo "Proxmox: Container created but not listed (ghost state)" ;;
|
||||
216) echo "Proxmox: RootFS entry missing in config" ;;
|
||||
217) echo "Proxmox: Storage not accessible" ;;
|
||||
219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
|
||||
224) echo "Proxmox: PBS storage is for backups only" ;;
|
||||
218) echo "Proxmox: Template file corrupted or incomplete" ;;
|
||||
220) echo "Proxmox: Unable to resolve template path" ;;
|
||||
221) echo "Proxmox: Template file not readable" ;;
|
||||
222) echo "Proxmox: Template download failed" ;;
|
||||
223) echo "Proxmox: Template not available after download" ;;
|
||||
225) echo "Proxmox: No template available for OS/Version" ;;
|
||||
231) echo "Proxmox: LXC stack upgrade failed" ;;
|
||||
|
||||
# --- Default ---
|
||||
*) echo "Unknown error" ;;
|
||||
esac
|
||||
}
|
||||
if ! declare -f explain_exit_code &>/dev/null; then
|
||||
explain_exit_code() {
|
||||
local code="$1"
|
||||
case "$code" in
|
||||
1) echo "General error / Operation not permitted" ;;
|
||||
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
|
||||
3) echo "General syntax or argument error" ;;
|
||||
10) echo "Docker / privileged mode required (unsupported environment)" ;;
|
||||
4) echo "curl: Feature not supported or protocol error" ;;
|
||||
5) echo "curl: Could not resolve proxy" ;;
|
||||
6) echo "curl: DNS resolution failed (could not resolve host)" ;;
|
||||
7) echo "curl: Failed to connect (network unreachable / host down)" ;;
|
||||
8) echo "curl: Server reply error (FTP/SFTP or apk untrusted key)" ;;
|
||||
16) echo "curl: HTTP/2 framing layer error" ;;
|
||||
18) echo "curl: Partial file (transfer not completed)" ;;
|
||||
22) echo "curl: HTTP error returned (404, 429, 500+)" ;;
|
||||
23) echo "curl: Write error (disk full or permissions)" ;;
|
||||
24) echo "curl: Write to local file failed" ;;
|
||||
25) echo "curl: Upload failed" ;;
|
||||
26) echo "curl: Read error on local file (I/O)" ;;
|
||||
27) echo "curl: Out of memory (memory allocation failed)" ;;
|
||||
28) echo "curl: Operation timeout (network slow or server not responding)" ;;
|
||||
30) echo "curl: FTP port command failed" ;;
|
||||
32) echo "curl: FTP SIZE command failed" ;;
|
||||
33) echo "curl: HTTP range error" ;;
|
||||
34) echo "curl: HTTP post error" ;;
|
||||
35) echo "curl: SSL/TLS handshake failed (certificate error)" ;;
|
||||
36) echo "curl: FTP bad download resume" ;;
|
||||
39) echo "curl: LDAP search failed" ;;
|
||||
44) echo "curl: Internal error (bad function call order)" ;;
|
||||
45) echo "curl: Interface error (failed to bind to specified interface)" ;;
|
||||
46) echo "curl: Bad password entered" ;;
|
||||
47) echo "curl: Too many redirects" ;;
|
||||
48) echo "curl: Unknown command line option specified" ;;
|
||||
51) echo "curl: SSL peer certificate or SSH host key verification failed" ;;
|
||||
52) echo "curl: Empty reply from server (got nothing)" ;;
|
||||
55) echo "curl: Failed sending network data" ;;
|
||||
56) echo "curl: Receive error (connection reset by peer)" ;;
|
||||
57) echo "curl: Unrecoverable poll/select error (system I/O failure)" ;;
|
||||
59) echo "curl: Couldn't use specified SSL cipher" ;;
|
||||
61) echo "curl: Bad/unrecognized transfer encoding" ;;
|
||||
63) echo "curl: Maximum file size exceeded" ;;
|
||||
75) echo "Temporary failure (retry later)" ;;
|
||||
78) echo "curl: Remote file not found (404 on FTP/file)" ;;
|
||||
79) echo "curl: SSH session error (key exchange/auth failed)" ;;
|
||||
92) echo "curl: HTTP/2 stream error (protocol violation)" ;;
|
||||
95) echo "curl: HTTP/3 layer error" ;;
|
||||
64) echo "Usage error (wrong arguments)" ;;
|
||||
65) echo "Data format error (bad input data)" ;;
|
||||
66) echo "Input file not found (cannot open input)" ;;
|
||||
67) echo "User not found (addressee unknown)" ;;
|
||||
68) echo "Host not found (hostname unknown)" ;;
|
||||
69) echo "Service unavailable" ;;
|
||||
70) echo "Internal software error" ;;
|
||||
71) echo "System error (OS-level failure)" ;;
|
||||
72) echo "Critical OS file missing" ;;
|
||||
73) echo "Cannot create output file" ;;
|
||||
74) echo "I/O error" ;;
|
||||
76) echo "Remote protocol error" ;;
|
||||
77) echo "Permission denied" ;;
|
||||
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
|
||||
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
|
||||
102) echo "APT: Lock held by another process (dpkg/apt still running)" ;;
|
||||
124) echo "Command timed out (timeout command)" ;;
|
||||
125) echo "Command failed to start (Docker daemon or execution error)" ;;
|
||||
126) echo "Command invoked cannot execute (permission problem?)" ;;
|
||||
127) echo "Command not found" ;;
|
||||
128) echo "Invalid argument to exit" ;;
|
||||
129) echo "Killed by SIGHUP (terminal closed / hangup)" ;;
|
||||
130) echo "Aborted by user (SIGINT)" ;;
|
||||
131) echo "Killed by SIGQUIT (core dumped)" ;;
|
||||
132) echo "Killed by SIGILL (illegal CPU instruction)" ;;
|
||||
134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;;
|
||||
137) echo "Killed (SIGKILL / Out of memory?)" ;;
|
||||
139) echo "Segmentation fault (core dumped)" ;;
|
||||
141) echo "Broken pipe (SIGPIPE - output closed prematurely)" ;;
|
||||
143) echo "Terminated (SIGTERM)" ;;
|
||||
144) echo "Killed by signal 16 (SIGUSR1 / SIGSTKFLT)" ;;
|
||||
146) echo "Killed by signal 18 (SIGTSTP)" ;;
|
||||
150) echo "Systemd: Service failed to start" ;;
|
||||
151) echo "Systemd: Service unit not found" ;;
|
||||
152) echo "Permission denied (EACCES)" ;;
|
||||
153) echo "Build/compile failed (make/gcc/cmake)" ;;
|
||||
154) echo "Node.js: Native addon build failed (node-gyp)" ;;
|
||||
160) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
||||
161) echo "Python: Dependency resolution failed" ;;
|
||||
162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
||||
170) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
|
||||
171) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
|
||||
172) echo "PostgreSQL: Database does not exist" ;;
|
||||
173) echo "PostgreSQL: Fatal error in query / syntax" ;;
|
||||
180) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
|
||||
181) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
|
||||
182) echo "MySQL/MariaDB: Database does not exist" ;;
|
||||
183) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
|
||||
190) echo "MongoDB: Connection failed (server not running)" ;;
|
||||
191) echo "MongoDB: Authentication failed (bad user/password)" ;;
|
||||
192) echo "MongoDB: Database not found" ;;
|
||||
193) echo "MongoDB: Fatal query error" ;;
|
||||
200) echo "Proxmox: Failed to create lock file" ;;
|
||||
203) echo "Proxmox: Missing CTID variable" ;;
|
||||
204) echo "Proxmox: Missing PCT_OSTYPE variable" ;;
|
||||
205) echo "Proxmox: Invalid CTID (<100)" ;;
|
||||
206) echo "Proxmox: CTID already in use" ;;
|
||||
207) echo "Proxmox: Password contains unescaped special characters" ;;
|
||||
208) echo "Proxmox: Invalid configuration (DNS/MAC/Network format)" ;;
|
||||
209) echo "Proxmox: Container creation failed" ;;
|
||||
210) echo "Proxmox: Cluster not quorate" ;;
|
||||
211) echo "Proxmox: Timeout waiting for template lock" ;;
|
||||
212) echo "Proxmox: Storage type 'iscsidirect' does not support containers (VMs only)" ;;
|
||||
213) echo "Proxmox: Storage type does not support 'rootdir' content" ;;
|
||||
214) echo "Proxmox: Not enough storage space" ;;
|
||||
215) echo "Proxmox: Container created but not listed (ghost state)" ;;
|
||||
216) echo "Proxmox: RootFS entry missing in config" ;;
|
||||
217) echo "Proxmox: Storage not accessible" ;;
|
||||
218) echo "Proxmox: Template file corrupted or incomplete" ;;
|
||||
219) echo "Proxmox: CephFS does not support containers - use RBD" ;;
|
||||
220) echo "Proxmox: Unable to resolve template path" ;;
|
||||
221) echo "Proxmox: Template file not readable" ;;
|
||||
222) echo "Proxmox: Template download failed" ;;
|
||||
223) echo "Proxmox: Template not available after download" ;;
|
||||
224) echo "Proxmox: PBS storage is for backups only" ;;
|
||||
225) echo "Proxmox: No template available for OS/Version" ;;
|
||||
231) echo "Proxmox: LXC stack upgrade failed" ;;
|
||||
239) echo "npm/Node.js: Unexpected runtime error or dependency failure" ;;
|
||||
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
|
||||
245) echo "Node.js: Invalid command-line option" ;;
|
||||
246) echo "Node.js: Internal JavaScript Parse Error" ;;
|
||||
247) echo "Node.js: Fatal internal error" ;;
|
||||
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||
249) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||
255) echo "DPKG: Fatal internal error" ;;
|
||||
*) echo "Unknown error" ;;
|
||||
esac
|
||||
}
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: ERROR HANDLERS
|
||||
@@ -153,10 +199,25 @@ error_handler() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Stop spinner and restore cursor FIRST — before any output
|
||||
# This prevents spinner text overlapping with error messages
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
printf "\e[?25h"
|
||||
|
||||
local explanation
|
||||
explanation="$(explain_exit_code "$exit_code")"
|
||||
|
||||
printf "\e[?25h"
|
||||
# ALWAYS report failure to API immediately - don't wait for container checks
|
||||
# This ensures we capture failures that occur before/after container exists
|
||||
if declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
|
||||
else
|
||||
# Container context: post_update_to_api not available (api.func not sourced)
|
||||
# Send status directly via curl so container failures are never lost
|
||||
_send_abort_telemetry "$exit_code" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Use msg_error if available, fallback to echo
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
@@ -184,55 +245,92 @@ error_handler() {
|
||||
active_log="$SILENT_LOGFILE"
|
||||
fi
|
||||
|
||||
# If active_log points to a container-internal path that doesn't exist on host,
|
||||
# fall back to BUILD_LOG (host-side log)
|
||||
if [[ -n "$active_log" && ! -s "$active_log" && -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
|
||||
active_log="$BUILD_LOG"
|
||||
fi
|
||||
|
||||
# Show last log lines if available
|
||||
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
echo "--- Last 20 lines of silent log ---"
|
||||
echo -e "\n${TAB}--- Last 20 lines of log ---"
|
||||
tail -n 20 "$active_log"
|
||||
echo "-----------------------------------"
|
||||
echo -e "${TAB}-----------------------------------\n"
|
||||
fi
|
||||
|
||||
# Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG)
|
||||
if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then
|
||||
# CONTAINER CONTEXT: Copy log and create flag file for host
|
||||
local container_log="/root/.install-${SESSION_ID:-error}.log"
|
||||
cp "$active_log" "$container_log" 2>/dev/null || true
|
||||
# Detect context: Container (INSTALL_LOG set + inside container /root) vs Host
|
||||
if [[ -n "${INSTALL_LOG:-}" && -f "${INSTALL_LOG:-}" && -d /root ]]; then
|
||||
# CONTAINER CONTEXT: Copy log and create flag file for host
|
||||
local container_log="/root/.install-${SESSION_ID:-error}.log"
|
||||
cp "${INSTALL_LOG}" "$container_log" 2>/dev/null || true
|
||||
|
||||
# Create error flag file with exit code for host detection
|
||||
echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true
|
||||
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
msg_custom "📋" "${YW}" "Log saved to: ${container_log}"
|
||||
else
|
||||
echo -e "${YW}Log saved to:${CL} ${BL}${container_log}${CL}"
|
||||
fi
|
||||
else
|
||||
# HOST CONTEXT: Show local log path and offer container cleanup
|
||||
# Create error flag file with exit code for host detection
|
||||
echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true
|
||||
# Log path is shown by host as combined log - no need to show container path
|
||||
else
|
||||
# HOST CONTEXT: Show local log path and offer container cleanup
|
||||
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
msg_custom "📋" "${YW}" "Full log: ${active_log}"
|
||||
else
|
||||
echo -e "${YW}Full log:${CL} ${BL}${active_log}${CL}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Offer to remove container if it exists (build errors after container creation)
|
||||
if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
|
||||
echo ""
|
||||
# Offer to remove container if it exists (build errors after container creation)
|
||||
if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
|
||||
echo ""
|
||||
if declare -f msg_custom >/dev/null 2>&1; then
|
||||
echo -en "${TAB}❓${TAB}${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||||
else
|
||||
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||||
fi
|
||||
|
||||
if read -t 60 -r response; then
|
||||
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
|
||||
echo -e "\n${YW}Removing container ${CTID}${CL}"
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
elif [[ "$response" =~ ^[Nn]$ ]]; then
|
||||
echo -e "\n${YW}Container ${CTID} kept for debugging${CL}"
|
||||
if read -t 60 -r response </dev/tty; then
|
||||
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
if declare -f msg_info >/dev/null 2>&1; then
|
||||
msg_info "Removing container ${CTID}"
|
||||
else
|
||||
echo -e "${YW}Removing container ${CTID}${CL}"
|
||||
fi
|
||||
else
|
||||
# Timeout - auto-remove
|
||||
echo -e "\n${YW}No response - auto-removing container${CL}"
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
if declare -f msg_ok >/dev/null 2>&1; then
|
||||
msg_ok "Container ${CTID} removed"
|
||||
else
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
fi
|
||||
elif [[ "$response" =~ ^[Nn]$ ]]; then
|
||||
echo ""
|
||||
if declare -f msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "Container ${CTID} kept for debugging"
|
||||
else
|
||||
echo -e "${YW}Container ${CTID} kept for debugging${CL}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Timeout - auto-remove
|
||||
echo ""
|
||||
if declare -f msg_info >/dev/null 2>&1; then
|
||||
msg_info "No response - removing container ${CTID}"
|
||||
else
|
||||
echo -e "${YW}No response - removing container ${CTID}${CL}"
|
||||
fi
|
||||
pct stop "$CTID" &>/dev/null || true
|
||||
pct destroy "$CTID" &>/dev/null || true
|
||||
if declare -f msg_ok >/dev/null 2>&1; then
|
||||
msg_ok "Container ${CTID} removed"
|
||||
else
|
||||
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force one final status update attempt after cleanup
|
||||
# This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
|
||||
if declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "failed" "$exit_code" "force"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -240,19 +338,127 @@ error_handler() {
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 3: SIGNAL HANDLERS
|
||||
# SECTION 3: TELEMETRY & CLEANUP HELPERS FOR SIGNAL HANDLERS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _send_abort_telemetry()
|
||||
#
|
||||
# - Sends failure/abort status to telemetry API
|
||||
# - Works in BOTH host context (post_update_to_api available) and
|
||||
# container context (only curl available, api.func not sourced)
|
||||
# - Container context is critical: without this, container-side failures
|
||||
# and signal exits are never reported, leaving records stuck in
|
||||
# "installing" or "configuring" forever
|
||||
# - Arguments: $1 = exit_code
|
||||
# ------------------------------------------------------------------------------
|
||||
_send_abort_telemetry() {
|
||||
local exit_code="${1:-1}"
|
||||
# Try full API function first (host context - api.func sourced)
|
||||
if declare -f post_update_to_api &>/dev/null; then
|
||||
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
# Fallback: direct curl (container context - api.func NOT sourced)
|
||||
# This is the ONLY way containers can report failures to telemetry
|
||||
command -v curl &>/dev/null || return 0
|
||||
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
||||
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
||||
|
||||
# Collect last 20 log lines for error diagnosis (best-effort)
|
||||
local error_text=""
|
||||
if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then
|
||||
error_text=$(tail -n 20 "$INSTALL_LOG" 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\\/\\\\/g; s/"/\\"/g; s/\r//g' | tr '\n' '|' | sed 's/|$//' | tr -d '\000-\010\013\014\016-\037\177') || true
|
||||
fi
|
||||
|
||||
# Calculate duration if start time is available
|
||||
local duration=""
|
||||
if [[ -n "${DIAGNOSTICS_START_TIME:-}" ]]; then
|
||||
duration=$(($(date +%s) - DIAGNOSTICS_START_TIME))
|
||||
fi
|
||||
|
||||
# Build JSON payload with error context
|
||||
local payload
|
||||
payload="{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-${app:-unknown}}\",\"status\":\"failed\",\"exit_code\":${exit_code}"
|
||||
[[ -n "$error_text" ]] && payload="${payload},\"error\":\"${error_text}\""
|
||||
[[ -n "$duration" ]] && payload="${payload},\"duration\":${duration}"
|
||||
payload="${payload}}"
|
||||
|
||||
local api_url="${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}"
|
||||
|
||||
# 2 attempts (retry once on failure) — original had no retry
|
||||
local attempt
|
||||
for attempt in 1 2; do
|
||||
if curl -fsS -m 5 -X POST "$api_url" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
[[ $attempt -eq 1 ]] && sleep 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# _stop_container_if_installing()
|
||||
#
|
||||
# - Stops the LXC container if we're in the install phase
|
||||
# - Prevents orphaned container processes when the host exits due to a signal
|
||||
# (SSH disconnect, Ctrl+C, SIGTERM) — without this, the container keeps
|
||||
# running and may send "configuring" status AFTER the host already sent
|
||||
# "failed", leaving records permanently stuck in "configuring"
|
||||
# - Only acts when:
|
||||
# * CONTAINER_INSTALLING flag is set (during lxc-attach in build_container)
|
||||
# * CTID is set (container was created)
|
||||
# * pct command is available (we're on the Proxmox host, not inside a container)
|
||||
# - Does NOT destroy the container — just stops it for potential debugging
|
||||
# ------------------------------------------------------------------------------
|
||||
_stop_container_if_installing() {
|
||||
[[ "${CONTAINER_INSTALLING:-}" == "true" ]] || return 0
|
||||
[[ -n "${CTID:-}" ]] || return 0
|
||||
command -v pct &>/dev/null || return 0
|
||||
pct stop "$CTID" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 4: SIGNAL HANDLERS
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_exit()
|
||||
#
|
||||
# - EXIT trap handler
|
||||
# - Cleans up lock files if lockfile variable is set
|
||||
# - Exits with captured exit code
|
||||
# - Always runs on script termination (success or failure)
|
||||
# - EXIT trap handler — runs on EVERY script termination
|
||||
# - Catches orphaned "installing"/"configuring" records:
|
||||
# * If post_to_api sent "installing" but post_update_to_api never ran
|
||||
# * Reports final status to prevent records stuck forever
|
||||
# - Best-effort log collection for failed installs
|
||||
# - Stops orphaned container processes on failure
|
||||
# - Cleans up lock files
|
||||
# ------------------------------------------------------------------------------
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
|
||||
# Report orphaned "installing" records to telemetry API
|
||||
# Catches ALL exit paths: errors, signals, AND clean exits where
|
||||
# post_to_api was called but post_update_to_api was never called
|
||||
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
_send_abort_telemetry "$exit_code"
|
||||
elif declare -f post_update_to_api >/dev/null 2>&1; then
|
||||
post_update_to_api "done" "0" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Best-effort log collection on failure (non-critical, telemetry already sent)
|
||||
if [[ $exit_code -ne 0 ]] && declare -f ensure_log_on_host >/dev/null 2>&1; then
|
||||
ensure_log_on_host 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Stop orphaned container if we're in the install phase and exiting with error
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
_stop_container_if_installing
|
||||
fi
|
||||
|
||||
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
||||
exit "$exit_code"
|
||||
}
|
||||
@@ -261,14 +467,23 @@ on_exit() {
|
||||
# on_interrupt()
|
||||
#
|
||||
# - SIGINT (Ctrl+C) trap handler
|
||||
# - Displays "Interrupted by user" message
|
||||
# - Reports status FIRST (time-critical: container may be dying)
|
||||
# - Stops orphaned container to prevent "configuring" ghost records
|
||||
# - Exits with code 130 (128 + SIGINT=2)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_interrupt() {
|
||||
# Stop spinner and restore cursor before any output
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
printf "\e[?25h" 2>/dev/null || true
|
||||
|
||||
_send_abort_telemetry "130"
|
||||
_stop_container_if_installing
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Interrupted by user (SIGINT)"
|
||||
msg_error "Interrupted by user (SIGINT)" 2>/dev/null || true
|
||||
else
|
||||
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
|
||||
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" 2>/dev/null || true
|
||||
fi
|
||||
exit 130
|
||||
}
|
||||
@@ -277,21 +492,51 @@ on_interrupt() {
|
||||
# on_terminate()
|
||||
#
|
||||
# - SIGTERM trap handler
|
||||
# - Displays "Terminated by signal" message
|
||||
# - Reports status FIRST (time-critical: process being killed)
|
||||
# - Stops orphaned container to prevent "configuring" ghost records
|
||||
# - Exits with code 143 (128 + SIGTERM=15)
|
||||
# - Triggered by external process termination
|
||||
# ------------------------------------------------------------------------------
|
||||
on_terminate() {
|
||||
# Stop spinner and restore cursor before any output
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
printf "\e[?25h" 2>/dev/null || true
|
||||
|
||||
_send_abort_telemetry "143"
|
||||
_stop_container_if_installing
|
||||
if declare -f msg_error >/dev/null 2>&1; then
|
||||
msg_error "Terminated by signal (SIGTERM)"
|
||||
msg_error "Terminated by signal (SIGTERM)" 2>/dev/null || true
|
||||
else
|
||||
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
|
||||
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" 2>/dev/null || true
|
||||
fi
|
||||
exit 143
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# on_hangup()
|
||||
#
|
||||
# - SIGHUP trap handler (SSH disconnect, terminal closed)
|
||||
# - CRITICAL: This was previously MISSING from catch_errors(), causing
|
||||
# container processes to become orphans on SSH disconnect — the #1 cause
|
||||
# of records stuck in "installing" and "configuring" states
|
||||
# - Reports status via direct curl (terminal is already closed, no output)
|
||||
# - Stops orphaned container to prevent ghost records
|
||||
# - Exits with code 129 (128 + SIGHUP=1)
|
||||
# ------------------------------------------------------------------------------
|
||||
on_hangup() {
|
||||
# Stop spinner (no cursor restore needed — terminal is already gone)
|
||||
if declare -f stop_spinner >/dev/null 2>&1; then
|
||||
stop_spinner 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_send_abort_telemetry "129"
|
||||
_stop_container_if_installing
|
||||
exit 129
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 4: INITIALIZATION
|
||||
# SECTION 5: INITIALIZATION
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -303,10 +548,11 @@ on_terminate() {
|
||||
# * set -o pipefail: Pipeline fails if any command fails
|
||||
# * set -u: (optional) Exit on undefined variable (if STRICT_UNSET=1)
|
||||
# - Sets up traps:
|
||||
# * ERR → error_handler
|
||||
# * EXIT → on_exit
|
||||
# * INT → on_interrupt
|
||||
# * TERM → on_terminate
|
||||
# * ERR → error_handler (script errors)
|
||||
# * EXIT → on_exit (any termination — cleanup + orphan detection)
|
||||
# * INT → on_interrupt (Ctrl+C)
|
||||
# * TERM → on_terminate (kill / systemd stop)
|
||||
# * HUP → on_hangup (SSH disconnect / terminal closed)
|
||||
# - Call this function early in every script
|
||||
# ------------------------------------------------------------------------------
|
||||
catch_errors() {
|
||||
@@ -319,4 +565,5 @@ catch_errors() {
|
||||
trap on_exit EXIT
|
||||
trap on_interrupt INT
|
||||
trap on_terminate TERM
|
||||
trap on_hangup HUP
|
||||
}
|
||||
|
||||
+57
-4
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: tteck (tteckster)
|
||||
# Co-Author: MickLesk
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
@@ -37,6 +37,39 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
|
||||
load_functions
|
||||
catch_errors
|
||||
|
||||
# Persist diagnostics setting inside container (exported from build.func)
|
||||
# so addon scripts running later can find the user's choice
|
||||
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
|
||||
mkdir -p /usr/local/community-scripts
|
||||
echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics
|
||||
fi
|
||||
|
||||
# Get LXC IP address (must be called INSIDE container, after network is up)
|
||||
get_lxc_ip
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# post_progress_to_api()
|
||||
#
|
||||
# - Lightweight progress ping from inside the container
|
||||
# - Updates the existing telemetry record status
|
||||
# - Arguments:
|
||||
# * $1: status (optional, default: "configuring")
|
||||
# - Signals that the installation is actively progressing (not stuck)
|
||||
# - Fire-and-forget: never blocks or fails the script
|
||||
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
|
||||
# ------------------------------------------------------------------------------
|
||||
post_progress_to_api() {
|
||||
command -v curl &>/dev/null || return 0
|
||||
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
||||
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
||||
|
||||
local progress_status="${1:-configuring}"
|
||||
|
||||
curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\"}" &>/dev/null || true
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# SECTION 2: NETWORK & CONNECTIVITY
|
||||
# ==============================================================================
|
||||
@@ -76,6 +109,13 @@ EOF
|
||||
# ------------------------------------------------------------------------------
|
||||
setting_up_container() {
|
||||
msg_info "Setting up Container OS"
|
||||
|
||||
# Fix Debian 13 LXC template bug where / is owned by nobody
|
||||
# Only attempt in privileged containers (unprivileged cannot chown /)
|
||||
if [[ "$(stat -c '%U' /)" != "root" ]]; then
|
||||
(chown root:root / 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
for ((i = RETRY_NUM; i > 0; i--)); do
|
||||
if [ "$(hostname -I)" != "" ]; then
|
||||
break
|
||||
@@ -93,6 +133,7 @@ setting_up_container() {
|
||||
msg_ok "Set up Container OS"
|
||||
#msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)"
|
||||
msg_ok "Network Connected: ${BL}$(hostname -I)"
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -162,7 +203,7 @@ network_check() {
|
||||
fi
|
||||
|
||||
set -e
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
trap 'error_handler' ERR
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
@@ -192,12 +233,22 @@ fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/apt-proxy-detect.sh
|
||||
fi
|
||||
$STD apt-get update
|
||||
apt_update_safe
|
||||
$STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade
|
||||
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||
msg_ok "Updated Container OS"
|
||||
post_progress_to_api
|
||||
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
||||
local tools_content
|
||||
tools_content=$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) || {
|
||||
msg_error "Failed to download tools.func"
|
||||
exit 6
|
||||
}
|
||||
source /dev/stdin <<<"$tools_content"
|
||||
if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then
|
||||
msg_error "tools.func loaded but incomplete — missing expected functions"
|
||||
exit 6
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
@@ -238,6 +289,7 @@ motd_ssh() {
|
||||
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
fi
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
@@ -276,4 +328,5 @@ EOF
|
||||
chmod 700 /root/.ssh
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
fi
|
||||
post_progress_to_api
|
||||
}
|
||||
|
||||
+2686
-546
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,626 @@
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# License: MIT | https://git.community-scripts.org/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
|
||||
|
||||
set -euo pipefail
|
||||
SPINNER_PID=""
|
||||
SPINNER_ACTIVE=0
|
||||
SPINNER_MSG=""
|
||||
declare -A MSG_INFO_SHOWN
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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
|
||||
shell_check
|
||||
get_valid_nextid
|
||||
cleanup_vmid
|
||||
cleanup
|
||||
check_root
|
||||
pve_check
|
||||
arch_check
|
||||
}
|
||||
|
||||
# Function to download & save header files
|
||||
get_header() {
|
||||
local app_name=$(echo "${APP,,}" | tr ' ' '-')
|
||||
local app_type=${APP_TYPE:-vm}
|
||||
local header_url="https://git.community-scripts.org/community-scripts/ProxmoxVE/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 ' ' '-')
|
||||
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
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sets ANSI color codes used for styled terminal output.
|
||||
# ------------------------------------------------------------------------------
|
||||
color() {
|
||||
YW=$(echo "\033[33m")
|
||||
YWB=$(echo "\033[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")
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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}"
|
||||
ICON_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}"
|
||||
GPU="${TAB}🎮${TAB}${CL}"
|
||||
HOURGLASS="${TAB}⏳${TAB}"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sets default verbose mode for script and os execution.
|
||||
# ------------------------------------------------------------------------------
|
||||
set_std_mode() {
|
||||
if [ "${VERBOSE:-no}" = "yes" ]; then
|
||||
STD=""
|
||||
else
|
||||
STD="silent"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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)
|
||||
# ------------------------------------------------------------------------------
|
||||
default_vars() {
|
||||
RETRY_NUM=10
|
||||
RETRY_EVERY=3
|
||||
i=$RETRY_NUM
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# get_active_logfile()
|
||||
#
|
||||
# - Returns the appropriate log file based on execution context
|
||||
# - BUILD_LOG: Host operations (VM creation)
|
||||
# - Fallback to /tmp/build-<timestamp>.log if not set
|
||||
# ------------------------------------------------------------------------------
|
||||
get_active_logfile() {
|
||||
if [[ -n "${BUILD_LOG:-}" ]]; then
|
||||
echo "$BUILD_LOG"
|
||||
else
|
||||
# Fallback for legacy scripts
|
||||
echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# silent()
|
||||
#
|
||||
# - Executes command with output redirected to active log file
|
||||
# - On error: displays last 20 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/ProxmoxVE/raw/branch/main/misc/error_handler.func) 2>/dev/null || true
|
||||
fi
|
||||
|
||||
local explanation=""
|
||||
if declare -f explain_exit_code >/dev/null 2>&1; then
|
||||
explanation="$(explain_exit_code "$rc")"
|
||||
fi
|
||||
|
||||
printf "\e[?25h"
|
||||
if [[ -n "$explanation" ]]; then
|
||||
msg_error "in line ${caller_line}: exit code ${rc} (${explanation})"
|
||||
else
|
||||
msg_error "in line ${caller_line}: exit code ${rc}"
|
||||
fi
|
||||
msg_custom "→" "${YWB}" "${cmd}"
|
||||
|
||||
if [[ -s "$logfile" ]]; then
|
||||
echo -e "\n${TAB}--- Last 20 lines of log ---"
|
||||
tail -n 20 "$logfile"
|
||||
echo -e "${TAB}----------------------------\n"
|
||||
fi
|
||||
|
||||
exit "$rc"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Performs a curl request with retry logic and inline feedback.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
run_curl() {
|
||||
if [ "$VERB" = "no" ]; then
|
||||
curl "$@" >/dev/null 2>>/tmp/curl_error.log
|
||||
else
|
||||
curl "$@" 2>>/tmp/curl_error.log
|
||||
fi
|
||||
}
|
||||
|
||||
curl_handler() {
|
||||
local args=()
|
||||
local url=""
|
||||
local max_retries=0 delay=2 attempt=1
|
||||
local exit_code has_output_file=false
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" != -* && -z "$url" ]]; then
|
||||
url="$arg"
|
||||
fi
|
||||
[[ "$arg" == "-o" || "$arg" == --output ]] && has_output_file=true
|
||||
args+=("$arg")
|
||||
done
|
||||
|
||||
if [[ -z "$url" ]]; then
|
||||
msg_error "no valid url or option entered for curl_handler"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$STD msg_info "Fetching: $url"
|
||||
|
||||
while :; do
|
||||
if $has_output_file; then
|
||||
$STD run_curl "${args[@]}"
|
||||
exit_code=$?
|
||||
else
|
||||
$STD result=$(run_curl "${args[@]}")
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
stop_spinner
|
||||
msg_ok "Fetched: $url"
|
||||
$has_output_file || printf '%s' "$result"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ((attempt >= max_retries)); then
|
||||
stop_spinner
|
||||
if [ -s /tmp/curl_error.log ]; then
|
||||
local curl_stderr
|
||||
curl_stderr=$(</tmp/curl_error.log)
|
||||
rm -f /tmp/curl_error.log
|
||||
fi
|
||||
__curl_err_handler "$exit_code" "$url" "$curl_stderr"
|
||||
exit 1 # hard exit if exit_code is not 0
|
||||
fi
|
||||
|
||||
$STD printf "\r\033[K${INFO}${YW}Retry $attempt/$max_retries in ${delay}s...${CL}" >&2
|
||||
sleep "$delay"
|
||||
((attempt++))
|
||||
done
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Handles specific curl error codes and displays descriptive messages.
|
||||
# ------------------------------------------------------------------------------
|
||||
__curl_err_handler() {
|
||||
local exit_code="$1"
|
||||
local target="$2"
|
||||
local curl_msg="$3"
|
||||
|
||||
case $exit_code in
|
||||
1) msg_error "Unsupported protocol: $target" ;;
|
||||
2) msg_error "Curl init failed: $target" ;;
|
||||
3) msg_error "Malformed URL: $target" ;;
|
||||
5) msg_error "Proxy resolution failed: $target" ;;
|
||||
6) msg_error "Host resolution failed: $target" ;;
|
||||
7) msg_error "Connection failed: $target" ;;
|
||||
9) msg_error "Access denied: $target" ;;
|
||||
18) msg_error "Partial file transfer: $target" ;;
|
||||
22) msg_error "HTTP error (e.g. 400/404): $target" ;;
|
||||
23) msg_error "Write error on local system: $target" ;;
|
||||
26) msg_error "Read error from local file: $target" ;;
|
||||
28) msg_error "Timeout: $target" ;;
|
||||
35) msg_error "SSL connect error: $target" ;;
|
||||
47) msg_error "Too many redirects: $target" ;;
|
||||
51) msg_error "SSL cert verify failed: $target" ;;
|
||||
52) msg_error "Empty server response: $target" ;;
|
||||
55) msg_error "Send error: $target" ;;
|
||||
56) msg_error "Receive error: $target" ;;
|
||||
60) msg_error "SSL CA not trusted: $target" ;;
|
||||
67) msg_error "Login denied by server: $target" ;;
|
||||
78) msg_error "Remote file not found (404): $target" ;;
|
||||
*) msg_error "Curl failed with code $exit_code: $target" ;;
|
||||
esac
|
||||
|
||||
[[ -n "$curl_msg" ]] && printf "%s\n" "$curl_msg" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# shell_check()
|
||||
#
|
||||
# - Verifies that the script is running under Bash shell
|
||||
# - Exits with error message if different shell is detected
|
||||
# ------------------------------------------------------------------------------
|
||||
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
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# clear_line()
|
||||
#
|
||||
# - Clears current terminal line using tput or ANSI escape codes
|
||||
# - Moves cursor to beginning of line (carriage return)
|
||||
# - 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"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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)
|
||||
# ------------------------------------------------------------------------------
|
||||
is_verbose_mode() {
|
||||
local verbose="${VERBOSE:-${var_verbose:-no}}"
|
||||
[[ "$verbose" != "no" || ! -t 2 ]]
|
||||
}
|
||||
|
||||
### dev spinner ###
|
||||
SPINNER_ACTIVE=0
|
||||
SPINNER_PID=""
|
||||
SPINNER_MSG=""
|
||||
declare -A MSG_INFO_SHOWN=()
|
||||
|
||||
# Trap cleanup on various signals
|
||||
trap 'cleanup_spinner' EXIT INT TERM HUP
|
||||
|
||||
# Cleans up spinner process on exit
|
||||
cleanup_spinner() {
|
||||
stop_spinner
|
||||
# Additional cleanup if needed
|
||||
}
|
||||
|
||||
start_spinner() {
|
||||
local msg="${1:-Processing...}"
|
||||
local frames=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||||
local spin_i=0
|
||||
local interval=0.1
|
||||
|
||||
# Set message and clear current line
|
||||
SPINNER_MSG="$msg"
|
||||
printf "\r\e[2K" >&2
|
||||
|
||||
# Stop any existing spinner
|
||||
stop_spinner
|
||||
|
||||
# Set active flag
|
||||
SPINNER_ACTIVE=1
|
||||
|
||||
# Start spinner in background
|
||||
{
|
||||
while [[ "$SPINNER_ACTIVE" -eq 1 ]]; do
|
||||
printf "\r\e[2K%s %b" "${TAB}${frames[spin_i]}${TAB}" "${YW}${SPINNER_MSG}${CL}" >&2
|
||||
spin_i=$(((spin_i + 1) % ${#frames[@]}))
|
||||
sleep "$interval"
|
||||
done
|
||||
} &
|
||||
|
||||
SPINNER_PID=$!
|
||||
|
||||
# Disown to prevent getting "Terminated" messages
|
||||
disown "$SPINNER_PID" 2>/dev/null || true
|
||||
}
|
||||
|
||||
stop_spinner() {
|
||||
# Check if spinner is active and PID exists
|
||||
if [[ "$SPINNER_ACTIVE" -eq 1 ]] && [[ -n "${SPINNER_PID}" ]]; then
|
||||
SPINNER_ACTIVE=0
|
||||
|
||||
if kill -0 "$SPINNER_PID" 2>/dev/null; then
|
||||
kill "$SPINNER_PID" 2>/dev/null
|
||||
# Give it a moment to terminate
|
||||
sleep 0.1
|
||||
# Force kill if still running
|
||||
if kill -0 "$SPINNER_PID" 2>/dev/null; then
|
||||
kill -9 "$SPINNER_PID" 2>/dev/null
|
||||
fi
|
||||
# Wait for process but ignore errors
|
||||
wait "$SPINNER_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Clear spinner line
|
||||
printf "\r\e[2K" >&2
|
||||
SPINNER_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
spinner_guard() {
|
||||
# Safely stop spinner if it's running
|
||||
if [[ "$SPINNER_ACTIVE" -eq 1 ]] && [[ -n "${SPINNER_PID}" ]]; then
|
||||
stop_spinner
|
||||
fi
|
||||
}
|
||||
|
||||
msg_info() {
|
||||
local msg="${1:-Information message}"
|
||||
|
||||
# Only show each message once unless reset
|
||||
if [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]]; then
|
||||
return
|
||||
fi
|
||||
MSG_INFO_SHOWN["$msg"]=1
|
||||
|
||||
spinner_guard
|
||||
start_spinner "$msg"
|
||||
}
|
||||
|
||||
msg_ok() {
|
||||
local msg="${1:-Operation completed successfully}"
|
||||
stop_spinner
|
||||
printf "\r\e[2K%s %b\n" "${CM}" "${GN}${msg}${CL}" >&2
|
||||
|
||||
# Remove from shown messages to allow it to be shown again
|
||||
local sanitized_msg
|
||||
sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g')
|
||||
unset 'MSG_INFO_SHOWN['"$sanitized_msg"']' 2>/dev/null || true
|
||||
}
|
||||
|
||||
msg_error() {
|
||||
local msg="${1:-An error occurred}"
|
||||
stop_spinner
|
||||
printf "\r\e[2K%s %b\n" "${CROSS}" "${RD}${msg}${CL}" >&2
|
||||
}
|
||||
|
||||
msg_warn() {
|
||||
stop_spinner
|
||||
local msg="$1"
|
||||
echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2
|
||||
}
|
||||
|
||||
# Helper function to display a message with custom symbol and color
|
||||
msg_custom() {
|
||||
local symbol="${1:-*}"
|
||||
local color="${2:-$CL}"
|
||||
local msg="${3:-Custom message}"
|
||||
[[ -z "$msg" ]] && return
|
||||
stop_spinner
|
||||
printf "\r\e[2K%s %b\n" "$symbol" "${color}${msg}${CL}" >&2
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# msg_debug()
|
||||
#
|
||||
# - Displays debug message with timestamp when var_full_verbose=1
|
||||
# - Automatically enables var_verbose if not already set
|
||||
# - 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
|
||||
}
|
||||
|
||||
# Displays error message and immediately terminates script
|
||||
fatal() {
|
||||
msg_error "$1"
|
||||
kill -INT $$
|
||||
}
|
||||
|
||||
get_valid_nextid() {
|
||||
local try_id
|
||||
try_id=$(pvesh get /cluster/nextid)
|
||||
while true; do
|
||||
if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
|
||||
try_id=$((try_id + 1))
|
||||
continue
|
||||
fi
|
||||
if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
|
||||
try_id=$((try_id + 1))
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
echo "$try_id"
|
||||
}
|
||||
|
||||
cleanup_vmid() {
|
||||
if [[ -z "${VMID:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
if qm status "$VMID" &>/dev/null; then
|
||||
qm stop "$VMID" &>/dev/null
|
||||
qm destroy "$VMID" &>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [[ "$(dirs -p | wc -l)" -gt 1 ]]; then
|
||||
popd >/dev/null || true
|
||||
fi
|
||||
# Report final telemetry status if post_to_api_vm was called but no update was sent
|
||||
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
|
||||
if declare -f post_update_to_api >/dev/null 2>&1; then
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
post_update_to_api "failed" "$exit_code"
|
||||
else
|
||||
# Exited cleanly but description()/success was never called — shouldn't happen
|
||||
post_update_to_api "failed" "1"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_root() {
|
||||
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() {
|
||||
if ! pveversion | grep -Eq "pve-manager/(8\.[1-4]|9\.[0-1])(\.[0-9]+)*"; then
|
||||
msg_error "This version of Proxmox Virtual Environment is not supported"
|
||||
echo -e "Requires Proxmox Virtual Environment Version 8.1 - 8.4 or 9.0 - 9.1."
|
||||
echo -e "Exiting..."
|
||||
sleep 2
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
exit_script() {
|
||||
clear
|
||||
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||||
exit
|
||||
}
|
||||
|
||||
check_hostname_conflict() {
|
||||
local hostname="$1"
|
||||
if qm list | awk '{print $2}' | grep -qx "$hostname"; then
|
||||
msg_error "Hostname $hostname already in use by another VM."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
set_description() {
|
||||
DESCRIPTION=$(
|
||||
cat <<EOF
|
||||
<div align='center'>
|
||||
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||||
</a>
|
||||
|
||||
<h2 style='font-size: 24px; margin: 20px 0;'>${NSAPP} VM</h2>
|
||||
|
||||
<p style='margin: 16px 0;'>
|
||||
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||||
</span>
|
||||
</div>
|
||||
EOF
|
||||
)
|
||||
qm set "$VMID" -description "$DESCRIPTION" >/dev/null
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user