mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 08:56: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 { 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 && (
|
||||||
|
|||||||
@@ -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', []),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user