From bf7f0d44ba775b576de4f07abecdf3c2252ac6c7 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 21 Mar 2026 08:58:46 +1300 Subject: [PATCH] Add GPU PCI ID fallback names and container passthrough indicators --- app/__pycache__/app.cpython-312.pyc | Bin 38400 -> 41056 bytes app/app.py | 67 ++++++++++++++++++++++++++++ app/gather_info.sh | 42 ++++++++++++++++- app/static/style.css | 6 +++ app/templates/index.html | 11 +++-- 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/app/__pycache__/app.cpython-312.pyc b/app/__pycache__/app.cpython-312.pyc index fcb753119fbb136cae9f5a65bf62cbd91a78450d..bdb98882acc8301166a26514f1d579ab7dcf9baf 100644 GIT binary patch delta 3192 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 delta 1035 zcmZ`&OH30{6n$^z%}l?xr7f*cQH+UbQxKyI@e2hiB52eV3I_WIu_B)t)Wp0=Fd>Ta z@$+1$Xpk6<+Q!68G;v|#$_-&5EYzy_i)%k@UHD!(cka38-t+Fenfdr!7)S|a zQ%^rbkgb-LE=31toFiDj_4BE=BY_VlR{P&0<|b{ zfu431U2a224J9t|ScZTJMCmDc38b{r?)BEHix*lLhZ0w~oVOg;r0ViaOQYJDxRw)O zy5qc6smC!^w4IhHE8z~aUfC!^n|agu>o|1{mI^Zhtr{z#y9Zau7!gPWI$7uorlcgw z4ro9u*MipN02m^Hw9oAe5i!359gg@3?6>Du1c*py&V&_ZL8;&$C1kK8Xk~F|pCMrj zkxl?j#8v}!hp>$!-Gc3zH^Pn_>tw7GFUlboLo#+}kbpe~!UehEf?LS+g zLm8z)i5EAe-fu$ZTr_mHnc}Ns>)l# z2lTL3%(AOsTI%R(KdhTo=lWnwtK~VHHq5&wm`jWL@k41_L#%n%p1NrD-a4(BQ@c4O zb@iN7wY03O?OME3Yv4o^bE~X$>2Q$l8(tGt^n>wEHGWjLvi!2VX`6ZJ82c46?kgKl zzsAr2_+&49?J9U@u@5!QT7m=BZ;{j7IZ_YlhtVkgLZ8^~@Fwm`%-dJyM8^Xbrcu17 zyIV`q(W^1(F-ugKZ?3L|u$elpr(lvky*_8p9F}Q*OrMM&vrn@!_5l5vXqMl2FO-IW tAiaXVm(X`{|12#1A*_eKc@YT9zw_vOFQj7=FC0_F!6rqVR-`7k@*DDh_LBer diff --git a/app/app.py b/app/app.py index fc331af..015b775 100644 --- a/app/app.py +++ b/app/app.py @@ -738,6 +738,19 @@ def temp_color(temp_c): return '#22c55e' +# Fallback lookup for GPUs with vague lspci names (outdated pciid database) +_GPU_PCI_FALLBACK = { + # Intel Arc Battlemage + 'e20b': 'Intel Arc B580', + 'e20c': 'Intel Arc B570', + # Intel Arc Alchemist + '56a0': 'Intel Arc A770', + '56a1': 'Intel Arc A770', + '56a5': 'Intel Arc A580', + '56a6': 'Intel Arc A380', +} + + @app.template_filter('clean_gpu') def clean_gpu(description): if not description: @@ -745,6 +758,14 @@ def clean_gpu(description): s = str(description) import re + # Extract PCI device ID from lspci -nn output (e.g. [8086:e20b]) + pci_id_match = re.search(r'\[([0-9a-f]{4}):([0-9a-f]{4})\]', s, flags=re.IGNORECASE) + pci_device_id = pci_id_match.group(2).lower() if pci_id_match else None + + # Remove PCI class and device ID brackets + s = re.sub(r'\s*\[[0-9a-f]{4}:[0-9a-f]{4}\]', '', s, flags=re.IGNORECASE) + s = re.sub(r'\s*\[[0-9a-f]{4}\]', '', s, flags=re.IGNORECASE) + # Strip PCI address prefix (e.g. "01:00.0 ") s = re.sub(r'^[0-9a-f:.]+\s+', '', s, flags=re.IGNORECASE) # Strip type prefix @@ -786,6 +807,14 @@ def clean_gpu(description): # Remove trailing whitespace s = s.strip() + # If the name is too vague, try PCI device ID fallback + if pci_device_id: + vague = not s or s.lower() in ('graphics',) or s.lower().startswith('device ') + if vague: + fallback = _GPU_PCI_FALLBACK.get(pci_device_id) + if fallback: + return fallback + # Don't duplicate manufacturer if already in the model name if manufacturer and s: s_check = s.lower() @@ -795,6 +824,44 @@ def clean_gpu(description): return s or '-' +def _normalize_pci(addr): + """Normalize PCI address to bus:device for matching (strip domain and function).""" + if not addr: + return '' + addr = str(addr).strip() + # Strip domain prefix (0000:) + if addr.count(':') == 2: + addr = addr.split(':', 1)[1] + # Strip function suffix (.0) + return addr.split('.')[0].lower() + + +@app.template_filter('gpu_passthrough_map') +def gpu_passthrough_map(details): + """Build mapping of normalized PCI address -> container name for GPU passthrough.""" + if not details: + return {} + mapping = {} + containers = details.get('container', []) + if not containers: + return {} + for c in containers: + pt = c.get('gpu_passthrough', '') + if not pt: + continue + name = c.get('name', c.get('_name', '?')) + for pci_addr in pt.split(','): + normalized = _normalize_pci(pci_addr) + if normalized: + mapping.setdefault(normalized, []).append(name) + return mapping + + +@app.template_filter('normalize_pci') +def normalize_pci_filter(addr): + return _normalize_pci(addr) + + @app.template_filter('usage_color') def usage_color(percent): try: diff --git a/app/gather_info.sh b/app/gather_info.sh index 337ef4a..a164f2a 100755 --- a/app/gather_info.sh +++ b/app/gather_info.sh @@ -135,13 +135,15 @@ df -B1 --output=target,size,used,avail,pcent \ echo "usage_percent=${percent%\%}" done -# GPUs +# GPUs (use -nn for numeric PCI IDs as fallback for outdated pciid databases) gpu_idx=0 while read -r line; do echo "[gpu:$gpu_idx]" echo "description=$line" + pci_addr=$(echo "$line" | awk '{print $1}') + echo "pci_address=$pci_addr" gpu_idx=$((gpu_idx + 1)) -done < <(lspci 2>/dev/null | grep -iE 'vga|3d|display') +done < <(lspci -nn 2>/dev/null | grep -iE 'vga|3d|display') # GPU utilization (NVIDIA) if command -v nvidia-smi &>/dev/null; then @@ -171,6 +173,10 @@ if [ -n "$igpu_cmd" ]; then igpu_raw=$(timeout 2 $igpu_prefix intel_gpu_top -J -s 500 -d /dev/dri/"$card" 2>/dev/null) if [ -n "$igpu_raw" ]; then echo "[intel_gpu:$igpu_idx]" + pci_addr=$(basename "$(readlink "$drm/device" 2>/dev/null)" 2>/dev/null) + [ -n "$pci_addr" ] && echo "pci_address=$pci_addr" + gpu_desc=$(lspci -nn -s "$pci_addr" 2>/dev/null) + [ -n "$gpu_desc" ] && echo "name=$gpu_desc" busy=$(echo "$igpu_raw" | grep -oP '"busy"\s*:\s*\K[0-9.]+' | sort -rn | head -1) echo "utilization_percent=${busy:-0}" freq=$(echo "$igpu_raw" | grep -oP '"actual"\s*:\s*\K[0-9.]+' | head -1) @@ -292,6 +298,31 @@ if command -v pct &>/dev/null; then if [ "$status" = "running" ]; then gather_container_stats "_sudo pct exec $vmid --" fi + # Check for GPU/device passthrough in LXC config + if [ -f "/etc/pve/lxc/${vmid}.conf" ]; then + pt_addrs="" + while read -r dev_line; do + dev_path=$(echo "$dev_line" | sed 's/dev[0-9]*: *//' | cut -d, -f1) + case "$dev_path" in + /dev/dri/card*) + card_name=$(basename "$dev_path") + pci=$(basename "$(readlink "/sys/class/drm/$card_name/device" 2>/dev/null)" 2>/dev/null) + if [ -n "$pci" ] && ! echo "$pt_addrs" | grep -q "$pci"; then + pt_addrs="${pt_addrs:+$pt_addrs,}$pci" + fi + ;; + /dev/dri/renderD*) + render_minor=$(basename "$dev_path" | grep -oE '[0-9]+') + card_num=$((render_minor - 128)) + pci=$(basename "$(readlink "/sys/class/drm/card${card_num}/device" 2>/dev/null)" 2>/dev/null) + if [ -n "$pci" ] && ! echo "$pt_addrs" | grep -q "$pci"; then + pt_addrs="${pt_addrs:+$pt_addrs,}$pci" + fi + ;; + esac + done < <(grep -E '^dev[0-9]+:' "/etc/pve/lxc/${vmid}.conf" 2>/dev/null) + [ -n "$pt_addrs" ] && echo "gpu_passthrough=$pt_addrs" + fi done fi @@ -313,6 +344,13 @@ if command -v qm &>/dev/null; then gather_container_stats "_sudo qm guest exec $vmid --" fi fi + # Get PCI passthrough devices from VM config + pt_addrs="" + while read -r pci_line; do + pci_dev=$(echo "$pci_line" | sed 's/hostpci[0-9]*: *//' | cut -d, -f1) + [ -n "$pci_dev" ] && pt_addrs="${pt_addrs:+$pt_addrs,}$pci_dev" + done < <(_sudo qm config "$vmid" 2>/dev/null | grep -E '^hostpci[0-9]+:') + [ -n "$pt_addrs" ] && echo "gpu_passthrough=$pt_addrs" done fi diff --git a/app/static/style.css b/app/static/style.css index 5cfa83c..02ee58c 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -356,6 +356,12 @@ main { word-break: break-all; } +.gpu-passthrough { + color: #38bdf8; + font-size: 0.85em; + opacity: 0.8; +} + .table-header td { font-weight: 600; color: #94a3b8 !important; diff --git a/app/templates/index.html b/app/templates/index.html index 1ef38fa..cee922c 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -329,20 +329,24 @@ {% set gpus = d.get('gpu', []) if d.get('gpu') else [] %} {% if gpus or nvidia_gpus or intel_gpus %} + {% set gpu_pt = d|gpu_passthrough_map %}

