Add CPU/RAM and GPU model telemetry

Extend telemetry collection to include GPU model, CPU vendor/model, and RAM speed. misc/api.func: enhance detect_gpu to capture GPU_MODEL and default unknown values; add detect_cpu and detect_ram (dmidecode used for RAM speed), export new globals, and include cpu_*/gpu_model/ram_speed in post_to_api and post_update_to_api payloads. misc/data/service.go: add GPUModel/CPUVendor/CPUModel/RAMSpeed fields to TelemetryIn/Out/StatusUpdate, update PBClient mapping, expand allowed enum values to accept "unknown", sanitize and default empty vendor/passthrough fields to "unknown", and validate new cpu_vendor values. Changes maintain backward compatibility by using "unknown" where data is unavailable.
This commit is contained in:
CanbiZ (MickLesk) 2026-02-10 16:38:03 +01:00
parent 1dcd83abea
commit 18fa3ec4e9
2 changed files with 165 additions and 31 deletions

View File

@ -171,38 +171,97 @@ explain_exit_code() {
# ------------------------------------------------------------------------------
# detect_gpu()
#
# - Detects GPU vendor and passthrough type
# - Sets GPU_VENDOR and GPU_PASSTHROUGH globals
# - Detects GPU vendor, model, and passthrough type
# - Sets GPU_VENDOR, GPU_MODEL, and GPU_PASSTHROUGH globals
# - Used for GPU analytics
# ------------------------------------------------------------------------------
detect_gpu() {
GPU_VENDOR=""
GPU_PASSTHROUGH="none"
GPU_VENDOR="unknown"
GPU_MODEL=""
GPU_PASSTHROUGH="unknown"
# Detect Intel GPU
if lspci 2>/dev/null | grep -qi "VGA.*Intel"; then
GPU_VENDOR="intel"
GPU_PASSTHROUGH="igpu"
fi
local gpu_line
gpu_line=$(lspci 2>/dev/null | grep -iE "VGA|3D|Display" | head -1)
# Detect AMD GPU
if lspci 2>/dev/null | grep -qi "VGA.*AMD\|VGA.*ATI"; then
GPU_VENDOR="amd"
# Check if discrete
if lspci 2>/dev/null | grep -qi "AMD.*Radeon"; then
GPU_PASSTHROUGH="dgpu"
else
if [[ -n "$gpu_line" ]]; then
# Extract model: everything after the colon, clean up
GPU_MODEL=$(echo "$gpu_line" | sed 's/.*: //' | sed 's/ (rev .*)$//' | cut -c1-64)
# Detect vendor and passthrough type
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
# Detect NVIDIA GPU
if lspci 2>/dev/null | grep -qi "VGA.*NVIDIA\|3D.*NVIDIA"; then
GPU_VENDOR="nvidia"
GPU_PASSTHROUGH="dgpu"
export GPU_VENDOR GPU_MODEL GPU_PASSTHROUGH
}
# ------------------------------------------------------------------------------
# detect_cpu()
#
# - Detects CPU vendor and model
# - Sets CPU_VENDOR (intel/amd/arm/unknown) and CPU_MODEL globals
# - Used for CPU analytics
# ------------------------------------------------------------------------------
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 ' ')
case "$vendor_id" in
GenuineIntel) CPU_VENDOR="intel" ;;
AuthenticAMD) CPU_VENDOR="amd" ;;
*)
# ARM doesn't have vendor_id, check for CPU implementer
if grep -qi "CPU implementer" /proc/cpuinfo 2>/dev/null; then
CPU_VENDOR="arm"
fi
;;
esac
# Extract model name and clean it up
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)
fi
export GPU_VENDOR GPU_PASSTHROUGH
export CPU_VENDOR CPU_MODEL
}
# ------------------------------------------------------------------------------
# detect_ram()
#
# - Detects RAM speed using dmidecode
# - Sets RAM_SPEED global (e.g., "4800" for DDR5-4800)
# - Requires root access for dmidecode
# - Returns empty if not available
# ------------------------------------------------------------------------------
detect_ram() {
RAM_SPEED=""
if command -v dmidecode &>/dev/null; then
# Get configured memory speed (actual running speed)
RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1)
# Fallback to Speed: if Configured not available
if [[ -z "$RAM_SPEED" ]]; then
RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1)
fi
fi
export RAM_SPEED
}
# ------------------------------------------------------------------------------
@ -256,8 +315,22 @@ post_to_api() {
if [[ -z "${GPU_VENDOR:-}" ]]; then
detect_gpu
fi
local gpu_vendor="${GPU_VENDOR:-}"
local gpu_passthrough="${GPU_PASSTHROUGH:-none}"
local gpu_vendor="${GPU_VENDOR:-unknown}"
local gpu_model="${GPU_MODEL:-}"
local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
# Detect CPU if not already set
if [[ -z "${CPU_VENDOR:-}" ]]; then
detect_cpu
fi
local cpu_vendor="${CPU_VENDOR:-unknown}"
local cpu_model="${CPU_MODEL:-}"
# Detect RAM if not already set
if [[ -z "${RAM_SPEED:-}" ]]; then
detect_ram
fi
local ram_speed="${RAM_SPEED:-}"
local JSON_PAYLOAD
JSON_PAYLOAD=$(
@ -275,8 +348,12 @@ post_to_api() {
"os_version": "${var_version:-}",
"pve_version": "${pve_version}",
"method": "${METHOD:-default}",
"cpu_vendor": "${cpu_vendor}",
"cpu_model": "${cpu_model}",
"gpu_vendor": "${gpu_vendor}",
"gpu_passthrough": "${gpu_passthrough}"
"gpu_model": "${gpu_model}",
"gpu_passthrough": "${gpu_passthrough}",
"ram_speed": "${ram_speed}"
}
EOF
)
@ -384,8 +461,16 @@ post_update_to_api() {
local exit_code=0 error="" pb_status error_category=""
# Get GPU info (if detected)
local gpu_vendor="${GPU_VENDOR:-}"
local gpu_passthrough="${GPU_PASSTHROUGH:-}"
local gpu_vendor="${GPU_VENDOR:-unknown}"
local gpu_model="${GPU_MODEL:-}"
local gpu_passthrough="${GPU_PASSTHROUGH:-unknown}"
# Get CPU info (if detected)
local cpu_vendor="${CPU_VENDOR:-unknown}"
local cpu_model="${CPU_MODEL:-}"
# Get RAM info (if detected)
local ram_speed="${RAM_SPEED:-}"
# Map status to telemetry values: installing, success, failed, unknown
case "$status" in
@ -449,8 +534,12 @@ post_update_to_api() {
"error": "${error}",
"error_category": "${error_category}",
"install_duration": ${duration},
"cpu_vendor": "${cpu_vendor}",
"cpu_model": "${cpu_model}",
"gpu_vendor": "${gpu_vendor}",
"gpu_passthrough": "${gpu_passthrough}"
"gpu_model": "${gpu_model}",
"gpu_passthrough": "${gpu_passthrough}",
"ram_speed": "${ram_speed}"
}
EOF
)

View File

@ -86,8 +86,16 @@ type TelemetryIn struct {
// GPU Passthrough stats
GPUVendor string `json:"gpu_vendor,omitempty"` // "intel", "amd", "nvidia"
GPUModel string `json:"gpu_model,omitempty"` // e.g., "Intel Arc Graphics"
GPUPassthrough string `json:"gpu_passthrough,omitempty"` // "igpu", "dgpu", "vgpu", "none"
// CPU stats
CPUVendor string `json:"cpu_vendor,omitempty"` // "intel", "amd", "arm"
CPUModel string `json:"cpu_model,omitempty"` // e.g., "Intel Core Ultra 7 155H"
// RAM stats
RAMSpeed string `json:"ram_speed,omitempty"` // e.g., "4800" (MT/s)
// Performance metrics
InstallDuration int `json:"install_duration,omitempty"` // Seconds
@ -114,7 +122,11 @@ type TelemetryOut struct {
// Extended fields
GPUVendor string `json:"gpu_vendor,omitempty"`
GPUModel string `json:"gpu_model,omitempty"`
GPUPassthrough string `json:"gpu_passthrough,omitempty"`
CPUVendor string `json:"cpu_vendor,omitempty"`
CPUModel string `json:"cpu_model,omitempty"`
RAMSpeed string `json:"ram_speed,omitempty"`
InstallDuration int `json:"install_duration,omitempty"`
ErrorCategory string `json:"error_category,omitempty"`
}
@ -127,7 +139,11 @@ type TelemetryStatusUpdate struct {
InstallDuration int `json:"install_duration,omitempty"`
ErrorCategory string `json:"error_category,omitempty"`
GPUVendor string `json:"gpu_vendor,omitempty"`
GPUModel string `json:"gpu_model,omitempty"`
GPUPassthrough string `json:"gpu_passthrough,omitempty"`
CPUVendor string `json:"cpu_vendor,omitempty"`
CPUModel string `json:"cpu_model,omitempty"`
RAMSpeed string `json:"ram_speed,omitempty"`
}
type PBClient struct {
@ -380,7 +396,11 @@ func (p *PBClient) UpsertTelemetry(ctx context.Context, payload TelemetryOut) er
InstallDuration: payload.InstallDuration,
ErrorCategory: payload.ErrorCategory,
GPUVendor: payload.GPUVendor,
GPUModel: payload.GPUModel,
GPUPassthrough: payload.GPUPassthrough,
CPUVendor: payload.CPUVendor,
CPUModel: payload.CPUModel,
RAMSpeed: payload.RAMSpeed,
}
return p.UpdateTelemetryStatus(ctx, recordID, update)
}
@ -548,10 +568,13 @@ var (
}
// Allowed values for 'gpu_vendor' field
allowedGPUVendor = map[string]bool{"intel": true, "amd": true, "nvidia": true, "": true}
allowedGPUVendor = map[string]bool{"intel": true, "amd": true, "nvidia": true, "unknown": true, "": true}
// Allowed values for 'gpu_passthrough' field
allowedGPUPassthrough = map[string]bool{"igpu": true, "dgpu": true, "vgpu": true, "none": true, "": true}
allowedGPUPassthrough = map[string]bool{"igpu": true, "dgpu": true, "vgpu": true, "none": true, "unknown": true, "": true}
// Allowed values for 'cpu_vendor' field
allowedCPUVendor = map[string]bool{"intel": true, "amd": true, "arm": true, "apple": true, "qualcomm": true, "unknown": true, "": true}
// Allowed values for 'error_category' field
allowedErrorCategory = map[string]bool{
@ -587,9 +610,24 @@ func validate(in *TelemetryIn) error {
// Sanitize extended fields
in.GPUVendor = strings.ToLower(sanitizeShort(in.GPUVendor, 16))
in.GPUModel = sanitizeShort(in.GPUModel, 64)
in.GPUPassthrough = strings.ToLower(sanitizeShort(in.GPUPassthrough, 16))
in.CPUVendor = strings.ToLower(sanitizeShort(in.CPUVendor, 16))
in.CPUModel = sanitizeShort(in.CPUModel, 64)
in.RAMSpeed = sanitizeShort(in.RAMSpeed, 16)
in.ErrorCategory = strings.ToLower(sanitizeShort(in.ErrorCategory, 32))
// Default empty values to "unknown" for consistency
if in.GPUVendor == "" {
in.GPUVendor = "unknown"
}
if in.GPUPassthrough == "" {
in.GPUPassthrough = "unknown"
}
if in.CPUVendor == "" {
in.CPUVendor = "unknown"
}
// IMPORTANT: "error" must be short and not contain identifiers/logs
in.Error = sanitizeShort(in.Error, 120)
@ -613,10 +651,13 @@ func validate(in *TelemetryIn) error {
// Validate new enum fields
if !allowedGPUVendor[in.GPUVendor] {
return errors.New("invalid gpu_vendor (must be 'intel', 'amd', 'nvidia', or empty)")
return errors.New("invalid gpu_vendor (must be 'intel', 'amd', 'nvidia', 'unknown')")
}
if !allowedGPUPassthrough[in.GPUPassthrough] {
return errors.New("invalid gpu_passthrough (must be 'igpu', 'dgpu', 'vgpu', 'none', or empty)")
return errors.New("invalid gpu_passthrough (must be 'igpu', 'dgpu', 'vgpu', 'none', 'unknown')")
}
if !allowedCPUVendor[in.CPUVendor] {
return errors.New("invalid cpu_vendor (must be 'intel', 'amd', 'arm', 'apple', 'qualcomm', 'unknown')")
}
if !allowedErrorCategory[in.ErrorCategory] {
return errors.New("invalid error_category")
@ -993,7 +1034,11 @@ func main() {
Error: in.Error,
ExitCode: in.ExitCode,
GPUVendor: in.GPUVendor,
GPUModel: in.GPUModel,
GPUPassthrough: in.GPUPassthrough,
CPUVendor: in.CPUVendor,
CPUModel: in.CPUModel,
RAMSpeed: in.RAMSpeed,
InstallDuration: in.InstallDuration,
ErrorCategory: in.ErrorCategory,
}