# 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) TELEMETRY_TIMEOUT=5 # ============================================================================== # 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' ' ' /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 # CI sed transforms this on promotion: ProxmoxVED → ProxmoxVE REPO_SOURCE="ProxmoxVED" ;; *) # 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, 2, 124, 126-130, 134, 137, 139, 141, 143) # * curl/wget errors (6, 7, 22, 28, 35) # * Package manager errors (APT, DPKG: 100-102, 255) # * 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 (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)" ;; # --- curl / wget errors (commonly seen in downloads) --- 6) echo "curl: DNS resolution failed (could not resolve host)" ;; 7) echo "curl: Failed to connect (network unreachable / host down)" ;; 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; 28) echo "curl: Operation timeout (network slow or server not responding)" ;; 35) echo "curl: SSL/TLS handshake failed (certificate 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)" ;; # --- Common shell/system errors --- 124) echo "Command timed out (timeout command)" ;; 126) echo "Command invoked cannot execute (permission problem?)" ;; 127) echo "Command not found" ;; 128) echo "Invalid argument to exit" ;; 130) echo "Aborted by user (SIGINT)" ;; 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)" ;; # --- 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 (243-249) --- 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 # - Handles backslashes, quotes, newlines, tabs, and carriage returns # ------------------------------------------------------------------------------ json_escape() { local s="$1" s=${s//\\/\\\\} s=${s//"/\\"} s=${s//$'\n'/\\n} s=${s//$'\r'/} s=${s//$'\t'/\\t} echo "$s" } # ------------------------------------------------------------------------------ # get_error_text() # # - Returns last 20 lines of the active log (INSTALL_LOG or BUILD_LOG) # - Falls back to empty string if no log is available # ------------------------------------------------------------------------------ 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 [[ -n "$logfile" && -s "$logfile" ]]; then tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' 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 # ------------------------------------------------------------------------------ detect_ram() { RAM_SPEED="" if command -v dmidecode &>/dev/null; then # Get configured memory speed (actual running speed) RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) # 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) 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:-}" 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:-}" # 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 <&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:-}" 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:-}" # 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 </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:-}" local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}" # Get CPU info (if detected) local cpu_vendor="${CPU_VENDOR:-unknown}" local cpu_model="${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 local error_text="" error_text=$(get_error_text) if [[ -n "$error_text" ]]; then error=$(json_escape "$error_text") else error=$(json_escape "$(explain_exit_code "$exit_code")") fi 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 ── local JSON_PAYLOAD JSON_PAYLOAD=$( cat </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 </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 </dev/null || true # Tried 3 times - mark as done regardless to prevent infinite loops POST_UPDATE_DONE=true } # ============================================================================== # 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 6 | 7 | 22 | 28 | 35) echo "network" ;; # Storage errors 214 | 217 | 219) echo "storage" ;; # Dependency/Package errors 100 | 101 | 102 | 127 | 160 | 161 | 162) echo "dependency" ;; # Permission errors 126 | 152) echo "permission" ;; # Timeout errors 124 | 28 | 211) echo "timeout" ;; # Configuration errors 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; # Aborted by user 130) echo "aborted" ;; # Resource errors (OOM, etc) 137 | 134) echo "resource" ;; # Default *) 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)) } # ------------------------------------------------------------------------------ # 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) if [[ -n "$error_text" ]]; then error=$(json_escape "$error_text") else error=$(json_escape "$(explain_exit_code "$exit_code")") fi 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 </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) if [[ -n "$error_text" ]]; then error=$(json_escape "$error_text") else error=$(json_escape "$(explain_exit_code "$exit_code")") fi 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 </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) if [[ -n "$error_text" ]]; then error=$(json_escape "$error_text") else error=$(json_escape "$(explain_exit_code "$exit_code")") fi error_category=$(categorize_error "$exit_code") [[ -z "$error" ]] && error="Unknown error" fi local JSON_PAYLOAD JSON_PAYLOAD=$( cat </dev/null || true POST_UPDATE_DONE=true }