Repository navigation
Expand file tree
/
Copy pathtelemetry.func
More file actions
413 lines (353 loc) · 13.9 KB
/
Copy pathtelemetry.func
File metadata and controls
413 lines (353 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# License: MIT | https://raw.githubusercontent.com/community-scripts/core/main/LICENSE
# ==============================================================================
# API/TELEMETRY TRANSPORT
# ==============================================================================
# Payload and delivery. Loaded through api/api.func. Every send is best-effort:
# telemetry must never be the reason a run fails.
# ==============================================================================
[[ -n "${_API_TELEMETRY_LOADED:-}" ]] && return 0
_API_TELEMETRY_LOADED=1
# curl present, opted in, session id set
_tm_enabled() {
command -v curl &>/dev/null || return 1
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 1
[[ -z "${RANDOM_UUID:-}" ]] && return 1
return 0
}
# ------------------------------------------------------------------------------
# _tm_payload <status> [exit_code] [error_trace]
#
# The full record. Every send carries all metadata, whichever event the server
# keeps. The error_* fields come from TM_ERR_* (_tm_error_context) and are only
# sent with a trace.
# ------------------------------------------------------------------------------
_tm_payload() {
local status="$1"
local exit_code="${2:-0}"
local error_raw="${3:-}"
telemetry_collect_sysinfo
[[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=199
((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="" error_message="" error_step="" error_phase="" failed_function=""
local failed_command="" failed_line=0
if [[ -n "$error_raw" ]]; then
error_json=$(json_escape "$error_raw")
error_category=$(categorize_error "$exit_code" "${TM_ERR_COMMAND:-${FAILED_COMMAND:-}}")
error_step=$(json_escape "${TM_ERR_STEP:-}")
if [[ "$status" != "aborted" ]]; then
error_message=$(json_escape "${TM_ERR_MESSAGE:-}")
error_phase="${TM_ERR_PHASE:-}"
failed_function=$(json_escape "${TM_ERR_FUNCTION:-}")
failed_command=$(json_escape "${TM_ERR_COMMAND:-${FAILED_COMMAND:-}}")
failed_line="${TM_ERR_LINE:-${FAILED_LINE:-0}}"
fi
fi
[[ ! "$failed_line" =~ ^[0-9]+$ ]] && failed_line=0
local gpu_model cpu_model
gpu_model=$(json_escape "${GPU_MODEL:-}")
cpu_model=$(json_escape "${CPU_MODEL:-}")
# Whether the devices reached the guest; the host's hardware is in gpu_vendor/gpu_model.
local gpu_passthrough="no"
[[ "${var_gpu:-no}" == "yes" || "${ENABLE_GPU:-no}" == "yes" ]] && gpu_passthrough="yes"
local app_version kernel_version
app_version=$(json_escape "${APPLICATION_VERSION:-${var_appversion:-}}")
kernel_version=$(json_escape "${KERNEL_VERSION:-}")
cat <<EOF
{
"payload_version": ${TELEMETRY_PAYLOAD_VERSION},
"random_id": "${RANDOM_UUID}",
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
"type": "${TELEMETRY_TYPE:-lxc}",
"nsapp": "${NSAPP:-${app:-unknown}}",
"status": "${status}",
"ct_type": ${CT_TYPE:-1},
"disk_size": ${disk_size},
"core_count": ${CORE_COUNT:-0},
"ram_size": ${RAM_SIZE:-0},
"os_type": "${var_os:-}",
"os_version": "${var_version:-}",
"pve_version": "${TM_PVE_VERSION:-}",
"method": "${METHOD:-default}",
"exit_code": ${exit_code},
"error": "${error_json}",
"error_category": "${error_category}",
"error_message": "${error_message}",
"error_step": "${error_step}",
"error_phase": "${error_phase}",
"install_duration": ${duration},
"cpu_vendor": "${CPU_VENDOR:-unknown}",
"cpu_model": "${cpu_model}",
"gpu_vendor": "${GPU_VENDOR:-unknown}",
"gpu_model": "${gpu_model}",
"gpu_passthrough": "${gpu_passthrough}",
"ram_speed": "${RAM_SPEED:-}",
"arch": "${TM_ARCH:-}",
"platform": "${TM_PLATFORM:-}",
"has_arm": ${HAS_ARM:-false},
"kernel_version": "${kernel_version}",
"app_version": "${app_version}",
"failed_command": "${failed_command}",
"failed_line": ${failed_line},
"failed_function": "${failed_function}",
"repo_source": "${REPO_SOURCE}",
"repo_slug": "${REPO_SLUG:-}"
}
EOF
}
# ------------------------------------------------------------------------------
# _tm_send <payload> [timeout] [attempts] - 0 on HTTP 2xx, 1 otherwise, never raises
# ------------------------------------------------------------------------------
_tm_send() {
local payload="$1"
local timeout="${2:-$TELEMETRY_TIMEOUT}"
local attempts="${3:-1}"
local http_code attempt
for ((attempt = 1; attempt <= attempts; attempt++)); do
if [[ "${DEV_MODE:-}" == "true" ]]; then
echo "[DEBUG] telemetry POST (attempt ${attempt}/${attempts}): $payload" >&2
fi
http_code=$(curl -sS -w "%{http_code}" -m "$timeout" -X POST "${TELEMETRY_URL}" \
-H "Content-Type: application/json" \
-A "community-scripts-core/${TELEMETRY_PAYLOAD_VERSION}" \
-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
}
# ==============================================================================
# PUBLIC API
# ==============================================================================
# _tm_quiet <function> [args] - a sender with errexit and the caller's ERR trap
# off. A failing command in here would otherwise re-enter the error handler,
# which reports again.
_tm_quiet() {
local - err_trap rc=0
err_trap="$(trap -p ERR)"
set +eE +o pipefail
trap - ERR
"$@" || rc=$?
[[ -n "$err_trap" ]] && eval "$err_trap"
return "$rc"
}
post_to_api() { _tm_quiet _tm_post_install "$@"; }
post_progress_to_api() { _tm_quiet _tm_post_progress "$@"; }
post_update_to_api() { _tm_quiet _tm_post_update "$@"; }
# post_to_api - the "installing" record; once per execution, retried 3x
_tm_post_install() {
[[ "${POST_TO_API_DONE:-}" == "true" ]] && return 0
_tm_enabled || return 0
TELEMETRY_TYPE="${TELEMETRY_TYPE:-lxc}"
# The clock starts at the first report, so storage, template and create count too.
[[ -z "${INSTALL_START_TIME:-}" ]] && export INSTALL_START_TIME="$(date +%s)"
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 - same for a VM; reads the opt-in itself since no container set it
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
TELEMETRY_TYPE="vm"
CT_TYPE=2
post_to_api
}
# post_progress_to_api [status] - "validation"/"configuring" ping, full payload, one attempt
_tm_post_progress() {
_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 <status> <exit_code> [force]
#
# The terminal status, exactly once per execution ("force" reports again when
# the outcome changed, e.g. a retry succeeded). status: done|success, failed,
# aborted. exit_code "none" means 0; anything else non-numeric becomes 199.
# ------------------------------------------------------------------------------
_tm_post_update() {
command -v curl &>/dev/null || return 0
local status="${1:-failed}"
local raw_exit_code="${2-}"
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=199
fi
local pb_status error_raw=""
case "$status" in
done | success)
pb_status="success"
exit_code=0
;;
aborted)
pb_status="aborted"
;;
failed)
# The engine asking a question and being told no, and signals: user decisions, not failures.
case "$exit_code" in
113 | 114 | 122 | 129 | 130 | 143 | 253 | 254) pb_status="aborted" ;;
*) pb_status="failed" ;;
esac
;;
*)
pb_status="unknown"
;;
esac
# Unset: no install was reported yet. A host run that failed in its checks
# (storage, ID, template) reports one now; an update run inside a container
# or a cancelled dialog does not. "false" means that send failed.
if [[ -z "${POST_TO_API_DONE:-}" ]]; then
if [[ "$pb_status" == "failed" && -n "${NSAPP:-}" ]] &&
! { declare -f _is_container_context >/dev/null 2>&1 && _is_container_context; }; then
post_to_api
fi
[[ -z "${POST_TO_API_DONE:-}" ]] && return 0
fi
if [[ "$pb_status" == "failed" || "$pb_status" == "unknown" ]]; then
_tm_error_context "$exit_code"
exit_code="$TM_ERR_CODE"
error_raw=$(_tm_format_error)
elif [[ "$pb_status" == "aborted" ]]; then
TM_ERR_STEP="${CS_STEP:-${CS_STEP_DONE:+after ${CS_STEP_DONE}}}"
TM_ERR_COMMAND=""
error_raw="exit_code=${exit_code} | $(explain_exit_code "$exit_code")${TM_ERR_STEP:+ | step: ${TM_ERR_STEP}}"
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
# The payload itself may be the problem: retry with the header line only.
if [[ -n "$error_raw" ]]; then
payload=$(_tm_payload "$pb_status" "$exit_code" "$(head -n1 <<<"$error_raw" | head -c 600)")
if _tm_send "$payload" "$STATUS_TIMEOUT" 2; then
POST_UPDATE_DONE=true
return 0
fi
fi
# Left false so the EXIT trap can try once more.
return 0
}
# ------------------------------------------------------------------------------
# _tm_post_run <type> <name> <status> <exit_code> - tool and add-on runs
#
# Each run is its own execution with a fresh id. Failures carry the same error
# fields as installs.
# ------------------------------------------------------------------------------
_tm_post_run() {
command -v curl &>/dev/null || return 0
[[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0
local type="$1" name="${2:-unknown}" status="${3:-success}" exit_code="${4:-0}"
local uuid error_raw="" payload
uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "${type}-$(date +%s)")
[[ "$status" == "done" ]] && status="success"
[[ "$exit_code" =~ ^[0-9]+$ ]] || exit_code=199
if [[ "$status" == "failed" ]]; then
_tm_error_context "$exit_code"
exit_code="$TM_ERR_CODE"
error_raw=$(_tm_format_error)
fi
payload=$(RANDOM_UUID="$uuid" EXECUTION_ID="$uuid" TELEMETRY_TYPE="$type" NSAPP="$name" CT_TYPE=0 \
_tm_payload "$status" "$exit_code" "$error_raw")
_tm_send "$payload" "$TELEMETRY_TIMEOUT" 1 || true
}
# post_tool_to_api <name> [status] [exit_code] - a PVE host tool ran
post_tool_to_api() {
_tm_quiet _tm_post_run "${TELEMETRY_TOOL_TYPE:-pve}" "$@"
}
# post_addon_to_api <name> [status] [exit_code] - an add-on ran inside a container
post_addon_to_api() {
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
var_os="$os_type" var_version="$os_version" _tm_quiet _tm_post_run addon "$@"
}
# telemetry_new_attempt - a retry from the recovery menu becomes its own execution
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 FAILED_FUNCTION ERROR_CATEGORY_OVERRIDE TELEMETRY_ERRINFO CS_ERROR_MSG
unset TM_ERR_CODE TM_ERR_COMMAND TM_ERR_LINE TM_ERR_FUNCTION TM_ERR_STEP TM_ERR_MESSAGE TM_ERR_PHASE TM_ERR_OUTPUT TM_ERR_EXPLANATION
export EXECUTION_ID
}
# ==============================================================================
# TOOLS & ADD-ONS
# ==============================================================================
# EXIT trap 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 [name] [pve|addon] - one-line setup for tool and add-on scripts
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
}
# ==============================================================================
# TIMERS
# ==============================================================================
# Keeps a clock that is already running; post_to_api starts it at the first report.
start_install_timer() {
[[ -n "${INSTALL_START_TIME:-}" ]] && return 0
INSTALL_START_TIME=$(date +%s)
export INSTALL_START_TIME
}
get_install_duration() {
if [[ -z "${INSTALL_START_TIME:-}" ]]; then
echo "0"
return 0
fi
local now
now=$(date +%s)
echo $((now - INSTALL_START_TIME))
}