mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
update nstall_coral_pve9.sh
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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', []),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -520,6 +520,7 @@ unload_nvidia_modules() {
|
||||
}
|
||||
|
||||
complete_nvidia_uninstall() {
|
||||
msg_info "$(translate 'Completing NVIDIA uninstallation...')"
|
||||
stop_and_disable_nvidia_services
|
||||
unload_nvidia_modules
|
||||
|
||||
|
||||
Reference in New Issue
Block a user