diff --git a/app/__pycache__/app.cpython-312.pyc b/app/__pycache__/app.cpython-312.pyc index fcb7531..bdb9888 100644 Binary files a/app/__pycache__/app.cpython-312.pyc and b/app/__pycache__/app.cpython-312.pyc differ 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 %}
| {{ 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 %} | @@ -350,7 +354,8 @@ {% endfor %} {% if not nvidia_gpus and not intel_gpus %} {% for gpu in gpus %} -
| {{ gpu.get('description', '-')|clean_gpu }} | ||||
| {{ gpu.get('description', '-')|clean_gpu }}{% if pt_users %} → {{ pt_users|join(', ') }}{% endif %} | ||||