# Copyright (c) 2021-2026 community-scripts ORG # Author: michelroegl-brunner # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/LICENSE # ============================================================================== # API.FUNC - TELEMETRY & DIAGNOSTICS API (PocketBase) # ============================================================================== # # Provides functions for sending anonymous telemetry data to PocketBase # backend at db.community-scripts.org for analytics and diagnostics. # # 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 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 settings # - Random UUID for session tracking only # # ============================================================================== # ============================================================================== # PocketBase Configuration # ============================================================================== PB_URL="http://db.community-scripts.org" PB_COLLECTION="_dev_telemetry_data" PB_API_URL="${PB_URL}/api/collections/${PB_COLLECTION}/records" # Store PocketBase record ID for update operations PB_RECORD_ID="" # ============================================================================== # 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 "Terminated by Ctrl+C (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 } # ============================================================================== # SECTION 2: TELEMETRY FUNCTIONS # ============================================================================== # ------------------------------------------------------------------------------ # post_to_api() # # - Sends LXC container creation statistics to PocketBase # - Creates a new record in the _dev_telemetry_data collection # - 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 # - Stores PB_RECORD_ID for later updates # - Anonymous telemetry (no personal data) # ------------------------------------------------------------------------------ post_to_api() { if ! command -v curl &>/dev/null; then return fi if [[ "${DIAGNOSTICS:-no}" == "no" ]]; then return fi if [[ -z "${RANDOM_UUID:-}" ]]; then return fi local pve_version="not found" if command -v pveversion &>/dev/null; then pve_version=$(pveversion | awk -F'[/ ]' '{print $2}') fi local JSON_PAYLOAD JSON_PAYLOAD=$( cat </dev/null) || true # Extract PocketBase record ID from response for later updates local http_code body http_code=$(echo "$RESPONSE" | tail -n1) body=$(echo "$RESPONSE" | sed '$d') if [[ "$http_code" == "200" ]] || [[ "$http_code" == "201" ]]; then PB_RECORD_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) || true fi } # ------------------------------------------------------------------------------ # post_to_api_vm() # # - Sends VM creation statistics to PocketBase # - Similar to post_to_api() but for virtual machines (not containers) # - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics file # - Payload differences: # * ct_type=2 (VM instead of LXC) # * type="vm" # * Disk size without 'G' suffix (parsed from DISK_SIZE variable) # - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set # ------------------------------------------------------------------------------ post_to_api_vm() { if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then return fi DIAGNOSTICS=$(grep -i "^DIAGNOSTICS=" /usr/local/community-scripts/diagnostics | awk -F'=' '{print $2}') if ! command -v curl &>/dev/null; then return fi if [[ "${DIAGNOSTICS:-no}" == "no" ]]; then return fi if [[ -z "${RANDOM_UUID:-}" ]]; then return fi local pve_version="not found" if command -v pveversion &>/dev/null; then pve_version=$(pveversion | awk -F'[/ ]' '{print $2}') fi local DISK_SIZE_API="${DISK_SIZE%G}" local JSON_PAYLOAD JSON_PAYLOAD=$( cat </dev/null) || true # Extract PocketBase record ID from response for later updates local http_code body http_code=$(echo "$RESPONSE" | tail -n1) body=$(echo "$RESPONSE" | sed '$d') if [[ "$http_code" == "200" ]] || [[ "$http_code" == "201" ]]; then PB_RECORD_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) || true fi } # ------------------------------------------------------------------------------ # post_update_to_api() # # - Reports installation completion status to PocketBase via PATCH # - 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) # - Uses PB_RECORD_ID if available, otherwise looks up by random_id # - Payload includes: # * Final status (mapped: "done"→"sucess", "failed"→"failed") # * Error description via explain_exit_code() # * Numeric exit code # - Only executes once per session # - Silently returns if: # * curl not available # * Already reported (POST_UPDATE_DONE=true) # * DIAGNOSTICS=no # ------------------------------------------------------------------------------ post_update_to_api() { if ! command -v curl &>/dev/null; then return fi # Initialize flag if not set (prevents 'unbound variable' error with set -u) POST_UPDATE_DONE=${POST_UPDATE_DONE:-false} if [[ "$POST_UPDATE_DONE" == "true" ]]; then return 0 fi if [[ "${DIAGNOSTICS:-no}" == "no" ]]; then return fi if [[ -z "${RANDOM_UUID:-}" ]]; then return fi local status="${1:-failed}" local raw_exit_code="${2:-1}" local exit_code error pb_status # Map status to PocketBase select values: installing, sucess, failed, unknown case "$status" in done | success | sucess) pb_status="sucess" exit_code=0 error="" ;; failed) pb_status="failed" ;; *) pb_status="unknown" ;; esac # For failed status, resolve exit code and error description if [[ "$pb_status" == "failed" ]] || [[ "$pb_status" == "unknown" ]]; then # If exit_code is numeric, use it; otherwise default to 1 if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then exit_code="$raw_exit_code" else exit_code=1 fi error=$(explain_exit_code "$exit_code") if [[ -z "$error" ]]; then error="Unknown error" fi fi # Resolve PocketBase record ID if not already known local record_id="${PB_RECORD_ID:-}" if [[ -z "$record_id" ]]; then # Look up record by random_id filter local lookup_url="${PB_API_URL}?filter=(random_id='${RANDOM_UUID}')&fields=id&perPage=1" local lookup_response lookup_response=$(curl -s -L "${lookup_url}" 2>/dev/null) || true record_id=$(echo "$lookup_response" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) || true if [[ -z "$record_id" ]]; then POST_UPDATE_DONE=true return fi fi local JSON_PAYLOAD JSON_PAYLOAD=$( cat </dev/null || true POST_UPDATE_DONE=true }