From 18fa3ec4e9bdfb819c0e0cd0c2db506e14416610 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:38:03 +0100 Subject: [PATCH] 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. --- misc/api.func | 143 +++++++++++++++++++++++++++++++++++-------- misc/data/service.go | 53 ++++++++++++++-- 2 files changed, 165 insertions(+), 31 deletions(-) diff --git a/misc/api.func b/misc/api.func index 12b63c5ad..6fb67cd12 100644 --- a/misc/api.func +++ b/misc/api.func @@ -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 ) diff --git a/misc/data/service.go b/misc/data/service.go index b34c91e49..b920a9178 100644 --- a/misc/data/service.go +++ b/misc/data/service.go @@ -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, }