mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-02-26 23:15:55 +01:00
* fix(zammad): configure Elasticsearch for LXC container startup
- Set discovery.type: single-node (required for single-node ES)
- Set xpack.security.enabled: false (not needed in local LXC)
- Set bootstrap.memory_lock: false (fails in unprivileged LXC)
- Add startup wait loop (up to 60s) to ensure ES is ready before
Zammad installation continues
Fixes #12301-related recurring Elasticsearch startup failures
* refactor(api): eliminate duplicate traps, harden error handling & telemetry
Phase 1 - Structural:
- Remove api_exit_script() and 5 inline traps from build.func
- error_handler.func is now the sole trap owner via catch_errors()
- Update api.func comment reference (api_exit_script -> on_exit)
Phase 2 - Quality:
- Add stop_spinner() + cursor restore to error_handler(), on_interrupt(),
on_terminate(), on_hangup() to prevent spinner/cursor artifacts
- Enhance _send_abort_telemetry() with error text (last 20 log lines),
duration calculation, and 2 retry attempts (was fire-and-forget)
- Harden json_escape() to also strip DEL (0x7F) character
* fix(build): show spinner during post_update_to_api to prevent Ctrl+Z abort
post_update_to_api can take up to 33 seconds worst-case (3 curl attempts
x 10s timeout + sleep delays). Without any terminal output during this
time, users think the script is stuck and press Ctrl+Z, which prevents
the recovery menu from ever appearing.
Add msg_info spinner before both post_update_to_api calls in the failure
path (initial report + final force retry after recovery menu).
* fix(build): prevent SIGTSTP from killing recovery dialog
- Replace msg_info/stop_spinner with plain echo for telemetry reporting
The background spinner process in non-interactive shells (bash -c)
can trigger SIGTSTP, stopping the entire process group before the
recovery dialog appears. Plain echo avoids this.
- Add trap '' TSTP at failure path entry to ignore suspension signals
Prevents Ctrl+Z or terminal-related SIGTSTP from interrupting the
recovery menu. Restored with trap - TSTP before exit.
- Root cause: msg_info starts a background process (spinner &) that
is not properly detached in non-interactive shells where job control
(set -m) is OFF. The disown builtin has no effect without job
control, leaving the spinner in the same process group. This can
cause terminal I/O conflicts during the 33-second post_update_to_api
retry window, resulting in [2]+ Stopped.
* fix(test): initialize colors and remove illegal local in test harness
- Call load_functions() after sourcing core.func to initialize
color/formatting/icon variables (RD, GN, YW, CL, TAB, etc.)
- Remove 'local' keyword from top-level scope (not inside function)
- Default REPO_SOURCE to ref_api instead of main
* chore: remove test-recovery-dialog.sh from branch
* Revert "fix(zammad): configure Elasticsearch for LXC container startup"
This reverts commit 10e450b72f.
* fix(build): show telemetry status only in verbose mode
Telemetry reporting is an implementation detail that doesn't help
the user during failure recovery. Wrap echo statements with
VERBOSE check so they only appear when verbose mode is enabled.
1387 lines
48 KiB
Bash
1387 lines
48 KiB
Bash
# Copyright (c) 2021-2026 community-scripts ORG
|
|
# Author: michelroegl-brunner | MickLesk
|
|
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
|
|
|
# ==============================================================================
|
|
# API.FUNC - TELEMETRY & DIAGNOSTICS API
|
|
# ==============================================================================
|
|
#
|
|
# Provides functions for sending anonymous telemetry data via the community
|
|
# telemetry ingest service at telemetry.community-scripts.org.
|
|
#
|
|
# Features:
|
|
# - Container/VM creation statistics
|
|
# - Installation success/failure tracking
|
|
# - Error code mapping and reporting
|
|
# - Privacy-respecting anonymous telemetry
|
|
#
|
|
# Usage:
|
|
# source <(curl -fsSL .../api.func)
|
|
# post_to_api # Report LXC container creation
|
|
# post_to_api_vm # Report VM creation
|
|
# post_update_to_api # Report installation status
|
|
#
|
|
# Privacy:
|
|
# - Only anonymous statistics (no personal data)
|
|
# - User can opt-out via DIAGNOSTICS=no
|
|
# - Random UUID for session tracking only
|
|
# - Data retention: 30 days
|
|
#
|
|
# ==============================================================================
|
|
|
|
# ==============================================================================
|
|
# Telemetry Configuration
|
|
# ==============================================================================
|
|
TELEMETRY_URL="https://telemetry.community-scripts.org/telemetry"
|
|
|
|
# Timeout for telemetry requests (seconds)
|
|
# Progress pings (validation/configuring) use the short timeout
|
|
TELEMETRY_TIMEOUT=5
|
|
# Final status updates (success/failed) use the longer timeout
|
|
# PocketBase may need more time under load (FindRecord + UpdateRecord)
|
|
STATUS_TIMEOUT=10
|
|
|
|
# ==============================================================================
|
|
# SECTION 0: REPOSITORY SOURCE DETECTION
|
|
# ==============================================================================
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# detect_repo_source()
|
|
#
|
|
# - Dynamically detects which GitHub/Gitea repo the scripts were loaded from
|
|
# - Inspects /proc/$$/cmdline and $0 to find the source URL
|
|
# - Maps detected repo to one of three canonical values:
|
|
# * "ProxmoxVE" — official community-scripts/ProxmoxVE (production)
|
|
# * "ProxmoxVED" — official community-scripts/ProxmoxVED (development)
|
|
# * "external" — any fork or unknown source
|
|
# - Fallback: "ProxmoxVED" (CI sed transforms ProxmoxVED → ProxmoxVE on promotion)
|
|
# - Sets and exports REPO_SOURCE global variable
|
|
# - Skips detection if REPO_SOURCE is already set (e.g., by environment)
|
|
# ------------------------------------------------------------------------------
|
|
detect_repo_source() {
|
|
# Allow explicit override via environment
|
|
[[ -n "${REPO_SOURCE:-}" ]] && return 0
|
|
|
|
local content="" owner_repo=""
|
|
|
|
# Method 1: Read from /proc/$$/cmdline
|
|
# When invoked via: bash -c "$(curl -fsSL https://.../ct/app.sh)"
|
|
# the full CT/VM script content is in /proc/$$/cmdline (same PID through source chain)
|
|
if [[ -r /proc/$$/cmdline ]]; then
|
|
content=$(tr '\0' ' ' </proc/$$/cmdline 2>/dev/null) || true
|
|
fi
|
|
|
|
# Method 2: Read from the original script file (bash ct/app.sh / bash vm/app.sh)
|
|
if [[ -z "$content" ]] || ! echo "$content" | grep -qE 'githubusercontent\.com|community-scripts\.org' 2>/dev/null; then
|
|
if [[ -f "$0" ]] && [[ "$0" != *bash* ]]; then
|
|
content=$(head -10 "$0" 2>/dev/null) || true
|
|
fi
|
|
fi
|
|
|
|
# Extract owner/repo from URL patterns found in the script content
|
|
if [[ -n "$content" ]]; then
|
|
# GitHub raw URL: raw.githubusercontent.com/OWNER/REPO/...
|
|
owner_repo=$(echo "$content" | grep -oE 'raw\.githubusercontent\.com/[^/]+/[^/]+' | head -1 | sed 's|raw\.githubusercontent\.com/||') || true
|
|
|
|
# Gitea URL: git.community-scripts.org/OWNER/REPO/...
|
|
if [[ -z "$owner_repo" ]]; then
|
|
owner_repo=$(echo "$content" | grep -oE 'git\.community-scripts\.org/[^/]+/[^/]+' | head -1 | sed 's|git\.community-scripts\.org/||') || true
|
|
fi
|
|
fi
|
|
|
|
# Map detected owner/repo to canonical repo_source value
|
|
case "$owner_repo" in
|
|
community-scripts/ProxmoxVE) REPO_SOURCE="ProxmoxVE" ;;
|
|
community-scripts/ProxmoxVED) REPO_SOURCE="ProxmoxVED" ;;
|
|
"")
|
|
# No URL detected — use hardcoded fallback
|
|
# This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev
|
|
REPO_SOURCE="ProxmoxVE"
|
|
;;
|
|
*)
|
|
# Fork or unknown repo
|
|
REPO_SOURCE="external"
|
|
;;
|
|
esac
|
|
|
|
export REPO_SOURCE
|
|
}
|
|
|
|
# Run detection immediately when api.func is sourced
|
|
detect_repo_source
|
|
|
|
# ==============================================================================
|
|
# SECTION 1: ERROR CODE DESCRIPTIONS
|
|
# ==============================================================================
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# explain_exit_code()
|
|
#
|
|
# - Maps numeric exit codes to human-readable error descriptions
|
|
# - Canonical source of truth for ALL exit code mappings
|
|
# - Used by both api.func (telemetry) and error_handler.func (error display)
|
|
# - Supports:
|
|
# * Generic/Shell errors (1-3, 10, 124-132, 134, 137, 139, 141, 143-146)
|
|
# * curl/wget errors (4-8, 16, 18, 22-28, 30, 32-36, 39, 44-48, 51-52, 55-57, 59, 61, 63, 75, 78-79, 92, 95)
|
|
# * Package manager errors (APT, DPKG: 100-102, 255)
|
|
# * BSD sysexits (64-78)
|
|
# * Systemd/Service errors (150-154)
|
|
# * Python/pip/uv errors (160-162)
|
|
# * PostgreSQL errors (170-173)
|
|
# * MySQL/MariaDB errors (180-183)
|
|
# * MongoDB errors (190-193)
|
|
# * Proxmox custom codes (200-231)
|
|
# * Node.js/npm errors (239, 243, 245-249)
|
|
# - Returns description string for given exit code
|
|
# ------------------------------------------------------------------------------
|
|
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)" ;;
|
|
3) echo "General syntax or argument error" ;;
|
|
10) echo "Docker / privileged mode required (unsupported environment)" ;;
|
|
|
|
# --- curl / wget errors (commonly seen in downloads) ---
|
|
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" ;;
|
|
|
|
# --- Package manager / APT / DPKG ---
|
|
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)" ;;
|
|
|
|
# --- BSD sysexits.h (64-78) ---
|
|
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" ;;
|
|
|
|
# --- Common shell/system errors ---
|
|
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)" ;;
|
|
|
|
# --- Systemd / Service errors (150-154) ---
|
|
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)" ;;
|
|
# --- Python / pip / uv (160-162) ---
|
|
160) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
|
161) echo "Python: Dependency resolution failed" ;;
|
|
162) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
|
|
|
# --- PostgreSQL (170-173) ---
|
|
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" ;;
|
|
|
|
# --- MySQL / MariaDB (180-183) ---
|
|
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" ;;
|
|
|
|
# --- MongoDB (190-193) ---
|
|
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" ;;
|
|
|
|
# --- Proxmox Custom Codes (200-231) ---
|
|
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" ;;
|
|
|
|
# --- Node.js / npm / pnpm / yarn (239-249) ---
|
|
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" ;;
|
|
|
|
# --- DPKG ---
|
|
255) echo "DPKG: Fatal internal error" ;;
|
|
|
|
# --- Default ---
|
|
*) echo "Unknown error" ;;
|
|
esac
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# json_escape()
|
|
#
|
|
# - Escapes a string for safe JSON embedding
|
|
# - Strips ANSI escape sequences and non-printable control characters
|
|
# - Handles backslashes, quotes, newlines, tabs, and carriage returns
|
|
# ------------------------------------------------------------------------------
|
|
json_escape() {
|
|
local s="$1"
|
|
# Strip ANSI escape sequences (color codes etc.)
|
|
s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g')
|
|
s=${s//\\/\\\\}
|
|
s=${s//"/\\"/}
|
|
s=${s//$'\n'/\\n}
|
|
s=${s//$'\r'/}
|
|
s=${s//$'\t'/\\t}
|
|
# Remove any remaining control characters (0x00-0x1F except those already handled)
|
|
# Also remove DEL (0x7F) and invalid high bytes that break JSON parsers
|
|
s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037\177')
|
|
printf '%s' "$s"
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# get_error_text()
|
|
#
|
|
# - Returns last 20 lines of the active log (INSTALL_LOG or BUILD_LOG)
|
|
# - Falls back to combined log or BUILD_LOG if primary is not accessible
|
|
# - Handles container paths that don't exist on the host
|
|
# ------------------------------------------------------------------------------
|
|
get_error_text() {
|
|
local logfile=""
|
|
if declare -f get_active_logfile >/dev/null 2>&1; then
|
|
logfile=$(get_active_logfile)
|
|
elif [[ -n "${INSTALL_LOG:-}" ]]; then
|
|
logfile="$INSTALL_LOG"
|
|
elif [[ -n "${BUILD_LOG:-}" ]]; then
|
|
logfile="$BUILD_LOG"
|
|
fi
|
|
|
|
# If logfile is inside container (e.g. /root/.install-*), try the host copy
|
|
if [[ -n "$logfile" && ! -s "$logfile" ]]; then
|
|
# Try combined log: /tmp/<app>-<CTID>-<SESSION_ID>.log
|
|
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then
|
|
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
|
|
if [[ -s "$combined_log" ]]; then
|
|
logfile="$combined_log"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Also try BUILD_LOG as fallback if primary log is empty/missing
|
|
if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
|
|
logfile="$BUILD_LOG"
|
|
fi
|
|
|
|
if [[ -n "$logfile" && -s "$logfile" ]]; then
|
|
tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# get_full_log()
|
|
#
|
|
# - Returns the FULL installation log (build + install combined)
|
|
# - Calls ensure_log_on_host() to pull container log if needed
|
|
# - Strips ANSI escape codes and carriage returns
|
|
# - Truncates to max_bytes (default: 120KB) to stay within API limits
|
|
# - Used for the error telemetry field (full trace instead of 20 lines)
|
|
# ------------------------------------------------------------------------------
|
|
get_full_log() {
|
|
local max_bytes="${1:-122880}" # 120KB default
|
|
local logfile=""
|
|
|
|
# Ensure logs are available on host (pulls from container if needed)
|
|
if declare -f ensure_log_on_host >/dev/null 2>&1; then
|
|
ensure_log_on_host
|
|
fi
|
|
|
|
# Try combined log first (most complete)
|
|
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then
|
|
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
|
|
if [[ -s "$combined_log" ]]; then
|
|
logfile="$combined_log"
|
|
fi
|
|
fi
|
|
|
|
# Fall back to INSTALL_LOG
|
|
if [[ -z "$logfile" || ! -s "$logfile" ]]; then
|
|
if [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]]; then
|
|
logfile="$INSTALL_LOG"
|
|
fi
|
|
fi
|
|
|
|
# Fall back to BUILD_LOG
|
|
if [[ -z "$logfile" || ! -s "$logfile" ]]; then
|
|
if [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
|
|
logfile="$BUILD_LOG"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$logfile" && -s "$logfile" ]]; then
|
|
# Strip ANSI codes, carriage returns, and anonymize IP addresses (GDPR)
|
|
sed 's/\r$//' "$logfile" 2>/dev/null |
|
|
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' |
|
|
sed -E 's/([0-9]{1,3}\.)[0-9]{1,3}\.[0-9]{1,3}/\1x.x/g' |
|
|
head -c "$max_bytes"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# build_error_string()
|
|
#
|
|
# - Builds a structured error string for telemetry reporting
|
|
# - Format: "exit_code=<N> | <explanation>\n---\n<last 20 log lines>"
|
|
# - If no log lines available, returns just the explanation
|
|
# - Arguments:
|
|
# * $1: exit_code (numeric)
|
|
# * $2: log_text (optional, output from get_error_text)
|
|
# - Returns structured error string via stdout
|
|
# ------------------------------------------------------------------------------
|
|
build_error_string() {
|
|
local exit_code="${1:-1}"
|
|
local log_text="${2:-}"
|
|
local explanation
|
|
explanation=$(explain_exit_code "$exit_code")
|
|
|
|
if [[ -n "$log_text" ]]; then
|
|
# Structured format: header + separator + log lines
|
|
printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text"
|
|
else
|
|
# No log available - just the explanation with exit code
|
|
printf 'exit_code=%s | %s' "$exit_code" "$explanation"
|
|
fi
|
|
}
|
|
|
|
# ==============================================================================
|
|
# SECTION 2: TELEMETRY FUNCTIONS
|
|
# ==============================================================================
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# detect_gpu()
|
|
#
|
|
# - Detects GPU vendor, model, and passthrough type
|
|
# - Sets GPU_VENDOR, GPU_MODEL, and GPU_PASSTHROUGH globals
|
|
# - Used for GPU analytics
|
|
# ------------------------------------------------------------------------------
|
|
detect_gpu() {
|
|
GPU_VENDOR="unknown"
|
|
GPU_MODEL=""
|
|
GPU_PASSTHROUGH="unknown"
|
|
|
|
local gpu_line
|
|
gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1)
|
|
|
|
if [[ -n "$gpu_line" ]]; then
|
|
# Extract model: everything after the colon, clean up
|
|
GPU_MODEL=$(echo "$gpu_line" | sed 's/.*: //' | sed 's/ (rev .*)$//' | cut -c1-64)
|
|
|
|
# Detect vendor and passthrough type
|
|
if echo "$gpu_line" | grep -qi "Intel"; then
|
|
GPU_VENDOR="intel"
|
|
GPU_PASSTHROUGH="igpu"
|
|
elif echo "$gpu_line" | grep -qi "AMD\|ATI"; then
|
|
GPU_VENDOR="amd"
|
|
if echo "$gpu_line" | grep -qi "Radeon RX\|Radeon Pro"; then
|
|
GPU_PASSTHROUGH="dgpu"
|
|
else
|
|
GPU_PASSTHROUGH="igpu"
|
|
fi
|
|
elif echo "$gpu_line" | grep -qi "NVIDIA"; then
|
|
GPU_VENDOR="nvidia"
|
|
GPU_PASSTHROUGH="dgpu"
|
|
fi
|
|
fi
|
|
|
|
export GPU_VENDOR GPU_MODEL GPU_PASSTHROUGH
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# detect_cpu()
|
|
#
|
|
# - Detects CPU vendor and model
|
|
# - Sets CPU_VENDOR (intel/amd/arm/unknown) and CPU_MODEL globals
|
|
# - Used for CPU analytics
|
|
# ------------------------------------------------------------------------------
|
|
detect_cpu() {
|
|
CPU_VENDOR="unknown"
|
|
CPU_MODEL=""
|
|
|
|
if [[ -f /proc/cpuinfo ]]; then
|
|
local vendor_id
|
|
vendor_id=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ')
|
|
|
|
case "$vendor_id" in
|
|
GenuineIntel) CPU_VENDOR="intel" ;;
|
|
AuthenticAMD) CPU_VENDOR="amd" ;;
|
|
*)
|
|
# ARM doesn't have vendor_id, check for CPU implementer
|
|
if grep -qi "CPU implementer" /proc/cpuinfo 2>/dev/null; then
|
|
CPU_VENDOR="arm"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Extract model name and clean it up
|
|
CPU_MODEL=$(grep -m1 "model name" /proc/cpuinfo 2>/dev/null | cut -d: -f2 | sed 's/^ *//' | sed 's/(R)//g' | sed 's/(TM)//g' | sed 's/ */ /g' | cut -c1-64)
|
|
fi
|
|
|
|
export CPU_VENDOR CPU_MODEL
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# detect_ram()
|
|
#
|
|
# - Detects RAM speed using dmidecode
|
|
# - Sets RAM_SPEED global (e.g., "4800" for DDR5-4800)
|
|
# - Requires root access for dmidecode
|
|
# - Returns empty if not available or if speed is "Unknown" (nested VMs)
|
|
# ------------------------------------------------------------------------------
|
|
detect_ram() {
|
|
RAM_SPEED=""
|
|
|
|
if command -v dmidecode &>/dev/null; then
|
|
# Get configured memory speed (actual running speed)
|
|
# Use || true to handle "Unknown" values in nested VMs (no numeric match)
|
|
RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true
|
|
|
|
# Fallback to Speed: if Configured not available
|
|
if [[ -z "$RAM_SPEED" ]]; then
|
|
RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) || true
|
|
fi
|
|
fi
|
|
|
|
export RAM_SPEED
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_to_api()
|
|
#
|
|
# - Sends LXC container creation statistics to telemetry ingest service
|
|
# - Only executes if:
|
|
# * curl is available
|
|
# * DIAGNOSTICS=yes
|
|
# * RANDOM_UUID is set
|
|
# - Payload includes:
|
|
# * Container type, disk size, CPU cores, RAM
|
|
# * OS type and version
|
|
# * Application name (NSAPP)
|
|
# * Installation method
|
|
# * PVE version
|
|
# * Status: "installing"
|
|
# * Random UUID for session tracking
|
|
# - Anonymous telemetry (no personal data)
|
|
# - Never blocks or fails script execution
|
|
# ------------------------------------------------------------------------------
|
|
post_to_api() {
|
|
# Prevent duplicate submissions (post_to_api is called from multiple places)
|
|
[[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0
|
|
|
|
# Silent fail - telemetry should never break scripts
|
|
command -v curl &>/dev/null || {
|
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] curl not found, skipping" >&2
|
|
return 0
|
|
}
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && {
|
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] DIAGNOSTICS=no, skipping" >&2
|
|
return 0
|
|
}
|
|
[[ -z "${RANDOM_UUID:-}" ]] && {
|
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] RANDOM_UUID empty, skipping" >&2
|
|
return 0
|
|
}
|
|
|
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2
|
|
|
|
# Set type for later status updates
|
|
TELEMETRY_TYPE="lxc"
|
|
|
|
local pve_version=""
|
|
if command -v pveversion &>/dev/null; then
|
|
pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
|
|
fi
|
|
|
|
# Detect GPU if not already set
|
|
if [[ -z "${GPU_VENDOR:-}" ]]; then
|
|
detect_gpu
|
|
fi
|
|
local gpu_vendor="${GPU_VENDOR:-unknown}"
|
|
local gpu_model
|
|
gpu_model=$(json_escape "${GPU_MODEL:-}")
|
|
local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
|
|
|
|
# Detect CPU if not already set
|
|
if [[ -z "${CPU_VENDOR:-}" ]]; then
|
|
detect_cpu
|
|
fi
|
|
local cpu_vendor="${CPU_VENDOR:-unknown}"
|
|
local cpu_model
|
|
cpu_model=$(json_escape "${CPU_MODEL:-}")
|
|
|
|
# Detect RAM if not already set
|
|
if [[ -z "${RAM_SPEED:-}" ]]; then
|
|
detect_ram
|
|
fi
|
|
local ram_speed="${RAM_SPEED:-}"
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
|
|
"type": "lxc",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "installing",
|
|
"ct_type": ${CT_TYPE:-1},
|
|
"disk_size": ${DISK_SIZE:-0},
|
|
"core_count": ${CORE_COUNT:-0},
|
|
"ram_size": ${RAM_SIZE:-0},
|
|
"os_type": "${var_os:-}",
|
|
"os_version": "${var_version:-}",
|
|
"pve_version": "${pve_version}",
|
|
"method": "${METHOD:-default}",
|
|
"cpu_vendor": "${cpu_vendor}",
|
|
"cpu_model": "${cpu_model}",
|
|
"gpu_vendor": "${gpu_vendor}",
|
|
"gpu_model": "${gpu_model}",
|
|
"gpu_passthrough": "${gpu_passthrough}",
|
|
"ram_speed": "${ram_speed}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2
|
|
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2
|
|
|
|
# Fire-and-forget: never block, never fail
|
|
local http_code
|
|
if [[ "${DEV_MODE:-}" == "true" ]]; then
|
|
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" -o /dev/stderr 2>&1) || true
|
|
echo "[DEBUG] HTTP response code: $http_code" >&2
|
|
else
|
|
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" &>/dev/null || true
|
|
fi
|
|
|
|
POST_TO_API_DONE=true
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_to_api_vm()
|
|
#
|
|
# - Sends VM creation statistics to telemetry ingest service
|
|
# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics file
|
|
# - Payload differences from LXC:
|
|
# * ct_type=2 (VM instead of LXC)
|
|
# * type="vm"
|
|
# * Disk size without 'G' suffix
|
|
# - Includes hardware detection: CPU, GPU, RAM speed
|
|
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
|
|
# - Never blocks or fails script execution
|
|
# ------------------------------------------------------------------------------
|
|
post_to_api_vm() {
|
|
# Read diagnostics setting from file
|
|
if [[ -f /usr/local/community-scripts/diagnostics ]]; then
|
|
DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true
|
|
fi
|
|
|
|
# Silent fail - telemetry should never break scripts
|
|
command -v curl &>/dev/null || return 0
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
|
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
|
|
|
# Set type for later status updates
|
|
TELEMETRY_TYPE="vm"
|
|
|
|
local pve_version=""
|
|
if command -v pveversion &>/dev/null; then
|
|
pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
|
|
fi
|
|
|
|
# Detect GPU if not already set
|
|
if [[ -z "${GPU_VENDOR:-}" ]]; then
|
|
detect_gpu
|
|
fi
|
|
local gpu_vendor="${GPU_VENDOR:-unknown}"
|
|
local gpu_model
|
|
gpu_model=$(json_escape "${GPU_MODEL:-}")
|
|
local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
|
|
|
|
# Detect CPU if not already set
|
|
if [[ -z "${CPU_VENDOR:-}" ]]; then
|
|
detect_cpu
|
|
fi
|
|
local cpu_vendor="${CPU_VENDOR:-unknown}"
|
|
local cpu_model
|
|
cpu_model=$(json_escape "${CPU_MODEL:-}")
|
|
|
|
# Detect RAM if not already set
|
|
if [[ -z "${RAM_SPEED:-}" ]]; then
|
|
detect_ram
|
|
fi
|
|
local ram_speed="${RAM_SPEED:-}"
|
|
|
|
# Remove 'G' suffix from disk size
|
|
local DISK_SIZE_API="${DISK_SIZE%G}"
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
|
|
"type": "vm",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "installing",
|
|
"ct_type": 2,
|
|
"disk_size": ${DISK_SIZE_API:-0},
|
|
"core_count": ${CORE_COUNT:-0},
|
|
"ram_size": ${RAM_SIZE:-0},
|
|
"os_type": "${var_os:-}",
|
|
"os_version": "${var_version:-}",
|
|
"pve_version": "${pve_version}",
|
|
"method": "${METHOD:-default}",
|
|
"cpu_vendor": "${cpu_vendor}",
|
|
"cpu_model": "${cpu_model}",
|
|
"gpu_vendor": "${gpu_vendor}",
|
|
"gpu_model": "${gpu_model}",
|
|
"gpu_passthrough": "${gpu_passthrough}",
|
|
"ram_speed": "${ram_speed}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
# Fire-and-forget: never block, never fail
|
|
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" &>/dev/null || true
|
|
|
|
POST_TO_API_DONE=true
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_progress_to_api()
|
|
#
|
|
# - Lightweight progress ping from host or container
|
|
# - Updates the existing telemetry record status
|
|
# - Arguments:
|
|
# * $1: status (optional, default: "configuring")
|
|
# Valid values: "validation", "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
|
|
# - Can be called multiple times safely
|
|
# ------------------------------------------------------------------------------
|
|
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}"
|
|
local app_name="${NSAPP:-${app:-unknown}}"
|
|
local telemetry_type="${TELEMETRY_TYPE:-lxc}"
|
|
|
|
curl -fsS -m 5 -X POST "${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${telemetry_type}\",\"nsapp\":\"${app_name}\",\"status\":\"${progress_status}\"}" &>/dev/null || true
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_update_to_api()
|
|
#
|
|
# - Reports installation completion status to telemetry ingest service
|
|
# - Prevents duplicate submissions via POST_UPDATE_DONE flag
|
|
# - Arguments:
|
|
# * $1: status ("done" or "failed")
|
|
# * $2: exit_code (numeric, default: 1 for failed, 0 for done)
|
|
# - Payload includes:
|
|
# * Final status (mapped: "done"→"success", "failed"→"failed")
|
|
# * Error description via explain_exit_code()
|
|
# * Numeric exit code
|
|
# - Only executes once per session
|
|
# - Never blocks or fails script execution
|
|
# ------------------------------------------------------------------------------
|
|
post_update_to_api() {
|
|
# Silent fail - telemetry should never break scripts
|
|
command -v curl &>/dev/null || return 0
|
|
|
|
# Support "force" mode (3rd arg) to bypass duplicate check for retries after cleanup
|
|
local force="${3:-}"
|
|
POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
|
|
if [[ "$POST_UPDATE_DONE" == "true" && "$force" != "force" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
|
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
|
|
|
local status="${1:-failed}"
|
|
local raw_exit_code="${2:-1}"
|
|
local exit_code=0 error="" pb_status error_category=""
|
|
|
|
# Get GPU info (if detected)
|
|
local gpu_vendor="${GPU_VENDOR:-unknown}"
|
|
local gpu_model
|
|
gpu_model=$(json_escape "${GPU_MODEL:-}")
|
|
local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
|
|
|
|
# Get CPU info (if detected)
|
|
local cpu_vendor="${CPU_VENDOR:-unknown}"
|
|
local cpu_model
|
|
cpu_model=$(json_escape "${CPU_MODEL:-}")
|
|
|
|
# Get RAM info (if detected)
|
|
local ram_speed="${RAM_SPEED:-}"
|
|
|
|
# Map status to telemetry values: installing, success, failed, unknown
|
|
case "$status" in
|
|
done | success)
|
|
pb_status="success"
|
|
exit_code=0
|
|
error=""
|
|
error_category=""
|
|
;;
|
|
failed)
|
|
pb_status="failed"
|
|
;;
|
|
*)
|
|
pb_status="unknown"
|
|
;;
|
|
esac
|
|
|
|
# For failed/unknown status, resolve exit code and error description
|
|
local short_error=""
|
|
if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
|
|
if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
|
|
exit_code="$raw_exit_code"
|
|
else
|
|
exit_code=1
|
|
fi
|
|
# Get full installation log for error field
|
|
local log_text=""
|
|
log_text=$(get_full_log 122880) || true # 120KB max
|
|
if [[ -z "$log_text" ]]; then
|
|
# Fallback to last 20 lines
|
|
log_text=$(get_error_text)
|
|
fi
|
|
local full_error
|
|
full_error=$(build_error_string "$exit_code" "$log_text")
|
|
error=$(json_escape "$full_error")
|
|
short_error=$(json_escape "$(explain_exit_code "$exit_code")")
|
|
error_category=$(categorize_error "$exit_code")
|
|
[[ -z "$error" ]] && error="Unknown error"
|
|
fi
|
|
|
|
# Calculate duration if timer was started
|
|
local duration=0
|
|
if [[ -n "${INSTALL_START_TIME:-}" ]]; then
|
|
duration=$(($(date +%s) - INSTALL_START_TIME))
|
|
fi
|
|
|
|
# Get PVE version
|
|
local pve_version=""
|
|
if command -v pveversion &>/dev/null; then
|
|
pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
|
|
fi
|
|
|
|
local http_code=""
|
|
|
|
# ── Attempt 1: Full payload with complete error text (includes full log) ──
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
|
|
"type": "${TELEMETRY_TYPE:-lxc}",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "${pb_status}",
|
|
"ct_type": ${CT_TYPE:-1},
|
|
"disk_size": ${DISK_SIZE:-0},
|
|
"core_count": ${CORE_COUNT:-0},
|
|
"ram_size": ${RAM_SIZE:-0},
|
|
"os_type": "${var_os:-}",
|
|
"os_version": "${var_version:-}",
|
|
"pve_version": "${pve_version}",
|
|
"method": "${METHOD:-default}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${error}",
|
|
"error_category": "${error_category}",
|
|
"install_duration": ${duration},
|
|
"cpu_vendor": "${cpu_vendor}",
|
|
"cpu_model": "${cpu_model}",
|
|
"gpu_vendor": "${gpu_vendor}",
|
|
"gpu_model": "${gpu_model}",
|
|
"gpu_passthrough": "${gpu_passthrough}",
|
|
"ram_speed": "${ram_speed}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
|
|
|
|
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
|
|
POST_UPDATE_DONE=true
|
|
return 0
|
|
fi
|
|
|
|
# ── Attempt 2: Short error text (no full log) ──
|
|
sleep 1
|
|
local RETRY_PAYLOAD
|
|
RETRY_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
|
|
"type": "${TELEMETRY_TYPE:-lxc}",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "${pb_status}",
|
|
"ct_type": ${CT_TYPE:-1},
|
|
"disk_size": ${DISK_SIZE:-0},
|
|
"core_count": ${CORE_COUNT:-0},
|
|
"ram_size": ${RAM_SIZE:-0},
|
|
"os_type": "${var_os:-}",
|
|
"os_version": "${var_version:-}",
|
|
"pve_version": "${pve_version}",
|
|
"method": "${METHOD:-default}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${short_error}",
|
|
"error_category": "${error_category}",
|
|
"install_duration": ${duration},
|
|
"cpu_vendor": "${cpu_vendor}",
|
|
"cpu_model": "${cpu_model}",
|
|
"gpu_vendor": "${gpu_vendor}",
|
|
"gpu_model": "${gpu_model}",
|
|
"gpu_passthrough": "${gpu_passthrough}",
|
|
"ram_speed": "${ram_speed}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$RETRY_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
|
|
|
|
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
|
|
POST_UPDATE_DONE=true
|
|
return 0
|
|
fi
|
|
|
|
# ── Attempt 3: Minimal payload (bare minimum to set status) ──
|
|
sleep 2
|
|
local MINIMAL_PAYLOAD
|
|
MINIMAL_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
|
|
"type": "${TELEMETRY_TYPE:-lxc}",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "${pb_status}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${short_error}",
|
|
"error_category": "${error_category}",
|
|
"install_duration": ${duration}
|
|
}
|
|
EOF
|
|
)
|
|
|
|
http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$MINIMAL_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
|
|
|
|
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
|
|
POST_UPDATE_DONE=true
|
|
return 0
|
|
fi
|
|
|
|
# All 3 attempts failed — do NOT set POST_UPDATE_DONE=true.
|
|
# This allows the EXIT trap (on_exit in error_handler.func) to retry.
|
|
# No infinite loop risk: EXIT trap fires exactly once.
|
|
}
|
|
|
|
# ==============================================================================
|
|
# SECTION 3: EXTENDED TELEMETRY FUNCTIONS
|
|
# ==============================================================================
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# categorize_error()
|
|
#
|
|
# - Maps exit codes to error categories for better analytics
|
|
# - Categories: network, storage, dependency, permission, timeout, config, resource, unknown
|
|
# - Used to group errors in dashboard
|
|
# ------------------------------------------------------------------------------
|
|
categorize_error() {
|
|
local code="$1"
|
|
case "$code" in
|
|
# Network errors (curl/wget)
|
|
6 | 7 | 22 | 35) echo "network" ;;
|
|
|
|
# Docker / Privileged mode required
|
|
10) echo "config" ;;
|
|
|
|
# Timeout errors
|
|
28 | 124 | 211) echo "timeout" ;;
|
|
|
|
# Storage errors (Proxmox storage)
|
|
214 | 217 | 219 | 224) echo "storage" ;;
|
|
|
|
# Dependency/Package errors (APT, DPKG, pip, commands)
|
|
100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;;
|
|
|
|
# Permission errors
|
|
126 | 152) echo "permission" ;;
|
|
|
|
# Configuration errors (Proxmox config, invalid args)
|
|
128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;;
|
|
|
|
# Proxmox container/template errors
|
|
200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;;
|
|
|
|
# Service/Systemd errors
|
|
150 | 151 | 153 | 154) echo "service" ;;
|
|
|
|
# Database errors (PostgreSQL, MySQL, MongoDB)
|
|
170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;;
|
|
|
|
# Node.js / JavaScript runtime errors
|
|
243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;;
|
|
|
|
# Python environment errors
|
|
# (already covered: 160-162 under dependency)
|
|
|
|
# Aborted by user (SIGHUP=terminal closed, SIGINT=Ctrl+C, SIGTERM=killed)
|
|
129 | 130 | 143) echo "user_aborted" ;;
|
|
|
|
# Resource errors (OOM, SIGKILL, SIGABRT)
|
|
134 | 137) echo "resource" ;;
|
|
|
|
# Signal/Process errors (SIGPIPE, SIGSEGV)
|
|
139 | 141) echo "signal" ;;
|
|
|
|
# Shell errors (general error, syntax error)
|
|
1 | 2) echo "shell" ;;
|
|
|
|
# Default - truly unknown
|
|
*) echo "unknown" ;;
|
|
esac
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# start_install_timer()
|
|
#
|
|
# - Captures start time for installation duration tracking
|
|
# - Call at the beginning of installation
|
|
# - Sets INSTALL_START_TIME global variable
|
|
# ------------------------------------------------------------------------------
|
|
start_install_timer() {
|
|
INSTALL_START_TIME=$(date +%s)
|
|
export INSTALL_START_TIME
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# get_install_duration()
|
|
#
|
|
# - Returns elapsed seconds since start_install_timer() was called
|
|
# - Returns 0 if timer was not started
|
|
# ------------------------------------------------------------------------------
|
|
get_install_duration() {
|
|
if [[ -z "${INSTALL_START_TIME:-}" ]]; then
|
|
echo "0"
|
|
return
|
|
fi
|
|
local now=$(date +%s)
|
|
echo $((now - INSTALL_START_TIME))
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# _telemetry_report_exit()
|
|
#
|
|
# - Internal handler called by EXIT trap set in init_tool_telemetry()
|
|
# - Determines success/failure from exit code and reports via appropriate API
|
|
# - Arguments:
|
|
# * $1: exit_code from the script
|
|
# ------------------------------------------------------------------------------
|
|
_telemetry_report_exit() {
|
|
local ec="${1:-0}"
|
|
local status="success"
|
|
[[ "$ec" -ne 0 ]] && status="failed"
|
|
|
|
# Lazy name resolution: use explicit name, fall back to $APP, then "unknown"
|
|
local name="${TELEMETRY_TOOL_NAME:-${APP:-unknown}}"
|
|
|
|
if [[ "${TELEMETRY_TOOL_TYPE:-pve}" == "addon" ]]; then
|
|
post_addon_to_api "$name" "$status" "$ec"
|
|
else
|
|
post_tool_to_api "$name" "$status" "$ec"
|
|
fi
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# init_tool_telemetry()
|
|
#
|
|
# - One-line telemetry setup for tools/addon scripts
|
|
# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics
|
|
# (persisted on PVE host during first build, and inside containers by install.func)
|
|
# - Starts install timer for duration tracking
|
|
# - Sets EXIT trap to automatically report success/failure on script exit
|
|
# - Arguments:
|
|
# * $1: tool_name (optional, falls back to $APP at exit time)
|
|
# * $2: type ("pve" for PVE host scripts, "addon" for container addons)
|
|
# - Usage:
|
|
# source <(curl -fsSL .../misc/api.func) 2>/dev/null || true
|
|
# init_tool_telemetry "post-pve-install" "pve"
|
|
# init_tool_telemetry "" "addon" # uses $APP at exit time
|
|
# ------------------------------------------------------------------------------
|
|
init_tool_telemetry() {
|
|
local name="${1:-}"
|
|
local type="${2:-pve}"
|
|
|
|
[[ -n "$name" ]] && TELEMETRY_TOOL_NAME="$name"
|
|
TELEMETRY_TOOL_TYPE="$type"
|
|
|
|
# Read diagnostics opt-in/opt-out
|
|
if [[ -f /usr/local/community-scripts/diagnostics ]]; then
|
|
DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics 2>/dev/null | awk -F'=' '{print $2}') || true
|
|
fi
|
|
|
|
start_install_timer
|
|
|
|
# EXIT trap: automatically report telemetry when script ends
|
|
trap '_telemetry_report_exit "$?"' EXIT
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_tool_to_api()
|
|
#
|
|
# - Reports tool usage to telemetry
|
|
# - Arguments:
|
|
# * $1: tool_name (e.g., "microcode", "lxc-update", "post-pve-install")
|
|
# * $2: status ("success" or "failed")
|
|
# * $3: exit_code (optional, default: 0 for success, 1 for failed)
|
|
# - For PVE host tools, not container installations
|
|
# ------------------------------------------------------------------------------
|
|
post_tool_to_api() {
|
|
command -v curl &>/dev/null || return 0
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
|
|
|
local tool_name="${1:-unknown}"
|
|
local status="${2:-success}"
|
|
local exit_code="${3:-0}"
|
|
local error="" error_category=""
|
|
local uuid duration
|
|
|
|
# Generate UUID for this tool execution
|
|
uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "tool-$(date +%s)")
|
|
duration=$(get_install_duration)
|
|
|
|
# Map status
|
|
[[ "$status" == "done" ]] && status="success"
|
|
|
|
if [[ "$status" == "failed" ]]; then
|
|
[[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
|
|
local error_text=""
|
|
error_text=$(get_error_text)
|
|
local full_error
|
|
full_error=$(build_error_string "$exit_code" "$error_text")
|
|
error=$(json_escape "$full_error")
|
|
error_category=$(categorize_error "$exit_code")
|
|
fi
|
|
|
|
local pve_version=""
|
|
if command -v pveversion &>/dev/null; then
|
|
pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
|
|
fi
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${uuid}",
|
|
"execution_id": "${EXECUTION_ID:-${uuid}}",
|
|
"type": "pve",
|
|
"nsapp": "${tool_name}",
|
|
"status": "${status}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${error}",
|
|
"error_category": "${error_category}",
|
|
"install_duration": ${duration:-0},
|
|
"pve_version": "${pve_version}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" &>/dev/null || true
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_addon_to_api()
|
|
#
|
|
# - Reports addon installation to telemetry
|
|
# - Arguments:
|
|
# * $1: addon_name (e.g., "filebrowser", "netdata")
|
|
# * $2: status ("success" or "failed")
|
|
# * $3: exit_code (optional)
|
|
# - For addons installed inside containers
|
|
# ------------------------------------------------------------------------------
|
|
post_addon_to_api() {
|
|
command -v curl &>/dev/null || return 0
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
|
|
|
local addon_name="${1:-unknown}"
|
|
local status="${2:-success}"
|
|
local exit_code="${3:-0}"
|
|
local error="" error_category=""
|
|
local uuid duration
|
|
|
|
# Generate UUID for this addon installation
|
|
uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "addon-$(date +%s)")
|
|
duration=$(get_install_duration)
|
|
|
|
# Map status
|
|
[[ "$status" == "done" ]] && status="success"
|
|
|
|
if [[ "$status" == "failed" ]]; then
|
|
[[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
|
|
local error_text=""
|
|
error_text=$(get_error_text)
|
|
local full_error
|
|
full_error=$(build_error_string "$exit_code" "$error_text")
|
|
error=$(json_escape "$full_error")
|
|
error_category=$(categorize_error "$exit_code")
|
|
fi
|
|
|
|
# Detect OS info
|
|
local os_type="" os_version=""
|
|
if [[ -f /etc/os-release ]]; then
|
|
os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"')
|
|
os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"')
|
|
fi
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${uuid}",
|
|
"execution_id": "${EXECUTION_ID:-${uuid}}",
|
|
"type": "addon",
|
|
"nsapp": "${addon_name}",
|
|
"status": "${status}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${error}",
|
|
"error_category": "${error_category}",
|
|
"install_duration": ${duration:-0},
|
|
"os_type": "${os_type}",
|
|
"os_version": "${os_version}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" &>/dev/null || true
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# post_update_to_api_extended()
|
|
#
|
|
# - Extended version of post_update_to_api with duration, GPU, and error category
|
|
# - Same arguments as post_update_to_api:
|
|
# * $1: status ("done" or "failed")
|
|
# * $2: exit_code (numeric)
|
|
# - Automatically includes:
|
|
# * Install duration (if start_install_timer was called)
|
|
# * Error category (for failed status)
|
|
# * GPU info (if detect_gpu was called)
|
|
# ------------------------------------------------------------------------------
|
|
post_update_to_api_extended() {
|
|
# Silent fail - telemetry should never break scripts
|
|
command -v curl &>/dev/null || return 0
|
|
|
|
# Prevent duplicate submissions
|
|
POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
|
|
[[ "$POST_UPDATE_DONE" == "true" ]] && return 0
|
|
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
|
|
[[ -z "${RANDOM_UUID:-}" ]] && return 0
|
|
|
|
local status="${1:-failed}"
|
|
local raw_exit_code="${2:-1}"
|
|
local exit_code=0 error="" pb_status error_category=""
|
|
local duration gpu_vendor gpu_passthrough
|
|
|
|
# Get duration
|
|
duration=$(get_install_duration)
|
|
|
|
# Get GPU info (if detected)
|
|
gpu_vendor="${GPU_VENDOR:-}"
|
|
gpu_passthrough="${GPU_PASSTHROUGH:-}"
|
|
|
|
# Map status to telemetry values
|
|
case "$status" in
|
|
done | success)
|
|
pb_status="success"
|
|
exit_code=0
|
|
error=""
|
|
error_category=""
|
|
;;
|
|
failed)
|
|
pb_status="failed"
|
|
;;
|
|
*)
|
|
pb_status="unknown"
|
|
;;
|
|
esac
|
|
|
|
# For failed/unknown status, resolve exit code and error description
|
|
if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then
|
|
if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then
|
|
exit_code="$raw_exit_code"
|
|
else
|
|
exit_code=1
|
|
fi
|
|
local error_text=""
|
|
error_text=$(get_error_text)
|
|
local full_error
|
|
full_error=$(build_error_string "$exit_code" "$error_text")
|
|
error=$(json_escape "$full_error")
|
|
error_category=$(categorize_error "$exit_code")
|
|
[[ -z "$error" ]] && error="Unknown error"
|
|
fi
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
|
|
"type": "${TELEMETRY_TYPE:-lxc}",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "${pb_status}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${error}",
|
|
"error_category": "${error_category}",
|
|
"install_duration": ${duration:-0},
|
|
"gpu_vendor": "${gpu_vendor}",
|
|
"gpu_passthrough": "${gpu_passthrough}",
|
|
"repo_source": "${REPO_SOURCE}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
local http_code
|
|
http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
|
|
|
|
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
|
|
POST_UPDATE_DONE=true
|
|
return 0
|
|
fi
|
|
|
|
# Retry with minimal payload
|
|
sleep 1
|
|
http_code=$(curl -sS -w "%{http_code}" -m "${STATUS_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"${TELEMETRY_TYPE:-lxc}\",\"nsapp\":\"${NSAPP:-unknown}\",\"status\":\"${pb_status}\",\"exit_code\":${exit_code},\"install_duration\":${duration:-0}}" \
|
|
-o /dev/null 2>/dev/null) || http_code="000"
|
|
|
|
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
|
|
POST_UPDATE_DONE=true
|
|
return 0
|
|
fi
|
|
|
|
# Do NOT set POST_UPDATE_DONE=true — let EXIT trap retry
|
|
}
|