From 75c6f74fc404f1a240134d54e8610973184f58db Mon Sep 17 00:00:00 2001 From: MacRimi Date: Fri, 17 Apr 2026 19:53:17 +0200 Subject: [PATCH] update nstall_coral_pve9.sh --- AppImage/components/hardware.tsx | 361 +++++++++++++++++++- AppImage/scripts/flask_server.py | 460 +++++++++++++++++++++++++- AppImage/types/hardware.ts | 38 +++ scripts/gpu_tpu/configure_igpu_lxc.sh | 204 ------------ scripts/gpu_tpu/install_coral_pve9.sh | 179 ++++++++-- scripts/gpu_tpu/nvidia_installer.sh | 1 + 6 files changed, 1003 insertions(+), 240 deletions(-) delete mode 100644 scripts/gpu_tpu/configure_igpu_lxc.sh diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx index 23202032..ab03297f 100644 --- a/AppImage/components/hardware.tsx +++ b/AppImage/components/hardware.tsx @@ -4,7 +4,7 @@ import { Card } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Cpu, HardDrive, Thermometer, Zap, Loader2, CpuIcon, Cpu as Gpu, Network, MemoryStick, PowerIcon, FanIcon, Battery } from "lucide-react" +import { Cpu, HardDrive, Thermometer, Zap, Loader2, CpuIcon, Cpu as Gpu, Network, MemoryStick, PowerIcon, FanIcon, Battery, Usb, BrainCircuit, AlertCircle } from "lucide-react" import { Download } from "lucide-react" import { Button } from "@/components/ui/button" import useSWR from "swr" @@ -14,6 +14,8 @@ import { type GPU, type PCIDevice, type StorageDevice, + type CoralTPU, + type UsbDevice, fetcher as swrFetcher, } from "../types/hardware" import { fetchApi } from "@/lib/api-config" @@ -189,6 +191,9 @@ export default function Hardware() { }) // Merge: static fields from initial load, live fields from the 5s poll. + // coral_tpus and usb_devices live in the dynamic payload so that the + // "Install Drivers" button disappears immediately after install_coral_pve9.sh + // finishes, without requiring a page reload. const hardwareData = staticHardwareData ? { ...staticHardwareData, @@ -197,6 +202,8 @@ export default function Hardware() { power_meter: dynamicHardwareData?.power_meter ?? staticHardwareData.power_meter, power_supplies: dynamicHardwareData?.power_supplies ?? staticHardwareData.power_supplies, ups: dynamicHardwareData?.ups ?? staticHardwareData.ups, + coral_tpus: dynamicHardwareData?.coral_tpus ?? staticHardwareData.coral_tpus, + usb_devices: dynamicHardwareData?.usb_devices ?? staticHardwareData.usb_devices, } : undefined @@ -230,6 +237,9 @@ export default function Hardware() { const [installingNvidiaDriver, setInstallingNvidiaDriver] = useState(false) const [showAmdInstaller, setShowAmdInstaller] = useState(false) const [showIntelInstaller, setShowIntelInstaller] = useState(false) + const [showCoralInstaller, setShowCoralInstaller] = useState(false) + const [selectedCoral, setSelectedCoral] = useState(null) + const [selectedUsbDevice, setSelectedUsbDevice] = useState(null) // GPU Switch Mode states const [editingSwitchModeGpu, setEditingSwitchModeGpu] = useState(null) // GPU slot being edited @@ -1306,6 +1316,222 @@ return ( + {/* Coral TPU / AI Accelerators — only rendered when at least one device is detected. + Unlike GPUs, Coral exposes no temperature/utilization/power counters, so the + modal shows identity + driver state + an Install CTA when drivers are missing. */} + {hardwareData?.coral_tpus && hardwareData.coral_tpus.length > 0 && ( + +
+ +

Coral TPU / AI Accelerators