GPUs

{% for ng in nvidia_gpus %} + {% set pt_users = gpu_pt.get(ng.get('pci_address', '')|normalize_pci) %} - + {% endfor %} {% for ig in intel_gpus %} + {% set ig_name = ig.get('name', '')|clean_gpu if ig.get('name') else 'Intel GPU ' ~ loop.index0 %} + {% set pt_users = gpu_pt.get(ig.get('pci_address', '')|normalize_pci) %} - + @@ -350,7 +354,8 @@ {% endfor %} {% if not nvidia_gpus and not intel_gpus %} {% for gpu in gpus %} - + {% set pt_users = gpu_pt.get(gpu.get('pci_address', '')|normalize_pci) %} + {% endfor %} {% endif %}
{{ ng.get('name', 'NVIDIA GPU ' ~ loop.index0) }}{{ ng.get('name', 'NVIDIA GPU ' ~ loop.index0) }}{% if pt_users %} → {{ pt_users|join(', ') }}{% endif %} {{ ng.get('utilization_percent', '-') }}% {{ ng.get('memory_used_mb', '-') }} / {{ ng.get('memory_total_mb', '-') }} MB {{ ng.get('temperature', '-') }}°C
Intel GPU {{ loop.index0 }}{{ ig_name }}{% if pt_users %} → {{ pt_users|join(', ') }}{% endif %} {{ ig.get('utilization_percent', '-') }}% {% if ig.get('frequency_mhz') %}{{ ig.get('frequency_mhz') }} MHz{% else %}-{% endif %} {% if ig.get('power_w') %}{{ ig.get('power_w') }} W{% else %}-{% endif %}
{{ gpu.get('description', '-')|clean_gpu }}
{{ gpu.get('description', '-')|clean_gpu }}{% if pt_users %} → {{ pt_users|join(', ') }}{% endif %}