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 { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" 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 { Download } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import useSWR from "swr" import useSWR from "swr"
@@ -14,6 +14,8 @@ import {
type GPU, type GPU,
type PCIDevice, type PCIDevice,
type StorageDevice, type StorageDevice,
type CoralTPU,
type UsbDevice,
fetcher as swrFetcher, fetcher as swrFetcher,
} from "../types/hardware" } from "../types/hardware"
import { fetchApi } from "@/lib/api-config" 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. // 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 const hardwareData = staticHardwareData
? { ? {
...staticHardwareData, ...staticHardwareData,
@@ -197,6 +202,8 @@ export default function Hardware() {
power_meter: dynamicHardwareData?.power_meter ?? staticHardwareData.power_meter, power_meter: dynamicHardwareData?.power_meter ?? staticHardwareData.power_meter,
power_supplies: dynamicHardwareData?.power_supplies ?? staticHardwareData.power_supplies, power_supplies: dynamicHardwareData?.power_supplies ?? staticHardwareData.power_supplies,
ups: dynamicHardwareData?.ups ?? staticHardwareData.ups, ups: dynamicHardwareData?.ups ?? staticHardwareData.ups,
coral_tpus: dynamicHardwareData?.coral_tpus ?? staticHardwareData.coral_tpus,
usb_devices: dynamicHardwareData?.usb_devices ?? staticHardwareData.usb_devices,
} }
: undefined : undefined
@@ -230,6 +237,9 @@ export default function Hardware() {
const [installingNvidiaDriver, setInstallingNvidiaDriver] = useState(false) const [installingNvidiaDriver, setInstallingNvidiaDriver] = useState(false)
const [showAmdInstaller, setShowAmdInstaller] = useState(false) const [showAmdInstaller, setShowAmdInstaller] = useState(false)
const [showIntelInstaller, setShowIntelInstaller] = 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 // GPU Switch Mode states
const [editingSwitchModeGpu, setEditingSwitchModeGpu] = useState<string | null>(null) // GPU slot being edited const [editingSwitchModeGpu, setEditingSwitchModeGpu] = useState<string | null>(null) // GPU slot being edited
@@ -1306,6 +1316,222 @@ return (
</DialogContent> </DialogContent>
</Dialog> </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 */} {/* Power Consumption */}
{hardwareData?.power_meter && ( {hardwareData?.power_meter && (
<Card className="border-border/50 bg-card/50 p-6"> <Card className="border-border/50 bg-card/50 p-6">
@@ -2215,6 +2441,125 @@ return (
</DialogContent> </DialogContent>
</Dialog> </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 */} {/* NVIDIA Installation Monitor */}
{/* <HybridScriptMonitor {/* <HybridScriptMonitor
sessionId={nvidiaSessionId} sessionId={nvidiaSessionId}
@@ -2273,6 +2618,20 @@ title="AMD GPU Tools Installation"
title="Intel GPU Tools Installation" title="Intel GPU Tools Installation"
description="Installing intel-gpu-tools for Intel GPU monitoring..." 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 */} {/* GPU Switch Mode Modal */}
{switchModeParams && ( {switchModeParams && (

View File

@@ -7,6 +7,7 @@ ProxMenux Flask Server
- Integrates a web terminal powered by xterm.js - Integrates a web terminal powered by xterm.js
""" """
import glob
import json import json
import logging import logging
import math import math
@@ -1185,6 +1186,17 @@ _ipmi_cache = {
} }
_IPMI_CACHE_TTL = 10 # 10 seconds _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) # Cache for hardware info (lspci, dmidecode, lsblk)
_hardware_cache = { _hardware_cache = {
'lspci': None, 'lspci': None,
@@ -1311,6 +1323,416 @@ def get_cached_lspci_k():
return _hardware_cache[cache_key] or '' 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(): def get_proxmox_version():
"""Get Proxmox version if available. Cached for 6 hours.""" """Get Proxmox version if available. Cached for 6 hours."""
global _system_info_cache global _system_info_cache
@@ -4299,6 +4721,8 @@ def get_hardware_live_info():
'power_meter': None, 'power_meter': None,
'power_supplies': [], 'power_supplies': [],
'ups': None, 'ups': None,
'coral_tpus': [],
'usb_devices': [],
} }
try: try:
@@ -4337,6 +4761,19 @@ def get_hardware_live_info():
except Exception: except Exception:
pass 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 return result
@@ -6302,6 +6739,15 @@ def get_hardware_info():
pci_device['gpu_info'] = gpu # Add the detected GPU info directly pci_device['gpu_info'] = gpu # Add the detected GPU info directly
break 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 return hardware_data
except Exception as e: except Exception as e:
@@ -9368,7 +9814,9 @@ def api_hardware():
'power_supplies': hardware_info.get('ipmi_power', {}).get('power_supplies', []), 'power_supplies': hardware_info.get('ipmi_power', {}).get('power_supplies', []),
'power_meter': hardware_info.get('power_meter'), 'power_meter': hardware_info.get('power_meter'),
'ups': hardware_info.get('ups') if hardware_info.get('ups') else None, '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 [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 { export interface GPU {
slot: string slot: string
name: string name: string
@@ -208,6 +244,8 @@ export interface HardwareData {
fans?: Fan[] fans?: Fan[]
power_supplies?: PowerSupply[] power_supplies?: PowerSupply[]
ups?: UPS | UPS[] ups?: UPS | UPS[]
coral_tpus?: CoralTPU[]
usb_devices?: UsbDevice[]
} }
export const fetcher = async (url: string) => { export const fetcher = async (url: string) => {

View File

@@ -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

View File

@@ -3,8 +3,8 @@
# ========================================= # =========================================
# Author : MacRimi # Author : MacRimi
# License : MIT # License : MIT
# Version : 1.4 (kernel-conditional patches, direct DKMS, no debuild) # Version : 1.5 (feranick fork primary; kernel 6.12+ support; broken-pkg recovery)
# Last Updated: 01/04/2026 # Last Updated: 17/04/2026
# ========================================= # =========================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
@@ -72,6 +72,119 @@ pre_install_prompt() {
fi 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() { install_coral_host() {
show_proxmenux_logo show_proxmenux_logo
: >"$LOG_FILE" : >"$LOG_FILE"
@@ -82,6 +195,9 @@ install_coral_host() {
KMAJ=$(echo "$KVER" | cut -d. -f1) KMAJ=$(echo "$KVER" | cut -d. -f1)
KMIN=$(echo "$KVER" | cut -d. -f2 | cut -d+ -f1 | 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...')" msg_info "$(translate 'Installing build dependencies...')"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
@@ -91,37 +207,39 @@ install_coral_host() {
fi fi
msg_ok "$(translate 'Build dependencies installed.')" msg_ok "$(translate 'Build dependencies installed.')"
# Clone sources (feranick fork preferred, google fallback).
cd /tmp || exit 1 # Sets GASKET_SOURCE_USED.
rm -rf gasket-driver >>"$LOG_FILE" 2>&1 clone_gasket_sources
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.')"
cd /tmp/gasket-driver || exit 1 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 # Apply compatibility patches ONLY when using the stale google/gasket-driver
if [[ "$KMAJ" -gt 6 ]] || [[ "$KMAJ" -eq 6 && "$KMIN" -ge 5 ]]; then # fallback. feranick/gasket-driver already has equivalent fixes upstream, so
sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c # 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 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...')" msg_info "$(translate 'Preparing DKMS source tree...')"
local GASKET_SRC="/usr/src/gasket-1.0" local GASKET_SRC="/usr/src/gasket-1.0"
# Remove any previous installation (package or manual) to avoid conflicts # Remove any leftover manual DKMS tree from a previous run (package-level
dpkg -r gasket-dkms >>"$LOG_FILE" 2>&1 || true # cleanup was already handled by cleanup_broken_gasket_dkms above).
dkms remove gasket/1.0 --all >>"$LOG_FILE" 2>&1 || true dkms remove gasket/1.0 --all >>"$LOG_FILE" 2>&1 || true
rm -rf "$GASKET_SRC" rm -rf "$GASKET_SRC"
cp -r /tmp/gasket-driver/. "$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...')" msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')"
if ! dkms build gasket/1.0 -k "$KVER" >>"$LOG_FILE" 2>&1; then 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 show_dkms_build_failure
msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1 msg_error "$(translate 'DKMS build failed.')"
exit 1
fi fi
if ! dkms install gasket/1.0 -k "$KVER" >>"$LOG_FILE" 2>&1; then 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 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 ensure_apex_group_and_udev

View File

@@ -520,6 +520,7 @@ unload_nvidia_modules() {
} }
complete_nvidia_uninstall() { complete_nvidia_uninstall() {
msg_info "$(translate 'Completing NVIDIA uninstallation...')"
stop_and_disable_nvidia_services stop_and_disable_nvidia_services
unload_nvidia_modules unload_nvidia_modules