+ + {hardwareData.coral_tpus.length} device{hardwareData.coral_tpus.length > 1 ? "s" : ""} + +
+ +
+ {hardwareData.coral_tpus.map((coral, index) => ( +
setSelectedCoral(coral)} + className="cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-4 transition-colors" + > +
+ + {coral.name} + + + {coral.type === "usb" ? "USB" : "PCIe"} + +
+ +
+ {coral.form_factor && ( +
+ {coral.form_factor} + {coral.interface_speed && · {coral.interface_speed}} +
+ )} +
+ {coral.type === "pcie" ? coral.slot : coral.bus_device} +
+
+ +
+ {coral.drivers_ready ? ( + <> + + Drivers ready + + ) : ( + <> + + Drivers not installed + + )} +
+
+ ))} +
+ + {/* Primary CTA at the section level when ANY of the detected Coral devices + is missing drivers — avoids a per-card button repetition. */} + {hardwareData.coral_tpus.some((c) => !c.drivers_ready) && ( +
+
+ +
+

Install Coral TPU drivers

+

+ One or more detected Coral devices need drivers. A server reboot is required after installation. +

+
+
+ +
+ )} +
+ )} + + {/* Coral TPU detail modal */} + !open && setSelectedCoral(null)}> + + + {selectedCoral?.name} + Coral TPU Device Information + + + {selectedCoral && ( +
+
+ Connection + + {selectedCoral.type === "usb" ? "USB" : "PCIe / M.2"} + +
+ + {selectedCoral.form_factor && ( +
+ Form Factor + {selectedCoral.form_factor} +
+ )} + + {selectedCoral.interface_speed && ( +
+ Link + {selectedCoral.interface_speed} +
+ )} + +
+ + {selectedCoral.type === "usb" ? "Bus:Device" : "PCI Slot"} + + + {selectedCoral.type === "usb" ? selectedCoral.bus_device : selectedCoral.slot} + +
+ +
+ Vendor / Product ID + + {selectedCoral.vendor_id}:{selectedCoral.device_id} + +
+ +
+ Vendor + {selectedCoral.vendor} +
+ + {selectedCoral.type === "pcie" && selectedCoral.kernel_driver && ( +
+ Kernel Driver + + {selectedCoral.kernel_driver} + +
+ )} + + {selectedCoral.kernel_modules && ( +
+ Kernel Modules +
+ + gasket {selectedCoral.kernel_modules.gasket ? "✓" : "✗"} + + + apex {selectedCoral.kernel_modules.apex ? "✓" : "✗"} + +
+
+ )} + + {selectedCoral.device_nodes && selectedCoral.device_nodes.length > 0 && ( +
+ Device Nodes + + {selectedCoral.device_nodes.join(", ")} + +
+ )} + + {selectedCoral.type === "usb" && ( +
+ Runtime State + + {selectedCoral.programmed ? "Programmed (runtime loaded)" : "Unprogrammed (runtime not loaded)"} + +
+ )} + +
+ Edge TPU Runtime + + {selectedCoral.edgetpu_runtime || not installed} + +
+ +
+ Note: Coral TPUs do not expose temperature, utilization or power telemetry through standard interfaces. Monitoring is limited to device presence and driver state. +
+ + {!selectedCoral.drivers_ready && ( + + )} +
+ )} +
+
+ {/* Power Consumption */} {hardwareData?.power_meter && ( @@ -2215,6 +2441,125 @@ return ( + {/* USB Devices — everything physically plugged into the host's USB ports. + Root hubs (vendor 1d6b) are already filtered out by the backend. The + section is hidden on headless servers that have nothing attached. */} + {hardwareData?.usb_devices && hardwareData.usb_devices.length > 0 && ( + +
+ +

USB Devices

+ + {hardwareData.usb_devices.length} device{hardwareData.usb_devices.length > 1 ? "s" : ""} + +
+ +
+ {hardwareData.usb_devices.map((usb, index) => ( +
setSelectedUsbDevice(usb)} + className="cursor-pointer rounded-lg border border-white/10 sm:border-border bg-white/5 sm:bg-card sm:hover:bg-white/5 p-3 transition-colors" + > +
+ + {usb.name} + + + {usb.class_label} + +
+
+ {usb.speed_label &&
{usb.speed_label}
} +
+ {usb.bus_device} · {usb.vendor_id}:{usb.product_id} +
+ {usb.driver && ( +
Driver: {usb.driver}
+ )} +
+
+ ))} +
+
+ )} + + {/* USB Device detail modal — mirrors the PCI Device modal for consistency. */} + !open && setSelectedUsbDevice(null)}> + + + {selectedUsbDevice?.name} + USB Device Information + + + {selectedUsbDevice && ( +
+
+ Class + + {selectedUsbDevice.class_label} + +
+ +
+ Bus:Device + {selectedUsbDevice.bus_device} +
+ +
+ Device Name + {selectedUsbDevice.name} +
+ + {selectedUsbDevice.vendor && ( +
+ Vendor + {selectedUsbDevice.vendor} +
+ )} + +
+ Vendor / Product ID + + {selectedUsbDevice.vendor_id}:{selectedUsbDevice.product_id} + +
+ + {selectedUsbDevice.speed_label && ( +
+ Speed + + {selectedUsbDevice.speed_label} + {selectedUsbDevice.speed_mbps > 0 && ( + ({selectedUsbDevice.speed_mbps} Mbps) + )} + +
+ )} + +
+ Class Code + 0x{selectedUsbDevice.class_code} +
+ + {selectedUsbDevice.driver && ( +
+ Driver + {selectedUsbDevice.driver} +
+ )} + + {selectedUsbDevice.serial && ( +
+ Serial + {selectedUsbDevice.serial} +
+ )} +
+ )} +
+
+ {/* NVIDIA Installation Monitor */} {/* + { + setShowCoralInstaller(false) + mutateStatic() + }} + scriptPath="/usr/local/share/proxmenux/scripts/gpu_tpu/install_coral_pve9.sh" + scriptName="install_coral_pve9" + params={{ + EXECUTION_MODE: "web", + }} + title="Coral TPU Driver Installation" + description="Installing gasket + apex kernel modules and Edge TPU runtime..." + /> {/* GPU Switch Mode Modal */} {switchModeParams && ( diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index b34e68e3..7ded4f3e 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -7,6 +7,7 @@ ProxMenux Flask Server - Integrates a web terminal powered by xterm.js """ +import glob import json import logging import math @@ -1185,6 +1186,17 @@ _ipmi_cache = { } _IPMI_CACHE_TTL = 10 # 10 seconds +# Cache for `lsusb -v` output. Parsed for the USB devices section and the Coral +# USB detector. USB plug/unplug events are rare enough that a 60s TTL is safe. +_lsusb_cache = { + 'simple': None, # output of `lsusb` (short form) + 'simple_time': 0, + 'verbose': None, # output of `lsusb -v` (detailed form with speed, driver) + 'verbose_time': 0, + 'unavailable': False, # set True if lsusb is missing +} +_LSUSB_CACHE_TTL = 60 # 60 seconds + # Cache for hardware info (lspci, dmidecode, lsblk) _hardware_cache = { 'lspci': None, @@ -1290,16 +1302,16 @@ def get_cached_lspci_k(): """Get lspci -k output with 5 minute cache.""" global _hardware_cache now = time.time() - + cache_key = 'lspci_k' if cache_key not in _hardware_cache: _hardware_cache[cache_key] = None _hardware_cache[cache_key + '_time'] = 0 - + if _hardware_cache[cache_key] is not None and \ now - _hardware_cache[cache_key + '_time'] < _HARDWARE_CACHE_TTL: return _hardware_cache[cache_key] - + try: result = subprocess.run(['lspci', '-k'], capture_output=True, text=True, timeout=10) if result.returncode == 0: @@ -1311,6 +1323,416 @@ def get_cached_lspci_k(): return _hardware_cache[cache_key] or '' +# ============================================================================ +# USB device enumeration (used by both USB section and Coral USB detector) +# ============================================================================ + +def get_cached_lsusb(): + """Get plain `lsusb` output with 60s cache. Lightweight; just IDs + names.""" + global _lsusb_cache + now = time.time() + + if _lsusb_cache['unavailable']: + return '' + + if _lsusb_cache['simple'] is not None and \ + now - _lsusb_cache['simple_time'] < _LSUSB_CACHE_TTL: + return _lsusb_cache['simple'] + + try: + result = subprocess.run(['lsusb'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + _lsusb_cache['simple'] = result.stdout + _lsusb_cache['simple_time'] = now + return result.stdout + except FileNotFoundError: + _lsusb_cache['unavailable'] = True + return '' + except Exception: + pass + return _lsusb_cache['simple'] or '' + + +def _usb_speed_label(mbps): + """Map USB sysfs speed (Mbps) to a human-readable label.""" + try: + m = int(mbps) + except (ValueError, TypeError): + return '' + # These are the standard USB generation speeds. + if m >= 20000: + return 'USB 3.2 Gen 2x2' + if m >= 10000: + return 'USB 3.1 Gen 2' + if m >= 5000: + return 'USB 3.0' + if m >= 480: + return 'USB 2.0' + if m >= 12: + return 'USB 1.1' + return 'USB 1.0' + + +_USB_CLASS_LABELS = { + '00': 'Device Specific', + '01': 'Audio', + '02': 'Communications', + '03': 'HID', + '05': 'Physical', + '06': 'Imaging', + '07': 'Printer', + '08': 'Mass Storage', + '09': 'Hub', + '0a': 'CDC Data', + '0b': 'Smart Card', + '0d': 'Content Security', + '0e': 'Video', + '0f': 'Personal Healthcare', + '10': 'Audio/Video', + '11': 'Billboard', + 'dc': 'Diagnostic', + 'e0': 'Wireless Controller', + 'ef': 'Miscellaneous', + 'fe': 'Application Specific', + 'ff': 'Vendor Specific', +} + + +def _read_sysfs(path, default=''): + """Read and trim a sysfs attribute file. Returns default on error.""" + try: + with open(path, 'r') as f: + return f.read().strip() + except Exception: + return default + + +def _interface_class_from_usb_device(dev_path): + """When bDeviceClass is 00, the device delegates classification to the + first interface. Read bInterfaceClass from /:1.0/bInterfaceClass. + """ + try: + # Sysfs names interfaces as -:1.0 etc. + base = os.path.basename(dev_path) + iface_file = os.path.join(dev_path, f'{base}:1.0', 'bInterfaceClass') + return _read_sysfs(iface_file, '') + except Exception: + return '' + + +def _get_usb_driver(dev_path): + """Best-effort bound driver name for a USB device. + + USB drivers are usually bound at the *interface* level (:1.0, :1.1...), + not the device level. We peek at the first interface. + """ + try: + base = os.path.basename(dev_path) + for ifnum in ('1.0', '1.1', '1.2'): + drv_link = os.path.join(dev_path, f'{base}:{ifnum}', 'driver') + if os.path.islink(drv_link): + return os.path.basename(os.readlink(drv_link)) + except Exception: + pass + return '' + + +def get_usb_devices(): + """Enumerate physically connected USB peripherals. + + Skips virtual root hubs (vendor 1d6b = Linux Foundation). Each entry is + a dict with fields suited to the "USB Devices" section in the hardware UI. + """ + devices = [] + usb_root = '/sys/bus/usb/devices' + if not os.path.isdir(usb_root): + return devices + + # Human-readable vendor/product names live in `lsusb` simple output. + # Build a quick lookup by "bus:dev". + name_map = {} + try: + for line in get_cached_lsusb().split('\n'): + m = re.match( + r'Bus\s+(\d+)\s+Device\s+(\d+):\s+ID\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.*)', + line, re.IGNORECASE) + if m: + bus, dev, _vid, _pid, rest = m.groups() + name_map[f'{int(bus):03d}:{int(dev):03d}'] = rest.strip() + except Exception: + pass + + try: + for entry in sorted(os.listdir(usb_root)): + dev_path = os.path.join(usb_root, entry) + # Interface nodes look like "1-0:1.0" — skip, we only want devices. + if ':' in entry: + continue + + vendor_id = _read_sysfs(os.path.join(dev_path, 'idVendor')) + product_id = _read_sysfs(os.path.join(dev_path, 'idProduct')) + if not vendor_id or not product_id: + continue + + # Skip virtual root hubs (Linux Foundation hubs). + if vendor_id.lower() == '1d6b': + continue + + busnum = _read_sysfs(os.path.join(dev_path, 'busnum'), '0') + devnum = _read_sysfs(os.path.join(dev_path, 'devnum'), '0') + bus_device = f'{int(busnum):03d}:{int(devnum):03d}' + + manufacturer = _read_sysfs(os.path.join(dev_path, 'manufacturer')) + product_name = _read_sysfs(os.path.join(dev_path, 'product')) + + # Fall back to lsusb-derived name when sysfs strings are absent. + lsusb_name = name_map.get(bus_device, '') + display_name = product_name or lsusb_name or f'USB device {vendor_id}:{product_id}' + display_vendor = manufacturer or (lsusb_name.split()[0] if lsusb_name else '') + + speed_raw = _read_sysfs(os.path.join(dev_path, 'speed')) + speed_label = _usb_speed_label(speed_raw) + + device_class = _read_sysfs(os.path.join(dev_path, 'bDeviceClass'), '00').lower() + if device_class == '00': + device_class = _interface_class_from_usb_device(dev_path).lower() + class_label = _USB_CLASS_LABELS.get(device_class, 'Unknown') + + serial = _read_sysfs(os.path.join(dev_path, 'serial')) + driver = _get_usb_driver(dev_path) + + devices.append({ + 'bus_device': bus_device, + 'vendor_id': vendor_id.lower(), + 'product_id': product_id.lower(), + 'vendor': display_vendor, + 'name': display_name, + 'class_code': device_class, + 'class_label': class_label, + 'speed_mbps': int(speed_raw) if speed_raw.isdigit() else 0, + 'speed_label': speed_label, + 'serial': serial, + 'driver': driver, + }) + except Exception: + # Never break the hardware payload just because USB enumeration fails. + pass + + return devices + + +# ============================================================================ +# Coral TPU (Edge TPU) detection — M.2 / PCIe + USB +# Google Edge TPU USB IDs: +# 1a6e:089a Global Unichip Corp. (unprogrammed — before runtime loads fw) +# 18d1:9302 Google Inc. (programmed — after runtime talks to it) +# M.2 / Mini PCIe Coral uses vendor 0x1ac1 (Global Unichip Corp.). +# ============================================================================ + +CORAL_USB_IDS = {('1a6e', '089a'), ('18d1', '9302')} +CORAL_PCI_VENDOR = '0x1ac1' + + +def _coral_kernel_modules_loaded(): + """Returns (gasket_loaded, apex_loaded) based on /proc/modules.""" + gasket = apex = False + try: + with open('/proc/modules', 'r') as f: + for line in f: + name = line.split(' ', 1)[0] + if name == 'gasket': + gasket = True + elif name == 'apex': + apex = True + except Exception: + pass + return gasket, apex + + +def _coral_device_nodes(): + """Find /dev/apex_* device nodes (PCIe Coral creates these).""" + try: + from glob import glob + return sorted(glob('/dev/apex_*')) + except Exception: + return [] + + +def _coral_edgetpu_runtime_version(): + """Return the installed libedgetpu1 package version, or empty string. + + Checks both -std and -max variants (max enables full clock; std is default). + """ + for pkg in ('libedgetpu1-std', 'libedgetpu1-max'): + try: + result = subprocess.run( + ['dpkg-query', '-W', '-f=${Version}', pkg], + capture_output=True, text=True, timeout=3) + if result.returncode == 0 and result.stdout.strip(): + return f'{pkg} {result.stdout.strip()}' + except Exception: + continue + return '' + + +def _coral_pcie_name_from_lspci(slot): + """Best-effort human name for a PCIe Coral from cached lspci output.""" + try: + # Match either full slot (0000:0c:00.0) or short form (0c:00.0). + short = slot.split(':', 1)[1] if slot.count(':') >= 2 else slot + for line in get_cached_lspci().split('\n'): + if line.startswith(short): + # Format: "0c:00.0 : " + parts = line.split(':', 2) + if len(parts) >= 3: + return parts[2].strip() + except Exception: + pass + return 'Coral Edge TPU' + + +def _coral_pci_form_factor(dev_path): + """Heuristic form factor for a Coral PCIe/M.2 device. + + Without a perfectly reliable sysfs indicator we key off the PCIe link + width: Coral M.2 modules are always x1. This keeps the label simple and + accurate for the common cases without over-promising. + """ + try: + width = _read_sysfs(os.path.join(dev_path, 'current_link_width'), '') + if width == '1': + return 'M.2 / Mini PCIe (x1)' + except Exception: + pass + return 'PCIe' + + +def _coral_pci_speed(dev_path): + try: + speed = _read_sysfs(os.path.join(dev_path, 'current_link_speed'), '') + width = _read_sysfs(os.path.join(dev_path, 'current_link_width'), '') + if speed and width: + return f'PCIe {speed} x{width}' + except Exception: + pass + return '' + + +def get_coral_info(): + """Detect Coral TPU accelerators (PCIe/M.2 + USB). + + Returns a list of dicts — empty if no Coral is present. The payload is + intentionally minimal: Coral exposes no temperature/utilization/power + counters, so the UI only needs identity + driver/runtime state. + """ + coral_devices = [] + gasket_loaded, apex_loaded = _coral_kernel_modules_loaded() + device_nodes = _coral_device_nodes() + runtime = _coral_edgetpu_runtime_version() + + # ── PCIe / M.2 Coral ──────────────────────────────────────────────── + try: + for dev_path in sorted(glob.glob('/sys/bus/pci/devices/*')): + vendor = _read_sysfs(os.path.join(dev_path, 'vendor')) + if vendor.lower() != CORAL_PCI_VENDOR: + continue + + slot = os.path.basename(dev_path) + device_id = _read_sysfs(os.path.join(dev_path, 'device'), '').replace('0x', '') + vendor_id = vendor.replace('0x', '') + + driver_name = '' + drv_link = os.path.join(dev_path, 'driver') + if os.path.islink(drv_link): + driver_name = os.path.basename(os.readlink(drv_link)) + + # "drivers_ready" for PCIe = apex bound and kernel modules present + drivers_ready = (driver_name == 'apex') and apex_loaded + + coral_devices.append({ + 'type': 'pcie', + 'name': _coral_pcie_name_from_lspci(slot), + 'vendor': 'Global Unichip Corp.', + 'vendor_id': vendor_id, + 'device_id': device_id, + 'slot': slot, + 'form_factor': _coral_pci_form_factor(dev_path), + 'interface_speed': _coral_pci_speed(dev_path), + 'kernel_driver': driver_name or None, + 'kernel_modules': { + 'gasket': gasket_loaded, + 'apex': apex_loaded, + }, + 'device_nodes': device_nodes, + 'edgetpu_runtime': runtime, + 'drivers_ready': drivers_ready, + }) + except Exception: + pass + + # ── USB Coral Accelerator ─────────────────────────────────────────── + try: + for line in get_cached_lsusb().split('\n'): + m = re.match( + r'Bus\s+(\d+)\s+Device\s+(\d+):\s+ID\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.*)', + line, re.IGNORECASE) + if not m: + continue + bus, dev, vid, pid, rest = m.groups() + vid = vid.lower() + pid = pid.lower() + if (vid, pid) not in CORAL_USB_IDS: + continue + + programmed = (vid == '18d1') # switched-state vendor/product after fw load + + # Best-effort sysfs path for USB speed. + bus_device = f'{int(bus):03d}:{int(dev):03d}' + usb_speed_label = '' + usb_driver = '' + try: + for usb_dev_dir in glob.glob('/sys/bus/usb/devices/*'): + base = os.path.basename(usb_dev_dir) + if ':' in base: + continue + b = _read_sysfs(os.path.join(usb_dev_dir, 'busnum'), '0') + d = _read_sysfs(os.path.join(usb_dev_dir, 'devnum'), '0') + if f'{int(b):03d}:{int(d):03d}' == bus_device: + usb_speed_label = _usb_speed_label( + _read_sysfs(os.path.join(usb_dev_dir, 'speed'), '')) + usb_driver = _get_usb_driver(usb_dev_dir) + break + except Exception: + pass + + # "drivers_ready" for USB = edgetpu runtime is installed. + # The USB Accelerator does not use a kernel driver; it speaks libusb + # straight to userspace via the libedgetpu1 library. + drivers_ready = bool(runtime) + + coral_devices.append({ + 'type': 'usb', + 'name': 'Coral USB Accelerator', + 'vendor': rest.strip() or 'Google Inc.', + 'vendor_id': vid, + 'device_id': pid, + 'bus_device': bus_device, + 'form_factor': 'USB Accelerator', + 'interface_speed': usb_speed_label, + 'programmed': programmed, + 'usb_driver': usb_driver or None, + 'kernel_driver': None, # USB Coral uses libusb in userspace + 'kernel_modules': {'gasket': gasket_loaded, 'apex': apex_loaded}, + 'device_nodes': [], # No /dev/apex_* for USB + 'edgetpu_runtime': runtime, + 'drivers_ready': drivers_ready, + }) + except Exception: + pass + + return coral_devices + + def get_proxmox_version(): """Get Proxmox version if available. Cached for 6 hours.""" global _system_info_cache @@ -4299,6 +4721,8 @@ def get_hardware_live_info(): 'power_meter': None, 'power_supplies': [], 'ups': None, + 'coral_tpus': [], + 'usb_devices': [], } try: @@ -4337,6 +4761,19 @@ def get_hardware_live_info(): except Exception: pass + # Coral TPU and USB devices are cheap (sysfs reads + cached lsusb) and we + # want them live so the "Install Drivers" button disappears as soon as the + # user finishes running install_coral_pve9.sh without needing a reload. + try: + result['coral_tpus'] = get_coral_info() + except Exception: + pass + + try: + result['usb_devices'] = get_usb_devices() + except Exception: + pass + return result @@ -6293,7 +6730,7 @@ def get_hardware_info(): hardware_data['ups'] = ups_info hardware_data['gpus'] = get_gpu_info() - + # Enrich PCI devices with GPU info where applicable for pci_device in hardware_data['pci_devices']: if pci_device.get('type') == 'Graphics Card': @@ -6301,7 +6738,16 @@ def get_hardware_info(): if pci_device.get('slot') == gpu.get('slot'): pci_device['gpu_info'] = gpu # Add the detected GPU info directly break - + + # Coral TPU (Edge TPU) — dedicated section in the Hardware UI. + # Empty list when no Coral is connected; the frontend skips the section. + hardware_data['coral_tpus'] = get_coral_info() + + # USB peripherals currently plugged into the host. Lists non-hub + # devices (Coral USB accelerators, UPSs, keyboards/mice, pendrives, + # serial adapters, etc.). Empty list on headless servers. + hardware_data['usb_devices'] = get_usb_devices() + return hardware_data except Exception as e: @@ -9368,7 +9814,9 @@ def api_hardware(): 'power_supplies': hardware_info.get('ipmi_power', {}).get('power_supplies', []), 'power_meter': hardware_info.get('power_meter'), 'ups': hardware_info.get('ups') if hardware_info.get('ups') else None, - 'gpus': hardware_info.get('gpus', []) + 'gpus': hardware_info.get('gpus', []), + 'coral_tpus': hardware_info.get('coral_tpus', []), + 'usb_devices': hardware_info.get('usb_devices', []), } diff --git a/AppImage/types/hardware.ts b/AppImage/types/hardware.ts index 90561ac4..36649d5f 100644 --- a/AppImage/types/hardware.ts +++ b/AppImage/types/hardware.ts @@ -112,6 +112,42 @@ export interface UPS { [key: string]: any } +export interface CoralTPU { + type: "pcie" | "usb" + name: string + vendor: string + vendor_id: string + device_id: string + slot?: string // PCIe only, e.g. "0000:0c:00.0" + bus_device?: string // USB only, e.g. "002:007" + form_factor?: string // "M.2 / Mini PCIe (x1)" | "USB Accelerator" | ... + interface_speed?: string // "PCIe 2.5GT/s x1" | "USB 3.0" | ... + kernel_driver?: string | null + usb_driver?: string | null + kernel_modules?: { + gasket: boolean + apex: boolean + } + device_nodes?: string[] + edgetpu_runtime?: string + programmed?: boolean // USB only: runtime has interacted with the device + drivers_ready: boolean +} + +export interface UsbDevice { + bus_device: string // "002:007" + vendor_id: string // "18d1" + product_id: string // "9302" + vendor: string + name: string + class_code: string // "ff" + class_label: string // "Vendor Specific", "HID", "Mass Storage", ... + speed_mbps: number + speed_label: string // "USB 3.0" | "USB 2.0" | ... + serial?: string + driver?: string +} + export interface GPU { slot: string name: string @@ -208,6 +244,8 @@ export interface HardwareData { fans?: Fan[] power_supplies?: PowerSupply[] ups?: UPS | UPS[] + coral_tpus?: CoralTPU[] + usb_devices?: UsbDevice[] } export const fetcher = async (url: string) => { diff --git a/scripts/gpu_tpu/configure_igpu_lxc.sh b/scripts/gpu_tpu/configure_igpu_lxc.sh deleted file mode 100644 index fe5754a5..00000000 --- a/scripts/gpu_tpu/configure_igpu_lxc.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/bin/bash - -# ========================================================== -# ProxMenux - A menu-driven script for Proxmox VE management -# ========================================================== -# Author : MacRimi -# Copyright : (c) 2024 MacRimi -# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) -# Version : 1.1 -# Last Updated: 17/08/2025 -# ========================================================== -# Description: -# This script automates the process of enabling and configuring Intel Integrated GPU (iGPU) support in Proxmox VE LXC containers. -# Its goal is to simplify the configuration of hardware-accelerated graphical capabilities within containers, allowing for efficient -# use of Intel iGPUs for tasks such as transcoding, rendering, and accelerating graphics-intensive applications. -# ========================================================== - -# Configuration ============================================ -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" -BASE_DIR="/usr/local/share/proxmenux" -UTILS_FILE="$BASE_DIR/utils.sh" -VENV_PATH="/opt/googletrans-env" - -if [[ -f "$UTILS_FILE" ]]; then - source "$UTILS_FILE" -fi - -load_language -initialize_cache - -# ========================================================== - - - - -select_container() { - - CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | xargs -n2) - if [ -z "$CONTAINERS" ]; then - msg_error "$(translate 'No containers available in Proxmox.')" - exit 1 - fi - - CONTAINER_ID=$(whiptail --title "$(translate 'Select Container')" \ - --menu "$(translate 'Select the LXC container:')" 20 70 10 $CONTAINERS 3>&1 1>&2 2>&3) - - if [ -z "$CONTAINER_ID" ]; then - msg_error "$(translate 'No container selected. Exiting.')" - exit 1 - fi - - if ! pct list | awk 'NR>1 {print $1}' | grep -qw "$CONTAINER_ID"; then - msg_error "$(translate 'Container with ID') $CONTAINER_ID $(translate 'does not exist. Exiting.')" - exit 1 - fi - - msg_ok "$(translate 'Container selected:') $CONTAINER_ID" -} - - - - -validate_container_id() { - if [ -z "$CONTAINER_ID" ]; then - msg_error "$(translate 'Container ID not defined. Make sure to select a container first.')" - exit 1 - fi - - - if pct status "$CONTAINER_ID" | grep -q "running"; then - msg_info "$(translate 'Stopping the container before applying configuration...')" - pct stop "$CONTAINER_ID" - msg_ok "$(translate 'Container stopped.')" - fi -} - - - -configure_lxc_for_igpu() { - validate_container_id - - CONFIG_FILE="/etc/pve/lxc/${CONTAINER_ID}.conf" - [[ -f "$CONFIG_FILE" ]] || { msg_error "$(translate 'Configuration file for container') $CONTAINER_ID $(translate 'not found.')"; exit 1; } - - - if [[ ! -d /dev/dri ]]; then - modprobe i915 2>/dev/null || true - for _ in {1..5}; do - [[ -d /dev/dri ]] && break - sleep 1 - done - fi - - CT_TYPE=$(pct config "$CONTAINER_ID" | awk '/^unprivileged:/ {print $2}') - [[ -z "$CT_TYPE" ]] && CT_TYPE="0" - - msg_info "$(translate 'Configuring Intel iGPU passthrough for container...')" - - for rn in /dev/dri/renderD*; do - [[ -e "$rn" ]] || continue - chmod 660 "$rn" 2>/dev/null || true - chgrp render "$rn" 2>/dev/null || true - done - - mapfile -t RENDER_NODES < <(find /dev/dri -maxdepth 1 -type c -name 'renderD*' 2>/dev/null || true) - mapfile -t CARD_NODES < <(find /dev/dri -maxdepth 1 -type c -name 'card*' 2>/dev/null || true) - FB_NODE="" - [[ -e /dev/fb0 ]] && FB_NODE="/dev/fb0" - - if [[ ${#RENDER_NODES[@]} -eq 0 && ${#CARD_NODES[@]} -eq 0 && -z "$FB_NODE" ]]; then - msg_warn "$(translate 'No VA-API devices found on host (/dev/dri*, /dev/fb0). Is i915 loaded?')" - return 0 - fi - - if grep -q '^features:' "$CONFIG_FILE"; then - grep -Eq '^features:.*(^|,)\s*nesting=1(\s|,|$)' "$CONFIG_FILE" || sed -i 's/^features:\s*/&nesting=1, /' "$CONFIG_FILE" - else - echo "features: nesting=1" >> "$CONFIG_FILE" - fi - - - - if [[ "$CT_TYPE" == "0" ]]; then - - sed -i '/^lxc\.cgroup2\.devices\.allow:\s*c\s*226:/d' "$CONFIG_FILE" - sed -i '\|^lxc\.mount\.entry:\s*/dev/dri|d' "$CONFIG_FILE" - sed -i '\|^lxc\.mount\.entry:\s*/dev/fb0|d' "$CONFIG_FILE" - - echo "lxc.cgroup2.devices.allow: c 226:* rwm" >> "$CONFIG_FILE" - echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >> "$CONFIG_FILE" - [[ -n "$FB_NODE" ]] && echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >> "$CONFIG_FILE" - - - else - sed -i '/^dev[0-9]\+:/d' "$CONFIG_FILE" - - idx=0 - for c in "${CARD_NODES[@]}"; do - echo "dev${idx}: $c,gid=44" >> "$CONFIG_FILE" - idx=$((idx+1)) - done - for r in "${RENDER_NODES[@]}"; do - echo "dev${idx}: $r,gid=104" >> "$CONFIG_FILE" - idx=$((idx+1)) - done - - fi - msg_ok "$(translate 'iGPU configuration added to container') $CONTAINER_ID." - -} - - - - - -install_igpu_in_container() { - - msg_info2 "$(translate 'Installing iGPU drivers inside the container...')" - tput sc - LOG_FILE=$(mktemp) - - - pct start "$CONTAINER_ID" >/dev/null 2>&1 - - script -q -c "pct exec \"$CONTAINER_ID\" -- bash -c ' - set -e - getent group video >/dev/null || groupadd -g 44 video - getent group render >/dev/null || groupadd -g 104 render - usermod -aG video,render root || true - - apt-get update >/dev/null 2>&1 - apt-get install -y va-driver-all ocl-icd-libopencl1 intel-opencl-icd vainfo intel-gpu-tools - - chgrp video /dev/dri 2>/dev/null || true - chmod 755 /dev/dri 2>/dev/null || true - '" "$LOG_FILE" - - if [ $? -eq 0 ]; then - tput rc - tput ed - rm -f "$LOG_FILE" - msg_ok "$(translate 'iGPU drivers installed inside the container.')" - else - tput rc - tput ed - msg_error "$(translate 'Failed to install iGPU drivers inside the container.')" - cat "$LOG_FILE" - rm -f "$LOG_FILE" - exit 1 - fi -} - - -select_container -show_proxmenux_logo -msg_title "$(translate "Add HW iGPU acceleration to an LXC")" -configure_lxc_for_igpu -install_igpu_in_container - - -msg_success "$(translate 'iGPU configuration completed in container') $CONTAINER_ID." -echo -e -msg_success "$(translate "Press Enter to return to menu...")" -read -r diff --git a/scripts/gpu_tpu/install_coral_pve9.sh b/scripts/gpu_tpu/install_coral_pve9.sh index 6fe32e6d..e3bf0a78 100644 --- a/scripts/gpu_tpu/install_coral_pve9.sh +++ b/scripts/gpu_tpu/install_coral_pve9.sh @@ -3,8 +3,8 @@ # ========================================= # Author : MacRimi # License : MIT -# Version : 1.4 (kernel-conditional patches, direct DKMS, no debuild) -# Last Updated: 01/04/2026 +# Version : 1.5 (feranick fork primary; kernel 6.12+ support; broken-pkg recovery) +# Last Updated: 17/04/2026 # ========================================= LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" @@ -72,6 +72,119 @@ pre_install_prompt() { fi } + +# ============================================================ +# Clean up a broken gasket-dkms package state. +# +# Context: if the user had gasket-dkms installed as a .deb (typical after +# following the Coral docs or via libedgetpu1-std's dependency chain), a +# kernel upgrade on PVE 9 triggers dkms autoinstall → compile fails on +# kernel 6.12+ → dpkg leaves the package half-configured. That broken state +# blocks further apt operations (including our own `apt-get install` below) +# with "E: Sub-process /usr/bin/dpkg returned an error code (1)". +# ============================================================ +cleanup_broken_gasket_dkms() { + # dpkg status codes of interest (first column of `dpkg -l`): + # iF half-configured + # iU unpacked (not configured) + # iH half-installed + # Also catch the case where the package is installed but the DKMS source tree + # is broken (kernel upgrade without rebuild). + local pkg_state + pkg_state=$(dpkg -l gasket-dkms 2>/dev/null | awk '/^[a-zA-Z][a-zA-Z]/ {print $1}' | tail -1) + + if [[ -z "$pkg_state" ]]; then + return 0 # package not present, nothing to clean + fi + + # Any state other than "ii" (installed+configured cleanly) or "rc" + # (removed but config remaining) warrants proactive cleanup. + case "$pkg_state" in + ii|rc) + # Even when state is "ii", a stale DKMS module may exist — drop it to + # ensure our fresh build replaces the old one. + msg_info "$(translate 'Removing any pre-existing gasket-dkms package...')" + dpkg -r gasket-dkms >>"$LOG_FILE" 2>&1 || true + dkms remove gasket/1.0 --all >>"$LOG_FILE" 2>&1 || true + msg_ok "$(translate 'Pre-existing gasket-dkms package removed.')" + ;; + *) + msg_warn "$(translate 'Detected broken gasket-dkms package state:') ${pkg_state}. $(translate 'Forcing removal...')" + dpkg --remove --force-remove-reinstreq gasket-dkms >>"$LOG_FILE" 2>&1 || true + dpkg --purge --force-all gasket-dkms >>"$LOG_FILE" 2>&1 || true + dkms remove gasket/1.0 --all >>"$LOG_FILE" 2>&1 || true + # apt-get install -f resolves any remaining dependency issues left by + # the forced removal above. + apt-get install -f -y >>"$LOG_FILE" 2>&1 || true + msg_ok "$(translate 'Broken gasket-dkms package state recovered.')" + ;; + esac +} + + +# ============================================================ +# Clone the gasket driver sources. +# +# Primary: feranick/gasket-driver — community fork, actively maintained, +# already carries patches for kernel +# 6.10 / 6.12 / 6.13. Preferred. +# Fallback: google/gasket-driver — upstream, stale. Requires the manual +# compatibility patches applied below. +# +# Sets GASKET_SOURCE_USED to "feranick" or "google" so downstream steps know +# whether to apply the local patches. +# ============================================================ +clone_gasket_sources() { + local FERANICK_URL="https://github.com/feranick/gasket-driver.git" + local GOOGLE_URL="https://github.com/google/gasket-driver.git" + + cd /tmp || exit 1 + rm -rf gasket-driver >>"$LOG_FILE" 2>&1 + + msg_info "$(translate 'Cloning Coral driver repository (feranick fork)...')" + if git clone --depth=1 "$FERANICK_URL" gasket-driver >>"$LOG_FILE" 2>&1; then + GASKET_SOURCE_USED="feranick" + msg_ok "$(translate 'feranick/gasket-driver cloned (actively maintained, kernel 6.12+ ready).')" + return 0 + fi + + msg_warn "$(translate 'feranick fork unreachable. Falling back to google/gasket-driver...')" + rm -rf gasket-driver >>"$LOG_FILE" 2>&1 + if git clone --depth=1 "$GOOGLE_URL" gasket-driver >>"$LOG_FILE" 2>&1; then + GASKET_SOURCE_USED="google" + msg_ok "$(translate 'google/gasket-driver cloned (fallback — will apply local patches).')" + return 0 + fi + + msg_error "$(translate 'Could not clone any gasket-driver repository. Check your internet connection and /tmp/coral_install.log')" + exit 1 +} + + +# ============================================================ +# On a failed DKMS build, surface the most relevant lines of make.log +# on-screen so the user (and bug reports) have immediate context without +# having to open the log file manually. +# ============================================================ +show_dkms_build_failure() { + local make_log="/var/lib/dkms/gasket/1.0/build/make.log" + echo "" >&2 + msg_warn "$(translate 'DKMS build failed. Last lines of make.log:')" + if [[ -f "$make_log" ]]; then + # Also append the full log to our install log for post-mortem. + { + echo "---- /var/lib/dkms/gasket/1.0/build/make.log ----" + cat "$make_log" + } >>"$LOG_FILE" 2>&1 + tail -n 50 "$make_log" >&2 + else + echo "$(translate '(make.log not found — DKMS may have failed before invoking make)')" >&2 + fi + echo "" >&2 + echo -e "${TAB}${BL}$(translate 'Full log:')${CL} /tmp/coral_install.log" >&2 + echo "" >&2 +} + install_coral_host() { show_proxmenux_logo : >"$LOG_FILE" @@ -82,6 +195,9 @@ install_coral_host() { KMAJ=$(echo "$KVER" | cut -d. -f1) KMIN=$(echo "$KVER" | cut -d. -f2 | cut -d+ -f1 | cut -d- -f1) + # Recover from a broken gasket-dkms package state (typical after a kernel + # upgrade on PVE 9) before attempting any apt operations. + cleanup_broken_gasket_dkms msg_info "$(translate 'Installing build dependencies...')" export DEBIAN_FRONTEND=noninteractive @@ -91,37 +207,39 @@ install_coral_host() { fi msg_ok "$(translate 'Build dependencies installed.')" - - cd /tmp || exit 1 - rm -rf gasket-driver >>"$LOG_FILE" 2>&1 - msg_info "$(translate 'Cloning Google Coral driver repository...')" - if ! git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1; then - msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1 - fi - msg_ok "$(translate 'Repository cloned successfully.')" - + # Clone sources (feranick fork preferred, google fallback). + # Sets GASKET_SOURCE_USED. + clone_gasket_sources cd /tmp/gasket-driver || exit 1 - msg_info "$(translate 'Patching source for kernel compatibility...')" - # Patch 1: no_llseek was removed in kernel 6.5 — replace with noop_llseek - if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 5 ]]; then - sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c + # Apply compatibility patches ONLY when using the stale google/gasket-driver + # fallback. feranick/gasket-driver already has equivalent fixes upstream, so + # re-applying them would double-edit (and in some cases break) the sources. + if [[ "$GASKET_SOURCE_USED" == "google" ]]; then + msg_info "$(translate 'Patching source for kernel compatibility...')" + + # Patch 1: no_llseek was removed in kernel 6.5 — replace with noop_llseek + if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 5 ]]; then + sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c + fi + + # Patch 2: MODULE_IMPORT_NS changed to string-literal syntax in kernel 6.13. + # IMPORTANT: applying this patch on kernel < 6.13 causes a compile error. + if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 13 ]]; then + sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c + fi + + msg_ok "$(translate 'Source patched successfully.') (kernel ${KVER})" + else + msg_info2 "$(translate 'Skipping manual patches — feranick fork already supports this kernel.')" fi - # Patch 2: MODULE_IMPORT_NS changed to string-literal syntax in kernel 6.13. - # IMPORTANT: applying this patch on kernel < 6.13 causes a compile error. - if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 13 ]]; then - sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c - fi - - msg_ok "$(translate 'Source patched successfully.') (kernel ${KVER})" - msg_info "$(translate 'Preparing DKMS source tree...')" local GASKET_SRC="/usr/src/gasket-1.0" - # Remove any previous installation (package or manual) to avoid conflicts - dpkg -r gasket-dkms >>"$LOG_FILE" 2>&1 || true + # Remove any leftover manual DKMS tree from a previous run (package-level + # cleanup was already handled by cleanup_broken_gasket_dkms above). dkms remove gasket/1.0 --all >>"$LOG_FILE" 2>&1 || true rm -rf "$GASKET_SRC" cp -r /tmp/gasket-driver/. "$GASKET_SRC" @@ -133,13 +251,16 @@ install_coral_host() { msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')" if ! dkms build gasket/1.0 -k "$KVER" >>"$LOG_FILE" 2>&1; then - sed -n '1,200p' /var/lib/dkms/gasket/1.0/build/make.log >>"$LOG_FILE" 2>&1 || true - msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1 + show_dkms_build_failure + msg_error "$(translate 'DKMS build failed.')" + exit 1 fi if ! dkms install gasket/1.0 -k "$KVER" >>"$LOG_FILE" 2>&1; then - msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1 + show_dkms_build_failure + msg_error "$(translate 'DKMS install failed.')" + exit 1 fi - msg_ok "$(translate 'Drivers compiled and installed via DKMS.')" + msg_ok "$(translate 'Drivers compiled and installed via DKMS.') (source: ${GASKET_SOURCE_USED})" ensure_apex_group_and_udev diff --git a/scripts/gpu_tpu/nvidia_installer.sh b/scripts/gpu_tpu/nvidia_installer.sh index 396f4ead..a9337769 100644 --- a/scripts/gpu_tpu/nvidia_installer.sh +++ b/scripts/gpu_tpu/nvidia_installer.sh @@ -520,6 +520,7 @@ unload_nvidia_modules() { } complete_nvidia_uninstall() { + msg_info "$(translate 'Completing NVIDIA uninstallation...')" stop_and_disable_nvidia_services unload_nvidia_modules