From ab68fea3b2c3629e978510e0d537243d75d9cfef Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:02:38 +0100 Subject: [PATCH] Drop misc/api.func vm/owncloud-vm.sh was its last caller; DevScripts holds that script's rewrite onto core/pve/vm-core.func. Merge this after the promotion. --- .github/autolabeler-config.json | 3 +- README.md | 1 - misc/api.func | 1224 ------------------------------- 3 files changed, 1 insertion(+), 1227 deletions(-) delete mode 100644 misc/api.func diff --git a/.github/autolabeler-config.json b/.github/autolabeler-config.json index 39c7b919c..22391b30f 100644 --- a/.github/autolabeler-config.json +++ b/.github/autolabeler-config.json @@ -103,8 +103,7 @@ { "fileStatus": null, "includeGlobs": [ - "api/**", - "misc/api.func" + "api/**" ], "excludeGlobs": [] } diff --git a/README.md b/README.md index c629f2c17..d44a494e4 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,6 @@ Virtual machine scripts need the same versions as containers. | `vm/` | Virtual machine scripts | | `tools/pve/` | Host tooling: post-install, hardware passthrough, maintenance | | `turnkey/` | TurnKey Linux appliance helper | -| `misc/` | One legacy engine file left, loaded only by `vm/owncloud-vm.sh` — everything else loads [core](https://github.com/community-scripts/core) at runtime | Application metadata — name, description, logo, tags — is managed on the website, not in this repository. Use the **Report Issue** link on any script page to diff --git a/misc/api.func b/misc/api.func deleted file mode 100644 index d413feed9..000000000 --- a/misc/api.func +++ /dev/null @@ -1,1224 +0,0 @@ -# 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. -# -# DESIGN PRINCIPLES: -# 1. SINGLE REPORTER: only the HOST sends terminal statuses (success/failed/ -# aborted). Containers never curl the telemetry endpoint for terminal -# events - they write local artifact files (.failed flag + .errinfo) that -# the host picks up. This prevents the "first terminal event wins" race -# on the server from persisting a metadata-less container payload and -# discarding the full host payload. -# 2. FULL PAYLOAD, ALWAYS: every send includes the complete system metadata -# (os, pve version, arch, cpu, gpu, ram, repo attribution). No more -# minimal payloads that create empty records. -# 3. FOCUSED ERROR TRACE: the error field contains exactly the output of the -# command that failed (captured by silent() via byte offset), prefixed -# with "exit_code=N | | at line L: ". Not a 120KB -# log dump, not the wrong phase of the log. -# 4. IDEMPOTENT FINALIZE: post_update_to_api() sends exactly one terminal -# status per execution, no matter how many traps fire. -# -# Usage: -# source <(curl -fsSL .../api.func) -# post_to_api # Report LXC container creation ("installing") -# post_to_api_vm # Report VM creation ("installing") -# post_progress_to_api # Progress ping ("validation"/"configuring") -# post_update_to_api # Final status ("done"/"failed"/"aborted") -# -# Privacy: -# - Only anonymous statistics (no personal data, IPs are anonymized) -# - User can opt-out via DIAGNOSTICS=no -# - Random UUID for session tracking only -# -# ============================================================================== - -# ============================================================================== -# Telemetry Configuration -# ============================================================================== -TELEMETRY_URL="${TELEMETRY_URL:-https://telemetry.community-scripts.org/telemetry}" - -# Timeout for progress pings (seconds) -TELEMETRY_TIMEOUT=5 -# Timeout for final status updates (they carry the error trace) -STATUS_TIMEOUT=10 - -# Max size of the error trace sent to the API (bytes) and max line count. -# Keep this SMALL and FOCUSED - the goal is "exactly the messages from the -# moment of failure", not the whole installation log. -TELEMETRY_ERROR_MAX_LINES=60 -TELEMETRY_ERROR_MAX_BYTES=10240 - -# ============================================================================== -# 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 -# - Additionally sets REPO_SLUG to the real "owner/repo" string -# - Fallback: "ProxmoxVE" (this file lives in the production repo) -# - Skips detection if REPO_SOURCE is already set (e.g., exported by the host -# into the container environment) -# ------------------------------------------------------------------------------ -detect_repo_source() { - # Allow explicit override via environment (also how the container inherits - # the host's attribution instead of re-detecting) - if [[ -n "${REPO_SOURCE:-}" ]]; then - if [[ -z "${REPO_SLUG:-}" ]]; then - case "$REPO_SOURCE" in - ProxmoxVE) REPO_SLUG="community-scripts/ProxmoxVE" ;; - ProxmoxVED) REPO_SLUG="community-scripts/ProxmoxVED" ;; - esac - export REPO_SLUG - fi - return 0 - fi - - local content="" owner_repo="" - - # Method 1: Read from /proc/$$/cmdline - # When invoked via: bash -c "$(curl -fsSL https://.../ct/app.sh)" - if [[ -r /proc/$$/cmdline ]]; then - content=$(tr '\0' ' ' /dev/null) || true - fi - - # Method 2: Read from the original script file (bash ct/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 - - if [[ -n "$content" ]]; then - owner_repo=$(echo "$content" | grep -oE 'raw\.githubusercontent\.com/[^/]+/[^/]+' | head -1 | sed 's|raw\.githubusercontent\.com/||') || true - 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 - - case "$owner_repo" in - community-scripts/ProxmoxVE) - REPO_SOURCE="ProxmoxVE" - REPO_SLUG="community-scripts/ProxmoxVE" - ;; - community-scripts/ProxmoxVED) - REPO_SOURCE="ProxmoxVED" - REPO_SLUG="community-scripts/ProxmoxVED" - ;; - "") - # No URL detected — hardcoded fallback (production repo) - REPO_SOURCE="ProxmoxVE" - REPO_SLUG="community-scripts/ProxmoxVE" - ;; - *) - REPO_SOURCE="external" - REPO_SLUG="$owner_repo" - ;; - esac - - export REPO_SOURCE REPO_SLUG -} - -# Run detection immediately when api.func is sourced -detect_repo_source - -# ============================================================================== -# SECTION 1: ERROR CODE DESCRIPTIONS & CATEGORIES -# ============================================================================== - -# ------------------------------------------------------------------------------ -# 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) -# ------------------------------------------------------------------------------ -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 --- - 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)" ;; - - # --- Script Validation & Setup (103-123) --- - 103) echo "Validation: Shell is not Bash" ;; - 104) echo "Validation: Not running as root (or invoked via sudo)" ;; - 105) echo "Validation: Proxmox VE version not supported" ;; - 106) echo "Validation: Unsupported architecture (requires amd64 or arm64)" ;; - 107) echo "Validation: Kernel key parameters unreadable" ;; - 108) echo "Validation: Kernel key limits exceeded" ;; - 109) echo "Proxmox: No available container ID after max attempts" ;; - 110) echo "Proxmox: Failed to apply default.vars" ;; - 111) echo "Proxmox: App defaults file not available" ;; - 112) echo "Proxmox: Invalid install menu option" ;; - 113) echo "LXC: Under-provisioned — user aborted update" ;; - 114) echo "LXC: Storage too low — user aborted update" ;; - 115) echo "Download: install.func download failed or incomplete" ;; - 116) echo "Proxmox: Default bridge vmbr0 not found" ;; - 117) echo "LXC: Container did not reach running state" ;; - 118) echo "LXC: No IP assigned to container after timeout" ;; - 119) echo "Proxmox: No valid storage for rootdir content" ;; - 120) echo "Proxmox: No valid storage for vztmpl content" ;; - 121) echo "LXC: Container network not ready (no IP after retries)" ;; - 122) echo "LXC: No internet connectivity — user declined to continue" ;; - 123) echo "LXC: Local IP detection failed" ;; - - # --- 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" ;; - 226) echo "Proxmox: VM disk import or post-creation setup failed" ;; - 231) echo "Proxmox: LXC stack upgrade failed" ;; - - # --- Tools & Addon Scripts (232-238) --- - 232) echo "Tools: Wrong execution environment (run on PVE host, not inside LXC)" ;; - 233) echo "Tools: Application not installed (update prerequisite missing)" ;; - 234) echo "Tools: No LXC containers found or available" ;; - 235) echo "Tools: Backup or restore operation failed" ;; - 236) echo "Tools: Required hardware not detected" ;; - 237) echo "Tools: Dependency package installation failed" ;; - 238) echo "Tools: OS or distribution not supported for this addon" ;; - - # --- 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" ;; - - # --- Application Install/Update Errors (250-254) --- - 250) echo "App: Download failed or version not determined" ;; - 251) echo "App: File extraction failed (corrupt or incomplete archive)" ;; - 252) echo "App: Required file or resource not found" ;; - 253) echo "App: Data migration required — update aborted" ;; - 254) echo "App: User declined prompt or input timed out" ;; - - # --- DPKG --- - 255) echo "DPKG: Fatal internal error" ;; - - # --- Default --- - *) echo "Unknown error" ;; - esac -} - -# ------------------------------------------------------------------------------ -# categorize_error() -# -# - Maps exit codes to error categories for dashboard grouping -# - Can be overridden via ERROR_CATEGORY_OVERRIDE (log-based subclassification) -# ------------------------------------------------------------------------------ -categorize_error() { - if [[ -n "${ERROR_CATEGORY_OVERRIDE:-}" ]]; then - echo "$ERROR_CATEGORY_OVERRIDE" - return - fi - - local code="$1" - case "$code" in - 6 | 7 | 22 | 35) echo "network" ;; - 10) echo "config" ;; - 28 | 124 | 211) echo "timeout" ;; - 214 | 217 | 219 | 224) echo "storage" ;; - 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; - 126 | 152) echo "permission" ;; - 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; - 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;; - 150 | 151 | 153 | 154) echo "service" ;; - 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; - 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; - 129 | 130 | 143) echo "user_aborted" ;; - 134 | 137) echo "resource" ;; - 139 | 141) echo "signal" ;; - 1 | 2) echo "shell" ;; - *) echo "unknown" ;; - esac -} - -# ============================================================================== -# SECTION 2: JSON & LOG HELPERS -# ============================================================================== - -# ------------------------------------------------------------------------------ -# json_escape() -# -# - Escapes a string for safe JSON embedding -# - Strips ANSI escape sequences and non-printable control characters -# - Uses jq when available (guaranteed correct), falls back to awk -# ------------------------------------------------------------------------------ -json_escape() { - local input - input=$(printf '%s' "$1" | - sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | - tr -d '\000-\010\013\014\016-\037\177\r') - - if command -v jq &>/dev/null; then - printf '%s' "$input" | jq -Rs '.' | sed 's/^"//;s/"$//' - return - fi - - # Fallback: character-by-character processing with awk - printf '%s' "$input" | - awk ' - BEGIN { ORS="" } - { - if (NR > 1) printf "%s", "\\n" - for (i = 1; i <= length($0); i++) { - c = substr($0, i, 1) - if (c == "\\") printf "%s", "\\\\" - else if (c == "\"") printf "%s", "\\\"" - else if (c == "\t") printf "%s", "\\t" - else printf "%s", c - } - }' -} - -# ------------------------------------------------------------------------------ -# _tm_clean_log() -# -# - stdin filter: strips ANSI/CR, anonymizes IPs (GDPR), drops pure -# progress-noise lines (apt/dpkg download and unpack chatter) -# ------------------------------------------------------------------------------ -_tm_clean_log() { - sed 's/\r$//' | - 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' | - grep -avE '^(Get:|Hit:|Ign:|Fetched |Reading package lists|Reading state information|Building dependency tree|Selecting previously|Preparing to unpack|Unpacking |Processing triggers for|\(Reading database|[0-9]+%[[:space:]]*\[)' | - grep -avE '^[[:space:]]*$' || true -} - -# ------------------------------------------------------------------------------ -# _tm_pick_logfile() -# -# - Returns the most specific available log file (combined → INSTALL_LOG → -# BUILD_LOG → SILENT_LOGFILE). Never downgrades a present log. -# ------------------------------------------------------------------------------ -_tm_pick_logfile() { - local candidate - for candidate in \ - "${combined_log:-}" \ - "/tmp/install-${SESSION_ID:-}-combined.log" \ - "/tmp/${NSAPP:-lxc}-${CTID:-}-${SESSION_ID:-}.log" \ - "${INSTALL_LOG:-}" \ - "${BUILD_LOG:-}" \ - "${SILENT_LOGFILE:-}"; do - if [[ -n "$candidate" && -s "$candidate" ]]; then - echo "$candidate" - return 0 - fi - done - return 0 -} - -# ------------------------------------------------------------------------------ -# get_error_log() -# -# - Returns the last N (default 50) RELEVANT lines of the active log -# - Fallback error source when no .errinfo capture exists -# ------------------------------------------------------------------------------ -get_error_log() { - local max_lines="${1:-50}" - local logfile - - if declare -f ensure_log_on_host >/dev/null 2>&1; then - ensure_log_on_host 2>/dev/null || true - fi - - logfile=$(_tm_pick_logfile) - [[ -z "$logfile" || ! -s "$logfile" ]] && return 0 - - _tm_clean_log <"$logfile" | tail -n "$max_lines" -} - -# ------------------------------------------------------------------------------ -# get_error_text() (legacy compatibility) -# -# - Returns last 20 relevant lines of the active log -# ------------------------------------------------------------------------------ -get_error_text() { - get_error_log 20 -} - -# ------------------------------------------------------------------------------ -# get_full_log() (legacy compatibility - dev/debug use only) -# -# - Returns the full log, ANSI-stripped and IP-anonymized, capped at max_bytes -# ------------------------------------------------------------------------------ -get_full_log() { - local max_bytes="${1:-122880}" - local logfile - if declare -f ensure_log_on_host >/dev/null 2>&1; then - ensure_log_on_host 2>/dev/null || true - fi - logfile=$(_tm_pick_logfile) - [[ -z "$logfile" || ! -s "$logfile" ]] && return 0 - 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" -} - -# ============================================================================== -# SECTION 3: STRUCTURED ERROR CAPTURE (.errinfo protocol) -# ============================================================================== -# -# When a command run via silent() fails, silent() writes ".errinfo": -# -# EXIT_CODE= -# LINE= -# COMMAND= -# --- OUTPUT --- -# -# -# error_handler() writes the same file for non-silent failures (best effort: -# tail of the active log). The HOST reads this file (pulling it from the -# container if needed) and builds the telemetry error field from it. -# ============================================================================== - -# ------------------------------------------------------------------------------ -# _tm_find_errinfo() -# -# - Locates the newest .errinfo file for this session -# - Honors TELEMETRY_ERRINFO override (set by the host after pulling the -# container's copy) -# ------------------------------------------------------------------------------ -_tm_find_errinfo() { - local candidate - for candidate in \ - "${TELEMETRY_ERRINFO:-}" \ - "/tmp/.errinfo-${SESSION_ID:-none}" \ - "${INSTALL_LOG:-/nonexistent}.errinfo" \ - "${BUILD_LOG:-/nonexistent}.errinfo"; do - if [[ -n "$candidate" && -s "$candidate" ]]; then - echo "$candidate" - return 0 - fi - done - return 1 -} - -# ------------------------------------------------------------------------------ -# write_errinfo() -# -# - Writes a structured .errinfo file next to the given logfile -# - Arguments: -# * $1: target file path -# * $2: exit_code -# * $3: line number -# * $4: command (flattened to one line, truncated) -# * stdin: output segment of the failing command -# ------------------------------------------------------------------------------ -write_errinfo() { - local target="$1" exit_code="$2" line="$3" command="$4" - command=$(printf '%s' "$command" | tr '\n' ' ' | head -c 300) - { - echo "EXIT_CODE=${exit_code}" - echo "LINE=${line}" - echo "COMMAND=${command}" - echo "--- OUTPUT ---" - _tm_clean_log | tail -n "${TELEMETRY_ERROR_MAX_LINES}" | head -c "${TELEMETRY_ERROR_MAX_BYTES}" - } >"$target" 2>/dev/null || true -} - -# ------------------------------------------------------------------------------ -# build_error_string() -# -# - Builds the final structured error string for telemetry: -# "exit_code=N | | at line L: \n---\n" -# - Sources, in priority order: -# 1. .errinfo file (exact output of the failing command) -# 2. FAILED_COMMAND/FAILED_LINE globals + filtered log tail -# 3. Explanation only -# - Arguments: -# * $1: exit_code -# * $2: log_text (optional override for the output section) -# ------------------------------------------------------------------------------ -build_error_string() { - local exit_code="${1:-1}" - local log_text="${2:-}" - local explanation location="" cmd="" line="" errinfo="" - - # Prefer the structured capture from the failure moment - if errinfo=$(_tm_find_errinfo); then - local captured_code - captured_code=$(sed -n 's/^EXIT_CODE=//p' "$errinfo" | head -1) - line=$(sed -n 's/^LINE=//p' "$errinfo" | head -1) - cmd=$(sed -n 's/^COMMAND=//p' "$errinfo" | head -1) - # Only trust the capture when it matches the reported failure (or the - # reported code is the generic 1 that bash propagates upward) - if [[ -n "$captured_code" && ("$captured_code" == "$exit_code" || "$exit_code" == "1" || "$exit_code" == "255") ]]; then - exit_code="$captured_code" - if [[ -z "$log_text" ]]; then - log_text=$(awk 'found{print} /^--- OUTPUT ---$/{found=1}' "$errinfo") - fi - else - cmd="" line="" - fi - fi - - # Fall back to globals exported by silent()/error_handler - [[ -z "$cmd" && -n "${FAILED_COMMAND:-}" ]] && cmd="${FAILED_COMMAND}" - [[ -z "$line" && -n "${FAILED_LINE:-}" ]] && line="${FAILED_LINE}" - - explanation=$(explain_exit_code "$exit_code") - - if [[ -n "$cmd" ]]; then - if [[ -n "$line" ]]; then - location=$(printf ' | at line %s: %s' "$line" "$cmd") - else - location=$(printf ' | command: %s' "$cmd") - fi - fi - - # Last-resort output: filtered tail of the active log - if [[ -z "$log_text" ]]; then - log_text=$(get_error_log 40) || true - fi - - if [[ -n "$log_text" ]]; then - printf 'exit_code=%s | %s%s\n---\n%s' "$exit_code" "$explanation" "$location" "$log_text" - else - printf 'exit_code=%s | %s%s' "$exit_code" "$explanation" "$location" - fi -} - -# ============================================================================== -# SECTION 4: SYSTEM INFO COLLECTION (collected once, sent with EVERY payload) -# ============================================================================== - -# ------------------------------------------------------------------------------ -# detect_gpu() - GPU vendor, model, passthrough type -# ------------------------------------------------------------------------------ -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 || true) - - if [[ -n "$gpu_line" ]]; then - GPU_MODEL=$(echo "$gpu_line" | sed 's/.*: //' | sed 's/ (rev .*)$//' | cut -c1-64) - - 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() - CPU vendor and model -# ------------------------------------------------------------------------------ -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 ' ' || true) - - case "$vendor_id" in - GenuineIntel) CPU_VENDOR="intel" ;; - AuthenticAMD) CPU_VENDOR="amd" ;; - *) - if grep -qi "CPU implementer" /proc/cpuinfo 2>/dev/null; then - CPU_VENDOR="arm" - fi - ;; - esac - - 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 || true) - fi - - export CPU_VENDOR CPU_MODEL -} - -# ------------------------------------------------------------------------------ -# detect_ram() - RAM speed via dmidecode (empty in nested VMs) -# ------------------------------------------------------------------------------ -detect_ram() { - RAM_SPEED="" - - if command -v dmidecode &>/dev/null; then - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true - 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 -} - -# ------------------------------------------------------------------------------ -# detect_arm() - true when running on arm64 hardware -# ------------------------------------------------------------------------------ -detect_arm() { - HAS_ARM="false" - case "$(dpkg --print-architecture 2>/dev/null || uname -m)" in - arm64 | aarch64) HAS_ARM="true" ;; - esac - export HAS_ARM -} - -# ------------------------------------------------------------------------------ -# telemetry_collect_sysinfo() -# -# - Collects ALL system metadata exactly once (cached via _TM_SYSINFO_DONE) -# - Sets: CPU_VENDOR, CPU_MODEL, GPU_VENDOR, GPU_MODEL, GPU_PASSTHROUGH, -# RAM_SPEED, HAS_ARM, TM_ARCH, TM_PVE_VERSION, TM_PLATFORM -# ------------------------------------------------------------------------------ -telemetry_collect_sysinfo() { - [[ "${_TM_SYSINFO_DONE:-}" == "true" ]] && return 0 - - [[ -z "${GPU_VENDOR:-}" ]] && detect_gpu - [[ -z "${CPU_VENDOR:-}" ]] && detect_cpu - [[ -z "${RAM_SPEED+x}" ]] && detect_ram - [[ -z "${HAS_ARM:-}" ]] && detect_arm - - TM_ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m || echo unknown)" - - # Virtualization platform: pve (Proxmox VE) or incus. - # Containers inherit TELEMETRY_PLATFORM from the host environment. - TM_PLATFORM="${TELEMETRY_PLATFORM:-}" - TM_PVE_VERSION="" - if command -v pveversion &>/dev/null; then - TM_PVE_VERSION=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true - TM_PLATFORM="${TM_PLATFORM:-pve}" - elif command -v incus &>/dev/null; then - TM_PVE_VERSION="incus-$(incus version 2>/dev/null | head -1 | awk '{print $NF}')" || true - TM_PLATFORM="${TM_PLATFORM:-incus}" - fi - - _TM_SYSINFO_DONE=true - export TM_ARCH TM_PVE_VERSION TM_PLATFORM _TM_SYSINFO_DONE -} - -# ============================================================================== -# SECTION 5: PAYLOAD BUILDER & SENDER -# ============================================================================== - -# ------------------------------------------------------------------------------ -# _tm_enabled() -# -# - Central gate: curl present, DIAGNOSTICS=yes, RANDOM_UUID set -# ------------------------------------------------------------------------------ -_tm_enabled() { - command -v curl &>/dev/null || return 1 - [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 1 - [[ -z "${RANDOM_UUID:-}" ]] && return 1 - return 0 -} - -# ------------------------------------------------------------------------------ -# _tm_payload() -# -# - Builds the COMPLETE JSON payload. Every send (installing, progress ping, -# final status) carries the full metadata so no record is ever empty, -# regardless of which event the server persists. -# - Arguments: -# * $1: status -# * $2: exit_code (optional, default 0) -# * $3: error text (optional, raw - will be JSON-escaped here) -# ------------------------------------------------------------------------------ -_tm_payload() { - local status="$1" - local exit_code="${2:-0}" - local error_raw="${3:-}" - - telemetry_collect_sysinfo - - # Numeric sanitation (server rejects out-of-range values) - [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 - ((exit_code > 255)) && exit_code=255 - - local disk_size="${DISK_SIZE:-0}" - disk_size="${disk_size%G}" - [[ ! "$disk_size" =~ ^[0-9]+$ ]] && disk_size=0 - - local duration=0 - if [[ -n "${INSTALL_START_TIME:-}" ]]; then - duration=$(($(date +%s) - INSTALL_START_TIME)) - ((duration < 0)) && duration=0 - ((duration > 86400)) && duration=86400 - fi - - local error_json="" error_category="" - if [[ -n "$error_raw" ]]; then - error_json=$(json_escape "$error_raw") - error_category=$(categorize_error "$exit_code") - fi - - local gpu_model cpu_model - gpu_model=$(json_escape "${GPU_MODEL:-}") - cpu_model=$(json_escape "${CPU_MODEL:-}") - - cat <&2 - fi - http_code=$(curl -sS -w "%{http_code}" -m "$timeout" -X POST "${TELEMETRY_URL}" \ - -H "Content-Type: application/json" \ - -d "$payload" -o /dev/null 2>/dev/null) || http_code="000" - [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] telemetry HTTP=${http_code}" >&2 - if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then - return 0 - fi - ((attempt < attempts)) && sleep 1 - done - return 1 -} - -# ============================================================================== -# SECTION 6: PUBLIC TELEMETRY API -# ============================================================================== - -# ------------------------------------------------------------------------------ -# post_to_api() -# -# - Sends the initial "installing" record for LXC container creation -# - Full metadata payload; retried up to 3x -# - Idempotent per execution (POST_TO_API_DONE) -# ------------------------------------------------------------------------------ -post_to_api() { - [[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0 - _tm_enabled || return 0 - - TELEMETRY_TYPE="${TELEMETRY_TYPE:-lxc}" - - local payload - payload=$(_tm_payload "installing") - - if _tm_send "$payload" "$TELEMETRY_TIMEOUT" 3; then - POST_TO_API_DONE=true - else - POST_TO_API_DONE=false - fi -} - -# ------------------------------------------------------------------------------ -# post_to_api_vm() -# -# - Sends the initial "installing" record for VM creation -# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics -# ------------------------------------------------------------------------------ -post_to_api_vm() { - [[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0 - - 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 - - _tm_enabled || return 0 - - TELEMETRY_TYPE="vm" - CT_TYPE=2 - - local payload - payload=$(_tm_payload "installing") - - if _tm_send "$payload" "$TELEMETRY_TIMEOUT" 3; then - POST_TO_API_DONE=true - else - POST_TO_API_DONE=false - fi -} - -# ------------------------------------------------------------------------------ -# post_progress_to_api() -# -# - Progress ping ("validation" / "configuring") -# - Sends the FULL payload (not a minimal one) so that even if the server -# ever has to fall back to a progress row, it carries all metadata -# - Fire-and-forget, single attempt -# ------------------------------------------------------------------------------ -post_progress_to_api() { - _tm_enabled || return 0 - - local progress_status="${1:-configuring}" - local payload - payload=$(_tm_payload "$progress_status") - _tm_send "$payload" "$TELEMETRY_TIMEOUT" 1 || true -} - -# ------------------------------------------------------------------------------ -# post_update_to_api() -# -# - Reports the FINAL installation status. This is the single most important -# telemetry event - it must carry full metadata AND the focused error trace. -# - Arguments: -# * $1: status ("done"/"success" | "failed" | "aborted") -# * $2: exit_code (numeric; "none" → 0) -# * $3: "force" to bypass the duplicate guard (new information available) -# - Signal exit codes (129/130/143) are reported as "aborted", not "failed" -# - Idempotent via POST_UPDATE_DONE -# ------------------------------------------------------------------------------ -post_update_to_api() { - command -v curl &>/dev/null || return 0 - - local status="${1:-failed}" - local raw_exit_code="${2:-1}" - 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 exit_code=0 - if [[ "$raw_exit_code" =~ ^[0-9]+$ ]]; then - exit_code="$raw_exit_code" - elif [[ "$raw_exit_code" == "none" ]]; then - exit_code=0 - else - exit_code=1 - fi - - local pb_status error_raw="" - case "$status" in - done | success) - pb_status="success" - exit_code=0 - ;; - aborted) - pb_status="aborted" - ;; - failed) - # Signal-based exits are user aborts, not installation failures - case "$exit_code" in - 129 | 130 | 143) pb_status="aborted" ;; - *) pb_status="failed" ;; - esac - ;; - *) - pb_status="unknown" - ;; - esac - - if [[ "$pb_status" == "failed" || "$pb_status" == "unknown" ]]; then - error_raw=$(build_error_string "$exit_code") - elif [[ "$pb_status" == "aborted" ]]; then - # Short context line only - no log dump for user aborts - error_raw="exit_code=${exit_code} | $(explain_exit_code "$exit_code")" - ERROR_CATEGORY_OVERRIDE="${ERROR_CATEGORY_OVERRIDE:-user_aborted}" - fi - - local payload - payload=$(_tm_payload "$pb_status" "$exit_code" "$error_raw") - - if _tm_send "$payload" "$STATUS_TIMEOUT" 2; then - POST_UPDATE_DONE=true - return 0 - fi - - # Retry with a smaller error trace (in case the payload was the problem) - if [[ -n "$error_raw" ]]; then - local short_error - short_error="exit_code=${exit_code} | $(explain_exit_code "$exit_code")" - [[ -n "${FAILED_COMMAND:-}" ]] && short_error+=" | at line ${FAILED_LINE:-?}: $(printf '%s' "$FAILED_COMMAND" | head -c 200)" - payload=$(_tm_payload "$pb_status" "$exit_code" "$short_error") - if _tm_send "$payload" "$STATUS_TIMEOUT" 2; then - POST_UPDATE_DONE=true - return 0 - fi - fi - - # All attempts failed - leave POST_UPDATE_DONE=false so the EXIT trap can retry - return 0 -} - -# ------------------------------------------------------------------------------ -# telemetry_new_attempt() -# -# - Resets telemetry state for a RETRY of the same session (recovery menu: -# rebuild, OOM retry, DNS retry, APT repair). Each attempt becomes its own -# execution on the server (the previous one keeps its terminal status). -# ------------------------------------------------------------------------------ -telemetry_new_attempt() { - EXECUTION_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "${RANDOM_UUID}-r$RANDOM")" - POST_TO_API_DONE=false - POST_UPDATE_DONE=false - unset FAILED_COMMAND FAILED_LINE ERROR_CATEGORY_OVERRIDE TELEMETRY_ERRINFO - export EXECUTION_ID -} - -# ============================================================================== -# SECTION 7: TIMERS -# ============================================================================== - -start_install_timer() { - INSTALL_START_TIME=$(date +%s) - export INSTALL_START_TIME -} - -get_install_duration() { - if [[ -z "${INSTALL_START_TIME:-}" ]]; then - echo "0" - return - fi - local now - now=$(date +%s) - echo $((now - INSTALL_START_TIME)) -} - -# ============================================================================== -# SECTION 8: TOOLS & ADDON TELEMETRY -# ============================================================================== - -# ------------------------------------------------------------------------------ -# _telemetry_report_exit() - EXIT trap handler set by init_tool_telemetry() -# ------------------------------------------------------------------------------ -_telemetry_report_exit() { - local ec="${1:-0}" - local status="success" - [[ "$ec" -ne 0 ]] && status="failed" - - 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 -# - Arguments: -# * $1: tool_name (optional, falls back to $APP at exit time) -# * $2: type ("pve" for PVE host scripts, "addon" for container addons) -# ------------------------------------------------------------------------------ -init_tool_telemetry() { - local name="${1:-}" - local type="${2:-pve}" - - [[ -n "$name" ]] && TELEMETRY_TOOL_NAME="$name" - TELEMETRY_TOOL_TYPE="$type" - - 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 - - trap '_telemetry_report_exit "$?"' EXIT -} - -# ------------------------------------------------------------------------------ -# post_tool_to_api() - PVE host tool usage report -# ------------------------------------------------------------------------------ -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 - - uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "tool-$(date +%s)") - duration=$(get_install_duration) - - [[ "$status" == "done" ]] && status="success" - - if [[ "$status" == "failed" ]]; then - [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 - error=$(json_escape "$(build_error_string "$exit_code")") - error_category=$(categorize_error "$exit_code") - fi - - telemetry_collect_sysinfo - - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat </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 - - uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "addon-$(date +%s)") - duration=$(get_install_duration) - - [[ "$status" == "done" ]] && status="success" - - if [[ "$status" == "failed" ]]; then - [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 - error=$(json_escape "$(build_error_string "$exit_code")") - error_category=$(categorize_error "$exit_code") - fi - - local os_type="" os_version="" - if [[ -f /etc/os-release ]]; then - os_type=$(grep "^ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true) - os_version=$(grep "^VERSION_ID=" /etc/os-release | cut -d= -f2 | tr -d '"' || true) - fi - - local JSON_PAYLOAD - JSON_PAYLOAD=$( - cat <