update nstall_coral_pve9.sh

This commit is contained in:
MacRimi
2026-04-17 19:53:17 +02:00
parent e6faec24fa
commit 75c6f74fc4
6 changed files with 1003 additions and 240 deletions

View File

@@ -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<CoralTPU | null>(null)
const [selectedUsbDevice, setSelectedUsbDevice] = useState<UsbDevice | null>(null)
// GPU Switch Mode states
const [editingSwitchModeGpu, setEditingSwitchModeGpu] = useState<string | null>(null) // GPU slot being edited
@@ -1306,6 +1316,222 @@ return (
</DialogContent>
</Dialog>
{/* 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 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<BrainCircuit className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">Coral TPU / AI Accelerators</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.coral_tpus.length} device{hardwareData.coral_tpus.length > 1 ? "s" : ""}
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareData.coral_tpus.map((coral, index) => (
<div
key={`coral-${index}-${coral.slot || coral.bus_device}`}
onClick={() => 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"
>
<div className="flex items-center justify-between gap-2 mb-2">
<span className="text-sm font-medium line-clamp-2 break-words flex-1">
{coral.name}
</span>
<Badge
className={
coral.type === "usb"
? "bg-purple-500/10 text-purple-500 border-purple-500/20 px-2.5 py-0.5 shrink-0"
: "bg-blue-500/10 text-blue-500 border-blue-500/20 px-2.5 py-0.5 shrink-0"
}
>
{coral.type === "usb" ? "USB" : "PCIe"}
</Badge>
</div>
<div className="space-y-1 text-xs text-muted-foreground">
{coral.form_factor && (
<div className="flex items-center gap-1.5">
<span>{coral.form_factor}</span>
{coral.interface_speed && <span className="text-muted-foreground/60">· {coral.interface_speed}</span>}
</div>
)}
<div className="font-mono">
{coral.type === "pcie" ? coral.slot : coral.bus_device}
</div>
</div>
<div className="mt-3 flex items-center gap-2 text-xs">
{coral.drivers_ready ? (
<>
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
<span className="text-green-500">Drivers ready</span>
</>
) : (
<>
<AlertCircle className="h-3.5 w-3.5 text-yellow-500" />
<span className="text-yellow-500">Drivers not installed</span>
</>
)}
</div>
</div>
))}
</div>
{/* 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) && (
<div className="mt-4 rounded-lg border border-blue-500/20 bg-blue-500/10 p-3 flex items-center justify-between gap-3">
<div className="flex items-start gap-3 flex-1">
<AlertCircle className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<div className="text-sm">
<p className="font-medium text-blue-500">Install Coral TPU drivers</p>
<p className="text-xs text-muted-foreground">
One or more detected Coral devices need drivers. A server reboot is required after installation.
</p>
</div>
</div>
<Button
onClick={() => setShowCoralInstaller(true)}
className="bg-blue-600 hover:bg-blue-700 text-white shrink-0"
>
<Download className="mr-2 h-4 w-4" />
Install Drivers
</Button>
</div>
)}
</Card>
)}
{/* Coral TPU detail modal */}
<Dialog open={selectedCoral !== null} onOpenChange={(open) => !open && setSelectedCoral(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{selectedCoral?.name}</DialogTitle>
<DialogDescription>Coral TPU Device Information</DialogDescription>
</DialogHeader>
{selectedCoral && (
<div className="space-y-3">
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Connection</span>
<Badge
className={
selectedCoral.type === "usb"
? "bg-purple-500/10 text-purple-500 border-purple-500/20"
: "bg-blue-500/10 text-blue-500 border-blue-500/20"
}
>
{selectedCoral.type === "usb" ? "USB" : "PCIe / M.2"}
</Badge>
</div>
{selectedCoral.form_factor && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Form Factor</span>
<span className="text-sm">{selectedCoral.form_factor}</span>
</div>
)}
{selectedCoral.interface_speed && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Link</span>
<span className="font-mono text-sm">{selectedCoral.interface_speed}</span>
</div>
)}
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">
{selectedCoral.type === "usb" ? "Bus:Device" : "PCI Slot"}
</span>
<span className="font-mono text-sm">
{selectedCoral.type === "usb" ? selectedCoral.bus_device : selectedCoral.slot}
</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Vendor / Product ID</span>
<span className="font-mono text-sm">
{selectedCoral.vendor_id}:{selectedCoral.device_id}
</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Vendor</span>
<span className="text-sm">{selectedCoral.vendor}</span>
</div>
{selectedCoral.type === "pcie" && selectedCoral.kernel_driver && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Kernel Driver</span>
<span className={`font-mono text-sm ${selectedCoral.kernel_driver === "apex" ? "text-green-500" : "text-yellow-500"}`}>
{selectedCoral.kernel_driver}
</span>
</div>
)}
{selectedCoral.kernel_modules && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Kernel Modules</span>
<div className="flex gap-2">
<Badge variant="outline" className={selectedCoral.kernel_modules.gasket ? "text-green-500 border-green-500/20" : "text-red-500 border-red-500/20"}>
gasket {selectedCoral.kernel_modules.gasket ? "✓" : "✗"}
</Badge>
<Badge variant="outline" className={selectedCoral.kernel_modules.apex ? "text-green-500 border-green-500/20" : "text-red-500 border-red-500/20"}>
apex {selectedCoral.kernel_modules.apex ? "✓" : "✗"}
</Badge>
</div>
</div>
)}
{selectedCoral.device_nodes && selectedCoral.device_nodes.length > 0 && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Device Nodes</span>
<span className="font-mono text-xs text-right">
{selectedCoral.device_nodes.join(", ")}
</span>
</div>
)}
{selectedCoral.type === "usb" && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Runtime State</span>
<span className="text-sm">
{selectedCoral.programmed ? "Programmed (runtime loaded)" : "Unprogrammed (runtime not loaded)"}
</span>
</div>
)}
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Edge TPU Runtime</span>
<span className="text-sm text-right">
{selectedCoral.edgetpu_runtime || <span className="text-muted-foreground/60">not installed</span>}
</span>
</div>
<div className="mt-4 rounded-md border border-border/50 bg-muted/30 p-3 text-xs text-muted-foreground">
<strong className="text-foreground">Note:</strong> Coral TPUs do not expose temperature, utilization or power telemetry through standard interfaces. Monitoring is limited to device presence and driver state.
</div>
{!selectedCoral.drivers_ready && (
<Button
onClick={() => {
setSelectedCoral(null)
setShowCoralInstaller(true)
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
<Download className="mr-2 h-4 w-4" />
Install Coral TPU Drivers
</Button>
)}
</div>
)}
</DialogContent>
</Dialog>
{/* Power Consumption */}
{hardwareData?.power_meter && (
<Card className="border-border/50 bg-card/50 p-6">
@@ -2215,6 +2441,125 @@ return (
</DialogContent>
</Dialog>
{/* 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 && (
<Card className="border-border/50 bg-card/50 p-6">
<div className="mb-4 flex items-center gap-2">
<Usb className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">USB Devices</h2>
<Badge variant="outline" className="ml-auto">
{hardwareData.usb_devices.length} device{hardwareData.usb_devices.length > 1 ? "s" : ""}
</Badge>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{hardwareData.usb_devices.map((usb, index) => (
<div
key={`usb-${index}-${usb.bus_device}`}
onClick={() => 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"
>
<div className="flex items-center justify-between gap-2 mb-1">
<span className="text-sm font-medium line-clamp-2 break-words flex-1">
{usb.name}
</span>
<Badge className={getDeviceTypeColor(usb.class_label)}>
{usb.class_label}
</Badge>
</div>
<div className="space-y-0.5 text-xs text-muted-foreground">
{usb.speed_label && <div>{usb.speed_label}</div>}
<div className="font-mono">
{usb.bus_device} · {usb.vendor_id}:{usb.product_id}
</div>
{usb.driver && (
<div className="font-mono text-green-500/80">Driver: {usb.driver}</div>
)}
</div>
</div>
))}
</div>
</Card>
)}
{/* USB Device detail modal — mirrors the PCI Device modal for consistency. */}
<Dialog open={selectedUsbDevice !== null} onOpenChange={(open) => !open && setSelectedUsbDevice(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{selectedUsbDevice?.name}</DialogTitle>
<DialogDescription>USB Device Information</DialogDescription>
</DialogHeader>
{selectedUsbDevice && (
<div className="space-y-3">
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Class</span>
<Badge className={getDeviceTypeColor(selectedUsbDevice.class_label)}>
{selectedUsbDevice.class_label}
</Badge>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Bus:Device</span>
<span className="font-mono text-sm">{selectedUsbDevice.bus_device}</span>
</div>
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Device Name</span>
<span className="text-sm text-right">{selectedUsbDevice.name}</span>
</div>
{selectedUsbDevice.vendor && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Vendor</span>
<span className="text-sm">{selectedUsbDevice.vendor}</span>
</div>
)}
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Vendor / Product ID</span>
<span className="font-mono text-sm">
{selectedUsbDevice.vendor_id}:{selectedUsbDevice.product_id}
</span>
</div>
{selectedUsbDevice.speed_label && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Speed</span>
<span className="text-sm">
{selectedUsbDevice.speed_label}
{selectedUsbDevice.speed_mbps > 0 && (
<span className="text-muted-foreground/60 ml-2">({selectedUsbDevice.speed_mbps} Mbps)</span>
)}
</span>
</div>
)}
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Class Code</span>
<span className="font-mono text-sm">0x{selectedUsbDevice.class_code}</span>
</div>
{selectedUsbDevice.driver && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Driver</span>
<span className="font-mono text-sm text-green-500">{selectedUsbDevice.driver}</span>
</div>
)}
{selectedUsbDevice.serial && (
<div className="flex justify-between border-b border-border/50 pb-2">
<span className="text-sm font-medium text-muted-foreground">Serial</span>
<span className="font-mono text-sm text-right break-all">{selectedUsbDevice.serial}</span>
</div>
)}
</div>
)}
</DialogContent>
</Dialog>
{/* NVIDIA Installation Monitor */}
{/* <HybridScriptMonitor
sessionId={nvidiaSessionId}
@@ -2273,6 +2618,20 @@ title="AMD GPU Tools Installation"
title="Intel GPU Tools Installation"
description="Installing intel-gpu-tools for Intel GPU monitoring..."
/>
<ScriptTerminalModal
open={showCoralInstaller}
onClose={() => {
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 && (

View File

@@ -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 <dev>/<dev>:1.0/bInterfaceClass.
"""
try:
# Sysfs names interfaces as <busnum>-<portpath>: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 <class>: <vendor> <device>"
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', []),
}

View File

@@ -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) => {