Send only changing fields for status updates and add server-side update flow. - Trimmed telemetry JSON payload in misc/api.func to include only status, error, and exit_code (removed static fields and pve_version) so updates are minimal. - Added TelemetryStatusUpdate type and new PBClient methods: FindRecordByRandomID, UpdateTelemetryStatus, and UpsertTelemetry in misc/data/service.go. UpsertTelemetry creates a record for status="installing", otherwise finds the record by random_id and PATCHes only status/error/exit_code (fallbacks to create if not found). - Relaxed validation logic in validate(): detect updates (status != "installing") and skip certain strict numeric checks for update requests while keeping required fields and other validations. - Main handler now calls UpsertTelemetry instead of CreateTelemetry and logs generic errors. These changes allow idempotent, minimal updates to existing telemetry records and avoid repeatedly sending/storing unchanged metadata.
410 lines
14 KiB
Bash
410 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
|
|
# ==============================================================================
|
|
#
|
|
# 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 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 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() {
|
|
# DEBUG: Show function entry
|
|
echo "[DEBUG] post_to_api() called" >&2
|
|
|
|
# Silent fail - telemetry should never break scripts
|
|
command -v curl &>/dev/null || {
|
|
echo "[DEBUG] curl not found, skipping" >&2
|
|
return 0
|
|
}
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && {
|
|
echo "[DEBUG] DIAGNOSTICS=no, skipping" >&2
|
|
return 0
|
|
}
|
|
[[ -z "${RANDOM_UUID:-}" ]] && {
|
|
echo "[DEBUG] RANDOM_UUID empty, skipping" >&2
|
|
return 0
|
|
}
|
|
|
|
echo "[DEBUG] Checks passed: 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
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_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}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
echo "[DEBUG] Sending to: $TELEMETRY_URL" >&2
|
|
echo "[DEBUG] Payload: $JSON_PAYLOAD" >&2
|
|
|
|
# Fire-and-forget: never block, never fail
|
|
local http_code
|
|
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
|
|
}
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# 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
|
|
# - 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
|
|
|
|
# Remove 'G' suffix from disk size
|
|
local DISK_SIZE_API="${DISK_SIZE%G}"
|
|
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_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}"
|
|
}
|
|
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_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"→"sucess", "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() {
|
|
# DEBUG: Show function entry
|
|
echo "[DEBUG] post_update_to_api() called with status=$1 exit_code=$2" >&2
|
|
|
|
# Silent fail - telemetry should never break scripts
|
|
command -v curl &>/dev/null || {
|
|
echo "[DEBUG] curl not found, skipping" >&2
|
|
return 0
|
|
}
|
|
|
|
# Prevent duplicate submissions
|
|
POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
|
|
[[ "$POST_UPDATE_DONE" == "true" ]] && {
|
|
echo "[DEBUG] Already sent update, skipping" >&2
|
|
return 0
|
|
}
|
|
|
|
[[ "${DIAGNOSTICS:-no}" == "no" ]] && {
|
|
echo "[DEBUG] DIAGNOSTICS=no, skipping" >&2
|
|
return 0
|
|
}
|
|
[[ -z "${RANDOM_UUID:-}" ]] && {
|
|
echo "[DEBUG] RANDOM_UUID empty, skipping" >&2
|
|
return 0
|
|
}
|
|
|
|
local status="${1:-failed}"
|
|
local raw_exit_code="${2:-1}"
|
|
local exit_code=0 error="" pb_status
|
|
|
|
# Map status to telemetry 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/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
|
|
error=$(explain_exit_code "$exit_code")
|
|
[[ -z "$error" ]] && error="Unknown error"
|
|
fi
|
|
|
|
# Update payload: only fields that change (status, error, exit_code)
|
|
# The Go service will find the record by random_id and PATCH only these fields
|
|
local JSON_PAYLOAD
|
|
JSON_PAYLOAD=$(
|
|
cat <<EOF
|
|
{
|
|
"random_id": "${RANDOM_UUID}",
|
|
"type": "${TELEMETRY_TYPE:-lxc}",
|
|
"nsapp": "${NSAPP:-unknown}",
|
|
"status": "${pb_status}",
|
|
"exit_code": ${exit_code},
|
|
"error": "${error}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
echo "[DEBUG] Sending update to: $TELEMETRY_URL" >&2
|
|
echo "[DEBUG] Update payload: $JSON_PAYLOAD" >&2
|
|
|
|
# Fire-and-forget: never block, never fail
|
|
local http_code
|
|
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
|
|
|
|
POST_UPDATE_DONE=true
|
|
}
|