ProxmoxVED/misc/api.func
CanbiZ (MickLesk) 820d4551a1 Replace Go API with PocketBase; update docs
Remove the old Go/Mongo API (api/main.go, go.mod, go.sum, .env.example) and switch telemetry backend to PocketBase (http://db.community-scripts.org). Update documentation and flowcharts to reflect the PocketBase collection (_dev_telemetry_data), new REST endpoints (POST/PATCH/GET), field schema, and revised api.func integration (LXC/VM reporting and status updates). Misc scripts and helpers were adjusted (misc/api.func, misc/build.func, misc/error_handler.func) and a new misc/ingest.go was added. This consolidates telemetry to a hosted PocketBase instance and updates docs and integration points accordingly.
2026-02-09 15:34:17 +01:00

420 lines
14 KiB
Bash

# 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 <<EOF
{
"ct_type": ${CT_TYPE:-1},
"type": "lxc",
"disk_size": ${DISK_SIZE:-0},
"core_count": ${CORE_COUNT:-0},
"ram_size": ${RAM_SIZE:-0},
"os_type": "${var_os:-}",
"os_version": "${var_version:-}",
"nsapp": "${NSAPP:-}",
"method": "${METHOD:-default}",
"pve_version": "${pve_version}",
"status": "installing",
"random_id": "${RANDOM_UUID}"
}
EOF
)
local RESPONSE
RESPONSE=$(curl -s -w "\n%{http_code}" -L -X POST "${PB_API_URL}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" 2>/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 <<EOF
{
"ct_type": 2,
"type": "vm",
"disk_size": ${DISK_SIZE_API:-0},
"core_count": ${CORE_COUNT:-0},
"ram_size": ${RAM_SIZE:-0},
"os_type": "${var_os:-}",
"os_version": "${var_version:-}",
"nsapp": "${NSAPP:-}",
"method": "${METHOD:-default}",
"pve_version": "${pve_version}",
"status": "installing",
"random_id": "${RANDOM_UUID}"
}
EOF
)
local RESPONSE
RESPONSE=$(curl -s -w "\n%{http_code}" -L -X POST "${PB_API_URL}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" 2>/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 <<EOF
{
"status": "${pb_status}",
"error": "${error:-}",
"exit_code": ${exit_code:-0}
}
EOF
)
# PATCH to update the existing record
curl -s -L -X PATCH "${PB_API_URL}/${record_id}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" &>/dev/null || true
POST_UPDATE_DONE=true
}