mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
Update scripts
This commit is contained in:
@@ -7,7 +7,7 @@ import { Button } from "./ui/button"
|
|||||||
import { Input } from "./ui/input"
|
import { Input } from "./ui/input"
|
||||||
import { Label } from "./ui/label"
|
import { Label } from "./ui/label"
|
||||||
import { Checkbox } from "./ui/checkbox"
|
import { Checkbox } from "./ui/checkbox"
|
||||||
import { Lock, User, AlertCircle, Server, Shield } from "lucide-react"
|
import { Lock, User, AlertCircle, Server, Shield, Eye, EyeOff } from "lucide-react"
|
||||||
import { getApiUrl } from "../lib/api-config"
|
import { getApiUrl } from "../lib/api-config"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export function Login({ onLogin }: LoginProps) {
|
|||||||
const [totpCode, setTotpCode] = useState("")
|
const [totpCode, setTotpCode] = useState("")
|
||||||
const [requiresTotp, setRequiresTotp] = useState(false)
|
const [requiresTotp, setRequiresTotp] = useState(false)
|
||||||
const [rememberMe, setRememberMe] = useState(false)
|
const [rememberMe, setRememberMe] = useState(false)
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
@@ -161,14 +162,27 @@ export function Login({ onLogin }: LoginProps) {
|
|||||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
id="login-password"
|
id="login-password"
|
||||||
type="password"
|
type={showPassword ? "text" : "password"}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="pl-10 text-base"
|
className="pl-10 pr-10 text-base"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
disabled={loading}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer, Archive, Info, Clock, Usb } from "lucide-react"
|
import { HardDrive, Database, AlertTriangle, CheckCircle2, XCircle, Square, Thermometer, Archive, Info, Clock, Usb, Server, Activity, FileText, Play, Loader2 } from "lucide-react"
|
||||||
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 { Button } from "@/components/ui/button"
|
||||||
import { fetchApi } from "../lib/api-config"
|
import { fetchApi } from "../lib/api-config"
|
||||||
|
|
||||||
interface DiskInfo {
|
interface DiskInfo {
|
||||||
@@ -44,6 +45,8 @@ interface DiskInfo {
|
|||||||
observations_count?: number
|
observations_count?: number
|
||||||
connection_type?: 'usb' | 'sata' | 'nvme' | 'sas' | 'internal' | 'unknown'
|
connection_type?: 'usb' | 'sata' | 'nvme' | 'sas' | 'internal' | 'unknown'
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
|
is_system_disk?: boolean
|
||||||
|
system_usage?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiskObservation {
|
interface DiskObservation {
|
||||||
@@ -118,6 +121,7 @@ export function StorageOverview() {
|
|||||||
const [detailsOpen, setDetailsOpen] = useState(false)
|
const [detailsOpen, setDetailsOpen] = useState(false)
|
||||||
const [diskObservations, setDiskObservations] = useState<DiskObservation[]>([])
|
const [diskObservations, setDiskObservations] = useState<DiskObservation[]>([])
|
||||||
const [loadingObservations, setLoadingObservations] = useState(false)
|
const [loadingObservations, setLoadingObservations] = useState(false)
|
||||||
|
const [activeModalTab, setActiveModalTab] = useState<"overview" | "smart">("overview")
|
||||||
|
|
||||||
const fetchStorageData = async () => {
|
const fetchStorageData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -838,12 +842,18 @@ export function StorageOverview() {
|
|||||||
>
|
>
|
||||||
<div className="space-y-2 mb-3">
|
<div className="space-y-2 mb-3">
|
||||||
{/* Row 1: Device name and type badge */}
|
{/* Row 1: Device name and type badge */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||||
<h3 className="font-semibold">/dev/{disk.name}</h3>
|
<h3 className="font-semibold">/dev/{disk.name}</h3>
|
||||||
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
|
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
|
||||||
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
|
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{disk.is_system_disk && (
|
||||||
|
<Badge className="bg-orange-500/10 text-orange-500 border-orange-500/20 gap-1">
|
||||||
|
<Server className="h-3 w-3" />
|
||||||
|
System
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: Model, temperature, and health status */}
|
{/* Row 2: Model, temperature, and health status */}
|
||||||
@@ -930,6 +940,12 @@ export function StorageOverview() {
|
|||||||
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
|
<Badge className={getDiskTypeBadge(disk.name, disk.rotation_rate).className}>
|
||||||
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
|
{getDiskTypeBadge(disk.name, disk.rotation_rate).label}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{disk.is_system_disk && (
|
||||||
|
<Badge className="bg-orange-500/10 text-orange-500 border-orange-500/20 gap-1">
|
||||||
|
<Server className="h-3 w-3" />
|
||||||
|
System
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: Model, temperature, and health status */}
|
{/* Row 2: Model, temperature, and health status */}
|
||||||
@@ -1187,9 +1203,12 @@ export function StorageOverview() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Disk Details Dialog */}
|
{/* Disk Details Dialog */}
|
||||||
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
|
<Dialog open={detailsOpen} onOpenChange={(open) => {
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] sm:max-h-[85vh] overflow-y-auto">
|
setDetailsOpen(open)
|
||||||
<DialogHeader>
|
if (!open) setActiveModalTab("overview")
|
||||||
|
}}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] sm:max-h-[85vh] overflow-hidden flex flex-col p-0">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
{selectedDisk?.connection_type === 'usb' ? (
|
{selectedDisk?.connection_type === 'usb' ? (
|
||||||
<Usb className="h-5 w-5 text-orange-400" />
|
<Usb className="h-5 w-5 text-orange-400" />
|
||||||
@@ -1200,10 +1219,47 @@ export function StorageOverview() {
|
|||||||
{selectedDisk?.connection_type === 'usb' && (
|
{selectedDisk?.connection_type === 'usb' && (
|
||||||
<Badge className="bg-orange-500/10 text-orange-400 border-orange-500/20 text-[10px] px-1.5">USB</Badge>
|
<Badge className="bg-orange-500/10 text-orange-400 border-orange-500/20 text-[10px] px-1.5">USB</Badge>
|
||||||
)}
|
)}
|
||||||
|
{selectedDisk?.is_system_disk && (
|
||||||
|
<Badge className="bg-orange-500/10 text-orange-500 border-orange-500/20 gap-1">
|
||||||
|
<Server className="h-3 w-3" />
|
||||||
|
System
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>Complete SMART information and health status</DialogDescription>
|
<DialogDescription>
|
||||||
|
{selectedDisk?.model !== "Unknown" ? selectedDisk?.model : "Physical disk"} - {selectedDisk?.size_formatted}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{selectedDisk && (
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="flex border-b border-border px-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveModalTab("overview")}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||||
|
activeModalTab === "overview"
|
||||||
|
? "border-blue-500 text-blue-500"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveModalTab("smart")}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||||
|
activeModalTab === "smart"
|
||||||
|
? "border-green-500 text-green-500"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
SMART Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
|
||||||
|
{selectedDisk && activeModalTab === "overview" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -1402,6 +1458,745 @@ export function StorageOverview() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* SMART Test Tab */}
|
||||||
|
{selectedDisk && activeModalTab === "smart" && (
|
||||||
|
<SmartTestTab disk={selectedDisk} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMART Test Tab Component
|
||||||
|
interface SmartTestTabProps {
|
||||||
|
disk: DiskInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmartTestStatus {
|
||||||
|
status: 'idle' | 'running' | 'completed' | 'failed'
|
||||||
|
test_type?: string
|
||||||
|
progress?: number
|
||||||
|
result?: string
|
||||||
|
last_test?: {
|
||||||
|
type: string
|
||||||
|
status: string
|
||||||
|
timestamp: string
|
||||||
|
duration?: string
|
||||||
|
}
|
||||||
|
smart_data?: {
|
||||||
|
device: string
|
||||||
|
model: string
|
||||||
|
serial: string
|
||||||
|
firmware: string
|
||||||
|
smart_status: string
|
||||||
|
temperature: number
|
||||||
|
power_on_hours: number
|
||||||
|
attributes: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
worst: number
|
||||||
|
threshold: number
|
||||||
|
raw_value: string
|
||||||
|
status: 'ok' | 'warning' | 'critical'
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SmartTestTab({ disk }: SmartTestTabProps) {
|
||||||
|
const [testStatus, setTestStatus] = useState<SmartTestStatus>({ status: 'idle' })
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [runningTest, setRunningTest] = useState<'short' | 'long' | null>(null)
|
||||||
|
const [showReport, setShowReport] = useState(false)
|
||||||
|
const [reportTab, setReportTab] = useState<'overview' | 'attributes' | 'history' | 'recommendations'>('overview')
|
||||||
|
|
||||||
|
// Fetch current SMART status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSmartStatus()
|
||||||
|
}, [disk.name])
|
||||||
|
|
||||||
|
const fetchSmartStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const data = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
|
||||||
|
setTestStatus(data)
|
||||||
|
} catch {
|
||||||
|
setTestStatus({ status: 'idle' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSmartTest = async (testType: 'short' | 'long') => {
|
||||||
|
try {
|
||||||
|
setRunningTest(testType)
|
||||||
|
await fetchApi(`/api/storage/smart/${disk.name}/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ test_type: testType })
|
||||||
|
})
|
||||||
|
// Poll for status updates
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchApi<SmartTestStatus>(`/api/storage/smart/${disk.name}`)
|
||||||
|
setTestStatus(data)
|
||||||
|
if (data.status !== 'running') {
|
||||||
|
clearInterval(pollInterval)
|
||||||
|
setRunningTest(null)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
clearInterval(pollInterval)
|
||||||
|
setRunningTest(null)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
} catch {
|
||||||
|
setRunningTest(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 gap-3">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
<p className="text-sm text-muted-foreground">Loading SMART data...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-semibold flex items-center gap-2">
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
Run SMART Test
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => runSmartTest('short')}
|
||||||
|
disabled={runningTest !== null}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{runningTest === 'short' ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Short Test (~2 min)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => runSmartTest('long')}
|
||||||
|
disabled={runningTest !== null}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{runningTest === 'long' ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Extended Test (background)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={fetchSmartStatus}
|
||||||
|
disabled={runningTest !== null}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
Refresh Status
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Short test takes ~2 minutes. Extended test runs in the background and can take several hours for large disks.
|
||||||
|
You will receive a notification when the test completes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Progress */}
|
||||||
|
{testStatus.status === 'running' && (
|
||||||
|
<div className="border rounded-lg p-4 bg-blue-500/5 border-blue-500/20">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-blue-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-blue-500">
|
||||||
|
{testStatus.test_type === 'short' ? 'Short' : 'Extended'} test in progress
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Please wait while the test completes...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{testStatus.progress !== undefined && (
|
||||||
|
<Progress value={testStatus.progress} className="h-2 [&>div]:bg-blue-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Last Test Result */}
|
||||||
|
{testStatus.last_test && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-semibold flex items-center gap-2">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
Last Test Result
|
||||||
|
</h4>
|
||||||
|
<div className={`border rounded-lg p-4 ${
|
||||||
|
testStatus.last_test.status === 'passed'
|
||||||
|
? 'bg-green-500/5 border-green-500/20'
|
||||||
|
: 'bg-red-500/5 border-red-500/20'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{testStatus.last_test.status === 'passed' ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">
|
||||||
|
{testStatus.last_test.type === 'short' ? 'Short' : 'Extended'} Test - {' '}
|
||||||
|
{testStatus.last_test.status === 'passed' ? 'Passed' : 'Failed'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge className={testStatus.last_test.status === 'passed'
|
||||||
|
? 'bg-green-500/10 text-green-500 border-green-500/20'
|
||||||
|
: 'bg-red-500/10 text-red-500 border-red-500/20'
|
||||||
|
}>
|
||||||
|
{testStatus.last_test.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Completed</p>
|
||||||
|
<p className="font-medium">{testStatus.last_test.timestamp}</p>
|
||||||
|
</div>
|
||||||
|
{testStatus.last_test.duration && (
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Duration</p>
|
||||||
|
<p className="font-medium">{testStatus.last_test.duration}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SMART Attributes Summary */}
|
||||||
|
{testStatus.smart_data?.attributes && testStatus.smart_data.attributes.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-semibold flex items-center gap-2">
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
SMART Attributes
|
||||||
|
</h4>
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<div className="grid grid-cols-12 gap-2 p-3 bg-muted/30 text-xs font-medium text-muted-foreground">
|
||||||
|
<div className="col-span-1">ID</div>
|
||||||
|
<div className="col-span-5">Attribute</div>
|
||||||
|
<div className="col-span-2 text-center">Value</div>
|
||||||
|
<div className="col-span-2 text-center">Worst</div>
|
||||||
|
<div className="col-span-2 text-center">Status</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border max-h-[200px] overflow-y-auto">
|
||||||
|
{testStatus.smart_data.attributes.slice(0, 15).map((attr) => (
|
||||||
|
<div key={attr.id} className="grid grid-cols-12 gap-2 p-3 text-sm items-center">
|
||||||
|
<div className="col-span-1 text-muted-foreground">{attr.id}</div>
|
||||||
|
<div className="col-span-5 truncate" title={attr.name}>{attr.name}</div>
|
||||||
|
<div className="col-span-2 text-center font-mono">{attr.value}</div>
|
||||||
|
<div className="col-span-2 text-center font-mono text-muted-foreground">{attr.worst}</div>
|
||||||
|
<div className="col-span-2 text-center">
|
||||||
|
{attr.status === 'ok' ? (
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mx-auto" />
|
||||||
|
) : attr.status === 'warning' ? (
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-500 mx-auto" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-4 w-4 text-red-500 mx-auto" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View Full Report Button */}
|
||||||
|
<div className="pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full gap-2"
|
||||||
|
onClick={() => setShowReport(true)}
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
View Full SMART Report
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-muted-foreground text-center mt-2">
|
||||||
|
Generate a comprehensive professional report with detailed analysis and recommendations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Full SMART Report Dialog */}
|
||||||
|
<Dialog open={showReport} onOpenChange={setShowReport}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-0">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
SMART Health Report: /dev/{disk.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Comprehensive analysis of disk health, SMART attributes, and recommendations
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Report Tabs */}
|
||||||
|
<div className="flex border-b border-border px-6 overflow-x-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => setReportTab('overview')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
||||||
|
reportTab === 'overview'
|
||||||
|
? "border-blue-500 text-blue-500"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setReportTab('attributes')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
||||||
|
reportTab === 'attributes'
|
||||||
|
? "border-purple-500 text-purple-500"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
Attributes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setReportTab('history')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
||||||
|
reportTab === 'history'
|
||||||
|
? "border-amber-500 text-amber-500"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
History
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setReportTab('recommendations')}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
||||||
|
reportTab === 'recommendations'
|
||||||
|
? "border-green-500 text-green-500"
|
||||||
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
Recommendations
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Report Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
|
||||||
|
{/* Overview Tab */}
|
||||||
|
{reportTab === 'overview' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Health Score Card */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className={`border rounded-lg p-4 ${
|
||||||
|
testStatus.smart_status === 'passed'
|
||||||
|
? 'bg-green-500/5 border-green-500/20'
|
||||||
|
: testStatus.smart_status === 'failed'
|
||||||
|
? 'bg-red-500/5 border-red-500/20'
|
||||||
|
: 'bg-yellow-500/5 border-yellow-500/20'
|
||||||
|
}`}>
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Overall Health</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{testStatus.smart_status === 'passed' ? (
|
||||||
|
<CheckCircle2 className="h-6 w-6 text-green-500" />
|
||||||
|
) : testStatus.smart_status === 'failed' ? (
|
||||||
|
<XCircle className="h-6 w-6 text-red-500" />
|
||||||
|
) : (
|
||||||
|
<AlertTriangle className="h-6 w-6 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
<span className="text-xl font-bold capitalize">
|
||||||
|
{testStatus.smart_status || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4 bg-muted/20">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Temperature</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Thermometer className="h-6 w-6 text-blue-500" />
|
||||||
|
<span className="text-xl font-bold">
|
||||||
|
{disk.temperature > 0 ? `${disk.temperature}°C` : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4 bg-muted/20">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Power On Time</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="h-6 w-6 text-purple-500" />
|
||||||
|
<span className="text-xl font-bold">
|
||||||
|
{disk.power_on_hours ? `${disk.power_on_hours.toLocaleString()}h` : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Executive Summary */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h4 className="font-semibold mb-3 flex items-center gap-2">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
Executive Summary
|
||||||
|
</h4>
|
||||||
|
<div className="prose prose-sm prose-invert max-w-none">
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
{testStatus.smart_status === 'passed' ? (
|
||||||
|
<>
|
||||||
|
This disk is operating within normal parameters. All SMART attributes are within acceptable thresholds,
|
||||||
|
indicating good health. The disk has been powered on for approximately{' '}
|
||||||
|
<span className="text-foreground font-medium">
|
||||||
|
{disk.power_on_hours ? `${Math.round(disk.power_on_hours / 24)} days` : 'an unknown period'}
|
||||||
|
</span>{' '}
|
||||||
|
and is currently operating at{' '}
|
||||||
|
<span className="text-foreground font-medium">{disk.temperature || 'N/A'}°C</span>.
|
||||||
|
{disk.reallocated_sectors === 0 && disk.pending_sectors === 0
|
||||||
|
? ' No bad sectors have been detected.'
|
||||||
|
: disk.reallocated_sectors && disk.reallocated_sectors > 0
|
||||||
|
? ` ${disk.reallocated_sectors} sectors have been reallocated, which may indicate early signs of wear.`
|
||||||
|
: ''}
|
||||||
|
</>
|
||||||
|
) : testStatus.smart_status === 'failed' ? (
|
||||||
|
<>
|
||||||
|
<span className="text-red-400 font-medium">Warning: This disk has failed SMART health assessment.</span>{' '}
|
||||||
|
One or more critical SMART attributes have exceeded their failure threshold.
|
||||||
|
It is strongly recommended to backup all data immediately and consider replacing this disk.
|
||||||
|
{disk.reallocated_sectors && disk.reallocated_sectors > 0
|
||||||
|
? ` The disk has ${disk.reallocated_sectors} reallocated sectors, indicating physical media degradation.`
|
||||||
|
: ''}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
The disk health status could not be fully determined. Some SMART attributes may be showing warning signs.
|
||||||
|
It is recommended to run a full SMART self-test and monitor the disk closely.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Metrics */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h4 className="font-semibold mb-3">Key Metrics</h4>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Model</p>
|
||||||
|
<p className="font-medium">{disk.model || 'Unknown'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Serial</p>
|
||||||
|
<p className="font-medium font-mono text-xs">{disk.serial?.replace(/\\x[0-9a-fA-F]{2}/g, '') || 'Unknown'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Capacity</p>
|
||||||
|
<p className="font-medium">{disk.size_formatted || 'Unknown'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Power Cycles</p>
|
||||||
|
<p className="font-medium">{disk.power_cycles?.toLocaleString() || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Reallocated Sectors</p>
|
||||||
|
<p className={`font-medium ${disk.reallocated_sectors && disk.reallocated_sectors > 0 ? 'text-yellow-500' : ''}`}>
|
||||||
|
{disk.reallocated_sectors ?? 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Pending Sectors</p>
|
||||||
|
<p className={`font-medium ${disk.pending_sectors && disk.pending_sectors > 0 ? 'text-yellow-500' : ''}`}>
|
||||||
|
{disk.pending_sectors ?? 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">CRC Errors</p>
|
||||||
|
<p className={`font-medium ${disk.crc_errors && disk.crc_errors > 0 ? 'text-yellow-500' : ''}`}>
|
||||||
|
{disk.crc_errors ?? 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Disk Type</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
{disk.name.startsWith('nvme') ? 'NVMe' : !disk.rotation_rate || disk.rotation_rate === 0 ? 'SSD' : 'HDD'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Attributes Tab */}
|
||||||
|
{reportTab === 'attributes' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="border rounded-lg p-4 bg-muted/20">
|
||||||
|
<h4 className="font-semibold mb-2">Understanding SMART Attributes</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
SMART (Self-Monitoring, Analysis and Reporting Technology) attributes are sensors built into hard drives and SSDs.
|
||||||
|
Each attribute has a current value, a worst recorded value, and a threshold. When the current value drops below the threshold,
|
||||||
|
the attribute is considered failed. Values typically decrease from 100 (or 200/253 on some drives) as the attribute degrades.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testStatus.smart_data?.attributes && testStatus.smart_data.attributes.length > 0 ? (
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<div className="grid grid-cols-12 gap-2 p-3 bg-muted/30 text-xs font-medium text-muted-foreground">
|
||||||
|
<div className="col-span-1">ID</div>
|
||||||
|
<div className="col-span-4">Attribute Name</div>
|
||||||
|
<div className="col-span-2 text-center">Value</div>
|
||||||
|
<div className="col-span-2 text-center">Worst</div>
|
||||||
|
<div className="col-span-2 text-center">Threshold</div>
|
||||||
|
<div className="col-span-1 text-center">Status</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-border max-h-[400px] overflow-y-auto">
|
||||||
|
{testStatus.smart_data.attributes.map((attr) => (
|
||||||
|
<div key={attr.id} className={`grid grid-cols-12 gap-2 p-3 text-sm items-center ${
|
||||||
|
attr.status === 'critical' ? 'bg-red-500/5' : attr.status === 'warning' ? 'bg-yellow-500/5' : ''
|
||||||
|
}`}>
|
||||||
|
<div className="col-span-1 text-muted-foreground font-mono">{attr.id}</div>
|
||||||
|
<div className="col-span-4">
|
||||||
|
<p className="truncate font-medium" title={attr.name}>{attr.name.replace(/_/g, ' ')}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Raw: {attr.raw_value}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 text-center font-mono font-medium">{attr.value}</div>
|
||||||
|
<div className="col-span-2 text-center font-mono text-muted-foreground">{attr.worst}</div>
|
||||||
|
<div className="col-span-2 text-center font-mono text-muted-foreground">{attr.threshold}</div>
|
||||||
|
<div className="col-span-1 text-center">
|
||||||
|
{attr.status === 'ok' ? (
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mx-auto" />
|
||||||
|
) : attr.status === 'warning' ? (
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-500 mx-auto" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-4 w-4 text-red-500 mx-auto" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<Activity className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>No SMART attribute data available.</p>
|
||||||
|
<p className="text-sm mt-1">Run a SMART test to collect attribute data.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* History Tab */}
|
||||||
|
{reportTab === 'history' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{testStatus.last_test ? (
|
||||||
|
<div className={`border rounded-lg p-4 ${
|
||||||
|
testStatus.last_test.status === 'passed'
|
||||||
|
? 'bg-green-500/5 border-green-500/20'
|
||||||
|
: 'bg-red-500/5 border-red-500/20'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="font-semibold flex items-center gap-2">
|
||||||
|
{testStatus.last_test.status === 'passed' ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-5 w-5 text-red-500" />
|
||||||
|
)}
|
||||||
|
Last Test Result
|
||||||
|
</h4>
|
||||||
|
<Badge className={testStatus.last_test.status === 'passed'
|
||||||
|
? 'bg-green-500/10 text-green-500 border-green-500/20'
|
||||||
|
: 'bg-red-500/10 text-red-500 border-red-500/20'
|
||||||
|
}>
|
||||||
|
{testStatus.last_test.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Test Type</p>
|
||||||
|
<p className="font-medium capitalize">{testStatus.last_test.type}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-muted-foreground">Completed</p>
|
||||||
|
<p className="font-medium">{testStatus.last_test.timestamp}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<Clock className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>No test history available.</p>
|
||||||
|
<p className="text-sm mt-1">Run a SMART self-test to see results here.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4 bg-muted/20">
|
||||||
|
<h4 className="font-semibold mb-2">About Self-Tests</h4>
|
||||||
|
<div className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
<strong className="text-foreground">Short Test (~2 minutes):</strong> Performs a quick check of the disk's
|
||||||
|
basic functionality including read/seek tests on a small portion of the disk surface.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong className="text-foreground">Extended Test (hours):</strong> Performs a comprehensive surface scan
|
||||||
|
of the entire disk. Duration depends on disk size - typically 1-2 hours per TB.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recommendations Tab */}
|
||||||
|
{reportTab === 'recommendations' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Status-based recommendations */}
|
||||||
|
{testStatus.smart_status === 'passed' && (
|
||||||
|
<div className="border rounded-lg p-4 bg-green-500/5 border-green-500/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-green-500">Disk is Healthy</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
All SMART attributes are within normal ranges. Continue with regular monitoring.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{testStatus.smart_status === 'failed' && (
|
||||||
|
<div className="border rounded-lg p-4 bg-red-500/5 border-red-500/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<XCircle className="h-5 w-5 text-red-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-red-500">Critical: Disk Replacement Recommended</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
This disk has failed SMART health assessment. Backup all data immediately and plan for disk replacement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Conditional recommendations */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-semibold">Recommendations</h4>
|
||||||
|
|
||||||
|
{(disk.reallocated_sectors ?? 0) > 0 && (
|
||||||
|
<div className="border rounded-lg p-3 bg-yellow-500/5 border-yellow-500/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Reallocated Sectors Detected</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{disk.reallocated_sectors} sectors have been reallocated. This indicates the disk has found and
|
||||||
|
remapped bad sectors. Monitor this value - if it increases rapidly, consider replacing the disk.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(disk.pending_sectors ?? 0) > 0 && (
|
||||||
|
<div className="border rounded-lg p-3 bg-yellow-500/5 border-yellow-500/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Pending Sectors Detected</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{disk.pending_sectors} sectors are pending reallocation. These sectors may be unreadable.
|
||||||
|
Run an extended self-test to force reallocation attempts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{disk.temperature > 55 && (
|
||||||
|
<div className="border rounded-lg p-3 bg-yellow-500/5 border-yellow-500/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Thermometer className="h-5 w-5 text-yellow-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Elevated Temperature</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Current temperature ({disk.temperature}°C) is above optimal. Improve airflow or reduce disk activity.
|
||||||
|
Sustained high temperatures can reduce disk lifespan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(disk.power_on_hours ?? 0) > 35000 && (
|
||||||
|
<div className="border rounded-lg p-3 bg-blue-500/5 border-blue-500/20">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Info className="h-5 w-5 text-blue-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">High Power-On Hours</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
This disk has been running for {Math.round((disk.power_on_hours ?? 0) / 8760)} years.
|
||||||
|
While still operational, consider planning for replacement as disks typically have a 3-5 year lifespan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* General best practices */}
|
||||||
|
<div className="border rounded-lg p-4 bg-muted/20 mt-6">
|
||||||
|
<h4 className="font-semibold mb-3">Best Practices</h4>
|
||||||
|
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>Run a short SMART test monthly to catch early issues</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>Run an extended test quarterly for comprehensive verification</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>Maintain regular backups - SMART can detect some failures but not all</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>Keep disk temperatures below 50°C for optimal lifespan</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span>Replace disks proactively after 4-5 years of heavy use</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Report Footer */}
|
||||||
|
<div className="border-t px-6 py-4 flex justify-between items-center bg-muted/10">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Report generated by ProxMenux Monitor
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => window.print()} className="gap-2">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
Print Report
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1784,6 +1784,270 @@ def is_disk_removable(disk_name):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_system_mount(mountpoint):
|
||||||
|
"""Check if mountpoint is a critical system path (matching bash scripts logic)."""
|
||||||
|
system_mounts = ('/', '/boot', '/boot/efi', '/efi', '/usr', '/var', '/etc',
|
||||||
|
'/lib', '/lib64', '/run', '/proc', '/sys')
|
||||||
|
if mountpoint in system_mounts:
|
||||||
|
return True
|
||||||
|
# Also check if it's under these paths
|
||||||
|
for prefix in ('/usr/', '/var/', '/lib/', '/lib64/'):
|
||||||
|
if mountpoint.startswith(prefix):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_zfs_root_pool():
|
||||||
|
"""Get the ZFS pool containing the root filesystem, if any (matches bash _get_zfs_root_pool)."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['df', '/'], capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
if len(lines) >= 2:
|
||||||
|
root_fs = lines[1].split()[0]
|
||||||
|
# A ZFS dataset looks like "rpool/ROOT/pve-1" — not /dev/
|
||||||
|
if not root_fs.startswith('/dev/') and '/' in root_fs:
|
||||||
|
return root_fs.split('/')[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_zfs_entry(entry):
|
||||||
|
"""
|
||||||
|
Resolve a ZFS device entry to a base disk name.
|
||||||
|
Handles: /dev/paths, by-id names, short kernel names (matches bash _resolve_zfs_entry).
|
||||||
|
"""
|
||||||
|
path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if entry.startswith('/dev/'):
|
||||||
|
path = os.path.realpath(entry)
|
||||||
|
elif os.path.exists(f'/dev/disk/by-id/{entry}'):
|
||||||
|
path = os.path.realpath(f'/dev/disk/by-id/{entry}')
|
||||||
|
elif os.path.exists(f'/dev/{entry}'):
|
||||||
|
path = os.path.realpath(f'/dev/{entry}')
|
||||||
|
|
||||||
|
if path:
|
||||||
|
# Get parent disk (base disk without partition number)
|
||||||
|
result = subprocess.run(
|
||||||
|
['lsblk', '-no', 'PKNAME', path],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
return result.stdout.strip()
|
||||||
|
# If no parent, path is the base disk itself
|
||||||
|
return os.path.basename(path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_disks():
|
||||||
|
"""
|
||||||
|
Detect which physical disks are used by the system (Proxmox).
|
||||||
|
|
||||||
|
Returns a dict mapping disk names to their system usage info:
|
||||||
|
{
|
||||||
|
'sda': {'is_system': True, 'usage': ['root', 'boot']},
|
||||||
|
'nvme0n1': {'is_system': True, 'usage': ['zfs:rpool']},
|
||||||
|
}
|
||||||
|
|
||||||
|
Detects (matching logic from disk-passthrough.sh, format-disk.sh, disk_host.sh):
|
||||||
|
- Root filesystem (/) and critical system mounts (/boot, /usr, /var, etc.)
|
||||||
|
- Boot partition (/boot, /boot/efi)
|
||||||
|
- Active swap partitions
|
||||||
|
- ZFS pools (especially root pool - these disks are critical)
|
||||||
|
- LVM physical volumes (especially 'pve' volume group)
|
||||||
|
- RAID members (mdadm)
|
||||||
|
- Disks with Proxmox partition labels
|
||||||
|
"""
|
||||||
|
system_disks = {}
|
||||||
|
|
||||||
|
def add_usage(disk_name, usage_type):
|
||||||
|
"""Helper to add a usage type to a disk."""
|
||||||
|
if not disk_name:
|
||||||
|
return
|
||||||
|
# Normalize disk name (strip partition numbers to get base disk)
|
||||||
|
base_disk = disk_name
|
||||||
|
if disk_name and disk_name[-1].isdigit():
|
||||||
|
if 'nvme' in disk_name or 'mmcblk' in disk_name:
|
||||||
|
# NVMe/eMMC: nvme0n1p1 -> nvme0n1
|
||||||
|
if 'p' in disk_name:
|
||||||
|
base_disk = disk_name.rsplit('p', 1)[0]
|
||||||
|
else:
|
||||||
|
# SATA/SAS: sda1 -> sda
|
||||||
|
base_disk = disk_name.rstrip('0123456789')
|
||||||
|
|
||||||
|
if base_disk not in system_disks:
|
||||||
|
system_disks[base_disk] = {'is_system': True, 'usage': []}
|
||||||
|
if usage_type not in system_disks[base_disk]['usage']:
|
||||||
|
system_disks[base_disk]['usage'].append(usage_type)
|
||||||
|
|
||||||
|
# Get ZFS root pool first (critical - these disks should never be touched)
|
||||||
|
zfs_root_pool = _get_zfs_root_pool()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Check mounted filesystems for system-critical mounts
|
||||||
|
with open('/proc/mounts', 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
device, mountpoint = parts[0], parts[1]
|
||||||
|
|
||||||
|
# Skip non-block devices
|
||||||
|
if not device.startswith('/dev/'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
disk_name = device.replace('/dev/', '').replace('mapper/', '')
|
||||||
|
|
||||||
|
# Identify system mountpoints (expanded from bash _is_system_mount)
|
||||||
|
if _is_system_mount(mountpoint):
|
||||||
|
if mountpoint == '/':
|
||||||
|
add_usage(disk_name, 'root')
|
||||||
|
elif mountpoint.startswith('/boot'):
|
||||||
|
add_usage(disk_name, 'boot')
|
||||||
|
else:
|
||||||
|
add_usage(disk_name, 'system')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2. Check active swap partitions
|
||||||
|
result = subprocess.run(
|
||||||
|
['swapon', '--noheadings', '--raw', '--show=NAME'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
device = line.strip()
|
||||||
|
if device.startswith('/dev/'):
|
||||||
|
disk_name = device.replace('/dev/', '')
|
||||||
|
add_usage(disk_name, 'swap')
|
||||||
|
except Exception:
|
||||||
|
# Fallback to /proc/swaps
|
||||||
|
try:
|
||||||
|
with open('/proc/swaps', 'r') as f:
|
||||||
|
lines = f.readlines()[1:] # Skip header
|
||||||
|
for line in lines:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 1:
|
||||||
|
device = parts[0]
|
||||||
|
if device.startswith('/dev/'):
|
||||||
|
disk_name = device.replace('/dev/', '').replace('mapper/', '')
|
||||||
|
add_usage(disk_name, 'swap')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 3. Check ZFS pools using zpool list -v -H (matches bash _build_pool_disks)
|
||||||
|
result = subprocess.run(
|
||||||
|
['zpool', 'list', '-v', '-H'],
|
||||||
|
capture_output=True, text=True, timeout=8
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
current_pool = None
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if not parts:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# First column is pool name or device
|
||||||
|
entry = parts[0]
|
||||||
|
|
||||||
|
# Skip metadata entries
|
||||||
|
if entry in ('-', 'mirror', 'raidz', 'raidz1', 'raidz2', 'raidz3', 'spare', 'log', 'cache'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if this is a pool name (pools have no leading whitespace)
|
||||||
|
if not line.startswith('\t') and not line.startswith(' '):
|
||||||
|
current_pool = entry
|
||||||
|
continue
|
||||||
|
|
||||||
|
# This is a device entry - resolve it
|
||||||
|
base_disk = _resolve_zfs_entry(entry)
|
||||||
|
if base_disk:
|
||||||
|
pool_label = current_pool or 'unknown'
|
||||||
|
# Mark root pool specially
|
||||||
|
if zfs_root_pool and pool_label == zfs_root_pool:
|
||||||
|
add_usage(base_disk, f'zfs:{pool_label} (root)')
|
||||||
|
else:
|
||||||
|
add_usage(base_disk, f'zfs:{pool_label}')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 4. Check LVM physical volumes
|
||||||
|
result = subprocess.run(
|
||||||
|
['pvs', '--noheadings', '-o', 'pv_name,vg_name'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if line.strip():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
pv_device = parts[0]
|
||||||
|
vg_name = parts[1]
|
||||||
|
if pv_device.startswith('/dev/'):
|
||||||
|
# Resolve to real path
|
||||||
|
try:
|
||||||
|
real_path = os.path.realpath(pv_device)
|
||||||
|
disk_name = os.path.basename(real_path)
|
||||||
|
except Exception:
|
||||||
|
disk_name = pv_device.replace('/dev/', '')
|
||||||
|
|
||||||
|
# Proxmox typically uses 'pve' volume group
|
||||||
|
if 'pve' in vg_name.lower():
|
||||||
|
add_usage(disk_name, f'lvm:{vg_name} (pve)')
|
||||||
|
else:
|
||||||
|
add_usage(disk_name, f'lvm:{vg_name}')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 5. Check active RAID arrays
|
||||||
|
if os.path.exists('/proc/mdstat'):
|
||||||
|
with open('/proc/mdstat', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
if 'active' in content:
|
||||||
|
# Parse mdstat for active arrays
|
||||||
|
for line in content.split('\n'):
|
||||||
|
if 'active raid' in line:
|
||||||
|
# Extract device names from line like "md0 : active raid1 sda1[0] sdb1[1]"
|
||||||
|
parts = line.split()
|
||||||
|
for part in parts:
|
||||||
|
if '[' in part:
|
||||||
|
dev = part.split('[')[0]
|
||||||
|
add_usage(dev, 'raid')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 6. Check for disks with Proxmox/system partition labels
|
||||||
|
result = subprocess.run(
|
||||||
|
['lsblk', '-o', 'NAME,PARTLABEL', '-n', '-l'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
parts = line.split(None, 1)
|
||||||
|
if len(parts) >= 2:
|
||||||
|
disk_name = parts[0]
|
||||||
|
partlabel = parts[1].lower() if len(parts) > 1 else ''
|
||||||
|
# Proxmox-specific and system partition labels
|
||||||
|
system_labels = ['pve', 'proxmox', 'bios', 'esp', 'efi', 'boot', 'grub']
|
||||||
|
if any(label in partlabel for label in system_labels):
|
||||||
|
add_usage(disk_name, 'system-partition')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return system_disks
|
||||||
|
|
||||||
|
|
||||||
def get_storage_info():
|
def get_storage_info():
|
||||||
"""Get storage and disk information"""
|
"""Get storage and disk information"""
|
||||||
try:
|
try:
|
||||||
@@ -1802,6 +2066,9 @@ def get_storage_info():
|
|||||||
physical_disks = {}
|
physical_disks = {}
|
||||||
total_disk_size_bytes = 0
|
total_disk_size_bytes = 0
|
||||||
|
|
||||||
|
# Get system disk information (disks used by Proxmox)
|
||||||
|
system_disks = get_system_disks()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# List all block devices
|
# List all block devices
|
||||||
result = subprocess.run(['lsblk', '-b', '-d', '-n', '-o', 'NAME,SIZE,TYPE'],
|
result = subprocess.run(['lsblk', '-b', '-d', '-n', '-o', 'NAME,SIZE,TYPE'],
|
||||||
@@ -1843,6 +2110,11 @@ def get_storage_info():
|
|||||||
conn_type = get_disk_connection_type(disk_name)
|
conn_type = get_disk_connection_type(disk_name)
|
||||||
removable = is_disk_removable(disk_name)
|
removable = is_disk_removable(disk_name)
|
||||||
|
|
||||||
|
# Check if this disk is used by the system
|
||||||
|
sys_info = system_disks.get(disk_name, {})
|
||||||
|
is_system_disk = sys_info.get('is_system', False)
|
||||||
|
system_usage = sys_info.get('usage', [])
|
||||||
|
|
||||||
physical_disks[disk_name] = {
|
physical_disks[disk_name] = {
|
||||||
'name': disk_name,
|
'name': disk_name,
|
||||||
'size': disk_size_kb, # In KB for formatMemory() in Storage Summary
|
'size': disk_size_kb, # In KB for formatMemory() in Storage Summary
|
||||||
@@ -1866,6 +2138,8 @@ def get_storage_info():
|
|||||||
'ssd_life_left': smart_data.get('ssd_life_left'),
|
'ssd_life_left': smart_data.get('ssd_life_left'),
|
||||||
'connection_type': conn_type,
|
'connection_type': conn_type,
|
||||||
'removable': removable,
|
'removable': removable,
|
||||||
|
'is_system_disk': is_system_disk,
|
||||||
|
'system_usage': system_usage,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -6075,6 +6349,322 @@ def api_proxmox_storage():
|
|||||||
"""Get Proxmox storage information"""
|
"""Get Proxmox storage information"""
|
||||||
return jsonify(get_proxmox_storage())
|
return jsonify(get_proxmox_storage())
|
||||||
|
|
||||||
|
|
||||||
|
# ─── SMART Disk Testing API ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SMART_DIR = '/usr/local/share/proxmenux/smart'
|
||||||
|
|
||||||
|
def _is_nvme(disk_name):
|
||||||
|
"""Check if disk is NVMe."""
|
||||||
|
return disk_name.startswith('nvme')
|
||||||
|
|
||||||
|
def _get_smart_json_path(disk_name):
|
||||||
|
"""Get path to SMART JSON file for a disk."""
|
||||||
|
return os.path.join(SMART_DIR, f"{disk_name}.json")
|
||||||
|
|
||||||
|
def _ensure_smart_tools():
|
||||||
|
"""Check if SMART tools are installed."""
|
||||||
|
has_smartctl = shutil.which('smartctl') is not None
|
||||||
|
has_nvme = shutil.which('nvme') is not None
|
||||||
|
return {'smartctl': has_smartctl, 'nvme': has_nvme}
|
||||||
|
|
||||||
|
def _parse_smart_attributes(output_lines):
|
||||||
|
"""Parse SMART attributes from smartctl output."""
|
||||||
|
attributes = []
|
||||||
|
in_attrs = False
|
||||||
|
for line in output_lines:
|
||||||
|
if 'ID#' in line and 'ATTRIBUTE_NAME' in line:
|
||||||
|
in_attrs = True
|
||||||
|
continue
|
||||||
|
if in_attrs:
|
||||||
|
if not line.strip():
|
||||||
|
break
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 10 and parts[0].isdigit():
|
||||||
|
attr_id = int(parts[0])
|
||||||
|
attr_name = parts[1]
|
||||||
|
value = int(parts[3]) if parts[3].isdigit() else 0
|
||||||
|
worst = int(parts[4]) if parts[4].isdigit() else 0
|
||||||
|
threshold = int(parts[5]) if parts[5].isdigit() else 0
|
||||||
|
raw_value = parts[9] if len(parts) > 9 else ''
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
status = 'ok'
|
||||||
|
if threshold > 0 and value <= threshold:
|
||||||
|
status = 'critical'
|
||||||
|
elif threshold > 0 and value <= threshold + 10:
|
||||||
|
status = 'warning'
|
||||||
|
|
||||||
|
attributes.append({
|
||||||
|
'id': attr_id,
|
||||||
|
'name': attr_name,
|
||||||
|
'value': value,
|
||||||
|
'worst': worst,
|
||||||
|
'threshold': threshold,
|
||||||
|
'raw_value': raw_value,
|
||||||
|
'status': status
|
||||||
|
})
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
@app.route('/api/storage/smart/<disk_name>', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
|
def api_smart_status(disk_name):
|
||||||
|
"""Get SMART status and data for a specific disk."""
|
||||||
|
try:
|
||||||
|
# Validate disk name (security)
|
||||||
|
if not re.match(r'^[a-zA-Z0-9]+$', disk_name):
|
||||||
|
return jsonify({'error': 'Invalid disk name'}), 400
|
||||||
|
|
||||||
|
device = f'/dev/{disk_name}'
|
||||||
|
if not os.path.exists(device):
|
||||||
|
return jsonify({'error': 'Device not found'}), 404
|
||||||
|
|
||||||
|
tools = _ensure_smart_tools()
|
||||||
|
result = {
|
||||||
|
'status': 'idle',
|
||||||
|
'tools_installed': tools
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if tools are available
|
||||||
|
is_nvme = _is_nvme(disk_name)
|
||||||
|
if is_nvme and not tools['nvme']:
|
||||||
|
result['error'] = 'nvme-cli not installed'
|
||||||
|
return jsonify(result)
|
||||||
|
if not is_nvme and not tools['smartctl']:
|
||||||
|
result['error'] = 'smartmontools not installed'
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
# Check for existing JSON file (from previous test)
|
||||||
|
json_path = _get_smart_json_path(disk_name)
|
||||||
|
if os.path.exists(json_path):
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
saved_data = json.load(f)
|
||||||
|
result['saved_data'] = saved_data
|
||||||
|
result['saved_timestamp'] = os.path.getmtime(json_path)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get current SMART status
|
||||||
|
if is_nvme:
|
||||||
|
# NVMe: Check for running test
|
||||||
|
proc = subprocess.run(
|
||||||
|
['nvme', 'self-test-log', device],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if 'in progress' in proc.stdout.lower():
|
||||||
|
result['status'] = 'running'
|
||||||
|
result['test_type'] = 'nvme'
|
||||||
|
|
||||||
|
# Get smart-log data
|
||||||
|
proc = subprocess.run(
|
||||||
|
['nvme', 'smart-log', device],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
lines = proc.stdout.strip().split('\n')
|
||||||
|
smart_data = {}
|
||||||
|
for line in lines:
|
||||||
|
if ':' in line:
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
smart_data[key.strip().lower().replace(' ', '_')] = value.strip()
|
||||||
|
result['smart_data'] = smart_data
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
crit_warn = smart_data.get('critical_warning', '0')
|
||||||
|
result['smart_status'] = 'passed' if crit_warn == '0' else 'warning'
|
||||||
|
else:
|
||||||
|
# SATA/SAS: Check for running test
|
||||||
|
proc = subprocess.run(
|
||||||
|
['smartctl', '-c', device],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if 'Self-test routine in progress' in proc.stdout or '% of test remaining' in proc.stdout:
|
||||||
|
result['status'] = 'running'
|
||||||
|
# Extract progress percentage
|
||||||
|
match = re.search(r'(\d+)% of test remaining', proc.stdout)
|
||||||
|
if match:
|
||||||
|
result['progress'] = 100 - int(match.group(1))
|
||||||
|
|
||||||
|
# Get SMART health
|
||||||
|
proc = subprocess.run(
|
||||||
|
['smartctl', '-H', device],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if 'PASSED' in proc.stdout:
|
||||||
|
result['smart_status'] = 'passed'
|
||||||
|
elif 'FAILED' in proc.stdout:
|
||||||
|
result['smart_status'] = 'failed'
|
||||||
|
else:
|
||||||
|
result['smart_status'] = 'unknown'
|
||||||
|
|
||||||
|
# Get SMART attributes
|
||||||
|
proc = subprocess.run(
|
||||||
|
['smartctl', '-A', device],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
attrs = _parse_smart_attributes(proc.stdout.split('\n'))
|
||||||
|
result['smart_data'] = {'attributes': attrs}
|
||||||
|
|
||||||
|
# Get self-test log for last test result
|
||||||
|
proc = subprocess.run(
|
||||||
|
['smartctl', '-l', 'selftest', device],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
lines = proc.stdout.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('# ') or line.startswith('# '):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 5:
|
||||||
|
test_type = 'short' if 'Short' in line else 'long' if 'Extended' in line or 'Long' in line else 'unknown'
|
||||||
|
test_status = 'passed' if 'Completed without error' in line else 'failed'
|
||||||
|
result['last_test'] = {
|
||||||
|
'type': test_type,
|
||||||
|
'status': test_status,
|
||||||
|
'timestamp': ' '.join(parts[-5:-2]) if len(parts) > 5 else 'unknown'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return jsonify({'error': 'Command timeout'}), 504
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/storage/smart/<disk_name>/test', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def api_smart_run_test(disk_name):
|
||||||
|
"""Start a SMART self-test on a disk."""
|
||||||
|
try:
|
||||||
|
# Validate disk name (security)
|
||||||
|
if not re.match(r'^[a-zA-Z0-9]+$', disk_name):
|
||||||
|
return jsonify({'error': 'Invalid disk name'}), 400
|
||||||
|
|
||||||
|
device = f'/dev/{disk_name}'
|
||||||
|
if not os.path.exists(device):
|
||||||
|
return jsonify({'error': 'Device not found'}), 404
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
test_type = data.get('test_type', 'short')
|
||||||
|
|
||||||
|
if test_type not in ('short', 'long'):
|
||||||
|
return jsonify({'error': 'Invalid test type. Use "short" or "long"'}), 400
|
||||||
|
|
||||||
|
tools = _ensure_smart_tools()
|
||||||
|
is_nvme = _is_nvme(disk_name)
|
||||||
|
|
||||||
|
# Ensure SMART directory exists
|
||||||
|
os.makedirs(SMART_DIR, exist_ok=True)
|
||||||
|
json_path = _get_smart_json_path(disk_name)
|
||||||
|
|
||||||
|
if is_nvme:
|
||||||
|
if not tools['nvme']:
|
||||||
|
return jsonify({'error': 'nvme-cli not installed'}), 400
|
||||||
|
|
||||||
|
# NVMe: self-test-code 1=short, 2=long
|
||||||
|
code = 1 if test_type == 'short' else 2
|
||||||
|
proc = subprocess.run(
|
||||||
|
['nvme', 'device-self-test', device, f'--self-test-code={code}'],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
return jsonify({'error': f'Failed to start test: {proc.stderr}'}), 500
|
||||||
|
|
||||||
|
# For long test, start background monitor
|
||||||
|
if test_type == 'long':
|
||||||
|
subprocess.Popen(
|
||||||
|
f'''
|
||||||
|
while nvme device-self-test {device} --self-test-code=0 2>/dev/null | grep -qi 'in progress'; do
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
|
nvme smart-log -o json {device} > {json_path} 2>/dev/null
|
||||||
|
''',
|
||||||
|
shell=True, start_new_session=True,
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not tools['smartctl']:
|
||||||
|
return jsonify({'error': 'smartmontools not installed'}), 400
|
||||||
|
|
||||||
|
test_flag = '-t short' if test_type == 'short' else '-t long'
|
||||||
|
proc = subprocess.run(
|
||||||
|
['smartctl'] + test_flag.split() + [device],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if proc.returncode not in (0, 4): # 4 = test started successfully
|
||||||
|
return jsonify({'error': f'Failed to start test: {proc.stderr}'}), 500
|
||||||
|
|
||||||
|
# For long test, start background monitor
|
||||||
|
if test_type == 'long':
|
||||||
|
subprocess.Popen(
|
||||||
|
f'''
|
||||||
|
while smartctl -c {device} 2>/dev/null | grep -qiE 'Self-test routine in progress|[1-9][0-9]?% of test remaining'; do
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
|
smartctl --json=c {device} > {json_path} 2>/dev/null
|
||||||
|
''',
|
||||||
|
shell=True, start_new_session=True,
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'test_type': test_type,
|
||||||
|
'device': device,
|
||||||
|
'message': f'{test_type.capitalize()} test started on {device}'
|
||||||
|
})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return jsonify({'error': 'Command timeout'}), 504
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/storage/smart/tools', methods=['GET'])
|
||||||
|
@require_auth
|
||||||
|
def api_smart_tools_status():
|
||||||
|
"""Check if SMART tools are installed."""
|
||||||
|
tools = _ensure_smart_tools()
|
||||||
|
return jsonify(tools)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/storage/smart/tools/install', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def api_smart_tools_install():
|
||||||
|
"""Install SMART tools (smartmontools and nvme-cli)."""
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
packages = data.get('packages', ['smartmontools', 'nvme-cli'])
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for pkg in packages:
|
||||||
|
if pkg not in ('smartmontools', 'nvme-cli'):
|
||||||
|
results[pkg] = {'success': False, 'error': 'Invalid package name'}
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update apt cache and install
|
||||||
|
proc = subprocess.run(
|
||||||
|
['apt-get', 'install', '-y', pkg],
|
||||||
|
capture_output=True, text=True, timeout=120
|
||||||
|
)
|
||||||
|
results[pkg] = {
|
||||||
|
'success': proc.returncode == 0,
|
||||||
|
'output': proc.stdout if proc.returncode == 0 else proc.stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(results)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return jsonify({'error': 'Installation timeout'}), 504
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
# ─── END SMART API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/network', methods=['GET'])
|
@app.route('/api/network', methods=['GET'])
|
||||||
@require_auth
|
@require_auth
|
||||||
def api_network():
|
def api_network():
|
||||||
|
|||||||
@@ -678,6 +678,59 @@ TEMPLATES = {
|
|||||||
'group': 'storage',
|
'group': 'storage',
|
||||||
'default_enabled': True,
|
'default_enabled': True,
|
||||||
},
|
},
|
||||||
|
'smart_test_complete': {
|
||||||
|
'title': '{hostname}: SMART test completed — {device}',
|
||||||
|
'body': 'SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}',
|
||||||
|
'label': 'SMART test completed',
|
||||||
|
'group': 'storage',
|
||||||
|
'default_enabled': True,
|
||||||
|
},
|
||||||
|
'smart_test_failed': {
|
||||||
|
'title': '{hostname}: SMART test FAILED — {device}',
|
||||||
|
'body': 'SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}',
|
||||||
|
'label': 'SMART test FAILED',
|
||||||
|
'group': 'storage',
|
||||||
|
'default_enabled': True,
|
||||||
|
},
|
||||||
|
|
||||||
|
# ── GPU / PCIe passthrough events ──
|
||||||
|
'gpu_mode_switch': {
|
||||||
|
'title': '{hostname}: GPU mode changed to {new_mode}',
|
||||||
|
'body': (
|
||||||
|
'GPU passthrough mode has been switched.\n'
|
||||||
|
'GPU: {gpu_name} ({gpu_pci})\n'
|
||||||
|
'Previous mode: {old_mode}\n'
|
||||||
|
'New mode: {new_mode}\n'
|
||||||
|
'{details}'
|
||||||
|
),
|
||||||
|
'label': 'GPU mode switched',
|
||||||
|
'group': 'hardware',
|
||||||
|
'default_enabled': True,
|
||||||
|
},
|
||||||
|
'gpu_passthrough_blocked': {
|
||||||
|
'title': '{hostname}: {guest_type} {guest_id} blocked at startup',
|
||||||
|
'body': (
|
||||||
|
'PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\n'
|
||||||
|
'Reason: {reason}\n'
|
||||||
|
'{details}'
|
||||||
|
),
|
||||||
|
'label': 'GPU passthrough blocked',
|
||||||
|
'group': 'hardware',
|
||||||
|
'default_enabled': True,
|
||||||
|
},
|
||||||
|
'pci_passthrough_conflict': {
|
||||||
|
'title': '{hostname}: PCIe device conflict detected',
|
||||||
|
'body': (
|
||||||
|
'A PCIe device is assigned to multiple guests.\n'
|
||||||
|
'Device: {device_pci}\n'
|
||||||
|
'Conflicting guests: {guest_list}\n'
|
||||||
|
'Action required: Stop one of the guests or reassign the device.'
|
||||||
|
),
|
||||||
|
'label': 'PCIe device conflict',
|
||||||
|
'group': 'hardware',
|
||||||
|
'default_enabled': True,
|
||||||
|
},
|
||||||
|
|
||||||
'load_high': {
|
'load_high': {
|
||||||
'title': '{hostname}: High system load — {value}',
|
'title': '{hostname}: High system load — {value}',
|
||||||
'body': 'System load average is {value} on {cores} cores.\n{details}',
|
'body': 'System load average is {value} on {cores} cores.\n{details}',
|
||||||
@@ -1203,6 +1256,7 @@ CATEGORY_EMOJI = {
|
|||||||
'services': '\u2699\uFE0F', # gear
|
'services': '\u2699\uFE0F', # gear
|
||||||
'health': '\U0001FA7A', # stethoscope
|
'health': '\U0001FA7A', # stethoscope
|
||||||
'updates': '\U0001F504', # counterclockwise arrows (update)
|
'updates': '\U0001F504', # counterclockwise arrows (update)
|
||||||
|
'hardware': '\U0001F3AE', # video game controller (GPU/PCIe hardware)
|
||||||
'other': '\U0001F4E8', # incoming envelope
|
'other': '\U0001F4E8', # incoming envelope
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,6 +1329,10 @@ EVENT_EMOJI = {
|
|||||||
'proxmenux_update': '\U0001F195', # NEW
|
'proxmenux_update': '\U0001F195', # NEW
|
||||||
# AI
|
# AI
|
||||||
'ai_model_migrated': '\U0001F504', # arrows counterclockwise (refresh/update)
|
'ai_model_migrated': '\U0001F504', # arrows counterclockwise (refresh/update)
|
||||||
|
# GPU / PCIe
|
||||||
|
'gpu_mode_switch': '\U0001F3AE', # video game controller (represents GPU)
|
||||||
|
'gpu_passthrough_blocked': '\U0001F6AB', # prohibited sign (blocked)
|
||||||
|
'pci_passthrough_conflict': '\u26A0\uFE0F', # warning triangle (conflict)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Decorative field-level icons for body text enrichment
|
# Decorative field-level icons for body text enrichment
|
||||||
|
|||||||
385
scripts/global/disk_ops_helpers.sh
Normal file
385
scripts/global/disk_ops_helpers.sh
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ProxMenux - Disk Operations Helpers
|
||||||
|
# ==========================================================
|
||||||
|
# Author : MacRimi
|
||||||
|
# Copyright : (c) 2024 MacRimi
|
||||||
|
# License : MIT
|
||||||
|
# Version : 1.0
|
||||||
|
# Last Updated: 11/04/2026
|
||||||
|
# ==========================================================
|
||||||
|
# Shared low-level disk operations: wipe, partition, format.
|
||||||
|
# Consumed by format-disk.sh, disk_host.sh and future scripts.
|
||||||
|
#
|
||||||
|
# Output variables (set by helpers, read by callers):
|
||||||
|
# DOH_CREATED_PARTITION — partition path set by doh_create_partition()
|
||||||
|
# DOH_PARTITION_ERROR_DETAIL — error detail set by doh_create_partition()
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
if [[ -n "${__PROXMENUX_DISK_OPS_HELPERS__}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
__PROXMENUX_DISK_OPS_HELPERS__=1
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034 # these are output variables read by callers (format-disk.sh, disk_host.sh)
|
||||||
|
DOH_CREATED_PARTITION=""
|
||||||
|
DOH_PARTITION_ERROR_DETAIL=""
|
||||||
|
DOH_FORMAT_ERROR_DETAIL=""
|
||||||
|
DOH_WIPE_ERROR_DETAIL=""
|
||||||
|
|
||||||
|
# Internal: print progress lines only when explicitly enabled by caller.
|
||||||
|
# Enabled with: export DOH_SHOW_PROGRESS=1
|
||||||
|
_doh_progress() {
|
||||||
|
[[ "${DOH_SHOW_PROGRESS:-0}" == "1" ]] || return 0
|
||||||
|
echo -e "${TAB}${YW}${HOLD}$*${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal: collect command stdout with timeout protection (best-effort).
|
||||||
|
# Usage: _doh_collect_cmd <seconds> <cmd> [args...]
|
||||||
|
_doh_collect_cmd() {
|
||||||
|
local seconds="$1"
|
||||||
|
shift
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout --kill-after=2 "${seconds}s" "$@" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
"$@" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal: run a command with a timeout, suppressing all output including
|
||||||
|
# the bash "Killed" job notification that leaks when --kill-after re-raises
|
||||||
|
# SIGKILL. Plain SIGTERM is not enough for processes stuck in kernel D-state
|
||||||
|
# (uninterruptible I/O wait on a busy ZFS/LVM disk), so --kill-after=2 is
|
||||||
|
# needed. The notification is suppressed by temporarily redirecting the
|
||||||
|
# current shell's stderr with exec before the call and restoring it after.
|
||||||
|
# Usage: _doh_run_quick_cmd <seconds> <cmd> [args...]
|
||||||
|
_doh_run_quick_cmd() {
|
||||||
|
local seconds="$1"
|
||||||
|
shift
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
local _saved_stderr
|
||||||
|
exec {_saved_stderr}>&2 2>/dev/null
|
||||||
|
timeout --kill-after=2 "${seconds}s" "$@" >/dev/null 2>&1
|
||||||
|
local rc=$?
|
||||||
|
exec 2>&"${_saved_stderr}" {_saved_stderr}>&-
|
||||||
|
return $rc
|
||||||
|
fi
|
||||||
|
"$@" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal: unmount all ZFS datasets then export (or destroy) any ZFS pools
|
||||||
|
# whose vdevs live on <disk>. Called at the very start of doh_wipe_disk so
|
||||||
|
# ZFS fully releases the device before wipefs/sgdisk/partprobe touch it.
|
||||||
|
# If the pool is still held after export, processes on it will be in D-state
|
||||||
|
# and --kill-after in _doh_run_quick_cmd handles the force-kill.
|
||||||
|
_doh_release_zfs_pools() {
|
||||||
|
local disk="$1"
|
||||||
|
command -v zpool >/dev/null 2>&1 || return 0
|
||||||
|
|
||||||
|
local pool_name dev resolved base parent
|
||||||
|
while read -r pool_name; do
|
||||||
|
[[ -z "$pool_name" ]] && continue
|
||||||
|
local found=false
|
||||||
|
while read -r dev; do
|
||||||
|
[[ -z "$dev" ]] && continue
|
||||||
|
if [[ "$dev" == /dev/* ]]; then
|
||||||
|
resolved=$(readlink -f "$dev" 2>/dev/null)
|
||||||
|
elif [[ -e "/dev/disk/by-id/$dev" ]]; then
|
||||||
|
resolved=$(readlink -f "/dev/disk/by-id/$dev" 2>/dev/null)
|
||||||
|
elif [[ -e "/dev/$dev" ]]; then
|
||||||
|
resolved=$(readlink -f "/dev/$dev" 2>/dev/null)
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ -z "$resolved" ]] && continue
|
||||||
|
base=$(lsblk -no PKNAME "$resolved" 2>/dev/null)
|
||||||
|
parent="${base:+/dev/$base}"
|
||||||
|
[[ -z "$parent" ]] && parent="$resolved"
|
||||||
|
if [[ "$parent" == "$disk" || "$resolved" == "$disk" ]]; then
|
||||||
|
found=true; break
|
||||||
|
fi
|
||||||
|
done < <(_doh_collect_cmd 12 zpool list -v -H "$pool_name" | awk '{print $1}' | \
|
||||||
|
grep -v '^-' | grep -v '^mirror' | grep -v '^raidz' | \
|
||||||
|
grep -v "^${pool_name}$")
|
||||||
|
if $found; then
|
||||||
|
_doh_progress "- Releasing active ZFS pool: $pool_name"
|
||||||
|
# Unmount all datasets (reverse order: deepest first)
|
||||||
|
if command -v zfs >/dev/null 2>&1; then
|
||||||
|
while read -r ds; do
|
||||||
|
[[ -z "$ds" ]] && continue
|
||||||
|
timeout 10s zfs unmount -f "$ds" >/dev/null 2>&1 || true
|
||||||
|
done < <(_doh_collect_cmd 10 zfs list -H -o name -r "$pool_name" | sort -r)
|
||||||
|
fi
|
||||||
|
# Export the pool so the kernel releases the block device
|
||||||
|
timeout 30s zpool export -f "$pool_name" >/dev/null 2>&1 || true
|
||||||
|
# Wait for udev to finish processing the device release
|
||||||
|
udevadm settle --timeout=5 >/dev/null 2>&1 || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
done < <(_doh_collect_cmd 8 zpool list -H -o name)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Internal: run a partitioning command with timeout, appending combined output to a file.
|
||||||
|
# Usage: _doh_part_cmd <seconds> <outfile> <cmd> [args...]
|
||||||
|
_doh_part_cmd() {
|
||||||
|
local secs="$1" outfile="$2"
|
||||||
|
shift 2
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout --kill-after=3 "${secs}s" "$@" >>"$outfile" 2>&1
|
||||||
|
else
|
||||||
|
"$@" >>"$outfile" 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# doh_wipe_disk <disk>
|
||||||
|
# Unmounts all partitions, deactivates swap, wipes all filesystem metadata
|
||||||
|
# and partition tables (wipefs + sgdisk + dd first/last 16 MiB).
|
||||||
|
# Never fails — all sub-commands run with "|| true".
|
||||||
|
doh_wipe_disk() {
|
||||||
|
local disk="$1"
|
||||||
|
local node mountpoint total_sectors seek_sectors discard_max base
|
||||||
|
|
||||||
|
DOH_WIPE_ERROR_DETAIL=""
|
||||||
|
_doh_progress "[1/8] Preparing disk $disk"
|
||||||
|
|
||||||
|
# Optional heavy release flow (disabled by default to avoid hangs in busy hosts).
|
||||||
|
if [[ "${DOH_ENABLE_STACK_RELEASE:-0}" == "1" ]]; then
|
||||||
|
# Release any ZFS pools using this disk so the kernel lets go of it
|
||||||
|
_doh_release_zfs_pools "$disk"
|
||||||
|
|
||||||
|
# Deactivate any LVM VGs backed by this disk
|
||||||
|
if command -v vgchange >/dev/null 2>&1; then
|
||||||
|
local pv rp vg
|
||||||
|
while read -r pv; do
|
||||||
|
rp=$(readlink -f "$pv" 2>/dev/null)
|
||||||
|
base=$(lsblk -no PKNAME "${rp:-$pv}" 2>/dev/null)
|
||||||
|
if [[ "/dev/${base}" == "$disk" || "$rp" == "$disk" ]]; then
|
||||||
|
vg=$(_doh_collect_cmd 8 pvs --noheadings -o vg_name "${rp:-$pv}" | xargs)
|
||||||
|
[[ -n "$vg" ]] && _doh_run_quick_cmd 8 vgchange -an "$vg" || true
|
||||||
|
fi
|
||||||
|
done < <(_doh_collect_cmd 8 pvs --noheadings -o pv_name | xargs -r -n1)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unmount all partitions
|
||||||
|
_doh_progress "[2/8] Unmounting partitions"
|
||||||
|
while read -r node mountpoint; do
|
||||||
|
[[ -z "$node" || -z "$mountpoint" ]] && continue
|
||||||
|
_doh_run_quick_cmd 8 umount -f "$node" || true
|
||||||
|
done < <(lsblk -lnpo NAME,MOUNTPOINT "$disk" 2>/dev/null | awk 'NR>1 && $2!="" {print $1" "$2}')
|
||||||
|
|
||||||
|
# Deactivate swap
|
||||||
|
_doh_progress "[3/8] Disabling swap signatures"
|
||||||
|
while read -r node; do
|
||||||
|
[[ -z "$node" ]] && continue
|
||||||
|
_doh_run_quick_cmd 8 swapoff "$node" || true
|
||||||
|
done < <(lsblk -lnpo NAME "$disk" 2>/dev/null | awk 'NR>1 {print $1}')
|
||||||
|
|
||||||
|
# Wipe filesystem signatures and RAID superblocks on every node
|
||||||
|
_doh_progress "[4/8] Removing filesystem/RAID signatures"
|
||||||
|
while read -r node; do
|
||||||
|
[[ -z "$node" ]] && continue
|
||||||
|
_doh_run_quick_cmd 10 wipefs -a -f "$node" || true
|
||||||
|
if command -v mdadm >/dev/null 2>&1; then
|
||||||
|
_doh_run_quick_cmd 8 mdadm --zero-superblock --force "$node" || true
|
||||||
|
fi
|
||||||
|
done < <(lsblk -lnpo NAME "$disk" 2>/dev/null)
|
||||||
|
|
||||||
|
# Zap partition table
|
||||||
|
_doh_progress "[5/8] Resetting partition table"
|
||||||
|
_doh_run_quick_cmd 12 sgdisk --zap-all "$disk" || true
|
||||||
|
|
||||||
|
# TRIM/discard if device supports it
|
||||||
|
_doh_progress "[6/8] Attempting discard/TRIM when supported"
|
||||||
|
discard_max=$(lsblk -dn -o DISC-MAX "$disk" 2>/dev/null | xargs)
|
||||||
|
if [[ -n "$discard_max" && "$discard_max" != "0B" && "$discard_max" != "0" ]]; then
|
||||||
|
_doh_run_quick_cmd 15 blkdiscard -f "$disk" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Zero first 16 MiB (destroys partition table / filesystem headers)
|
||||||
|
_doh_progress "[7/8] Zeroing first metadata region"
|
||||||
|
_doh_run_quick_cmd 20 dd if=/dev/zero of="$disk" bs=1M count=16 conv=fsync status=none || true
|
||||||
|
|
||||||
|
# Zero last 16 MiB (destroys backup GPT header)
|
||||||
|
_doh_progress "[8/8] Zeroing backup GPT region"
|
||||||
|
total_sectors=$(blockdev --getsz "$disk" 2>/dev/null || echo 0)
|
||||||
|
if [[ "$total_sectors" =~ ^[0-9]+$ ]] && (( total_sectors > 32768 )); then
|
||||||
|
seek_sectors=$(( total_sectors - 32768 ))
|
||||||
|
_doh_run_quick_cmd 20 dd if=/dev/zero of="$disk" bs=512 seek="$seek_sectors" count=32768 conv=fsync status=none || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
udevadm settle --timeout=10 >/dev/null 2>&1 || true
|
||||||
|
_doh_run_quick_cmd 8 partprobe "$disk" || true
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# doh_create_partition <disk>
|
||||||
|
# Creates a single GPT partition spanning the whole disk.
|
||||||
|
# Tries parted → sgdisk → sfdisk in order; stops at first success.
|
||||||
|
#
|
||||||
|
# On success: sets DOH_CREATED_PARTITION to the new partition path, returns 0.
|
||||||
|
# On failure: sets DOH_PARTITION_ERROR_DETAIL with tool diagnostics, returns 1.
|
||||||
|
doh_create_partition() {
|
||||||
|
local disk="$1"
|
||||||
|
local created=false tmp_out err_snippet
|
||||||
|
|
||||||
|
DOH_CREATED_PARTITION=""
|
||||||
|
DOH_PARTITION_ERROR_DETAIL=""
|
||||||
|
|
||||||
|
_doh_run_quick_cmd 5 blockdev --setrw "$disk" || true
|
||||||
|
|
||||||
|
# --- attempt 1: parted ---
|
||||||
|
if command -v parted >/dev/null 2>&1; then
|
||||||
|
tmp_out=$(mktemp)
|
||||||
|
if _doh_part_cmd 15 "$tmp_out" parted -s -f "$disk" mklabel gpt; then
|
||||||
|
if _doh_part_cmd 20 "$tmp_out" parted -s -f "$disk" mkpart primary 1MiB 100%; then
|
||||||
|
created=true
|
||||||
|
else
|
||||||
|
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="parted mkpart: ${err_snippet:-no details}"$'\n'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="parted mklabel: ${err_snippet:-no details}"$'\n'
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_out"
|
||||||
|
else
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="parted command not found"$'\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- attempt 2: sgdisk ---
|
||||||
|
if [[ "$created" != "true" ]] && command -v sgdisk >/dev/null 2>&1; then
|
||||||
|
tmp_out=$(mktemp)
|
||||||
|
_doh_run_quick_cmd 10 sgdisk --zap-all "$disk" || true
|
||||||
|
# sgdisk does not accept "1MiB" notation — use sector 2048 (= 1 MiB at 512 B/sector)
|
||||||
|
if _doh_part_cmd 20 "$tmp_out" sgdisk -o -n 1:2048:0 -t 1:8300 "$disk"; then
|
||||||
|
created=true
|
||||||
|
else
|
||||||
|
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="sgdisk create: ${err_snippet:-no details}"$'\n'
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_out"
|
||||||
|
elif [[ "$created" != "true" ]]; then
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="sgdisk command not found"$'\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- attempt 3: sfdisk ---
|
||||||
|
if [[ "$created" != "true" ]] && command -v sfdisk >/dev/null 2>&1; then
|
||||||
|
tmp_out=$(mktemp)
|
||||||
|
local sfdisk_ok=1
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
printf 'label: gpt\n,;\n' | timeout --kill-after=3 20s sfdisk --wipe always "$disk" >>"$tmp_out" 2>&1
|
||||||
|
sfdisk_ok=$?
|
||||||
|
else
|
||||||
|
printf 'label: gpt\n,;\n' | sfdisk --wipe always "$disk" >>"$tmp_out" 2>&1
|
||||||
|
sfdisk_ok=$?
|
||||||
|
fi
|
||||||
|
if [[ $sfdisk_ok -eq 0 ]]; then
|
||||||
|
created=true
|
||||||
|
else
|
||||||
|
err_snippet=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="sfdisk create: ${err_snippet:-no details}"$'\n'
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_out"
|
||||||
|
elif [[ "$created" != "true" ]]; then
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="sfdisk command not found"$'\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$created" == "true" ]] || return 1
|
||||||
|
|
||||||
|
# Wait for the kernel to expose the new partition node
|
||||||
|
udevadm settle --timeout=10 >/dev/null 2>&1 || true
|
||||||
|
_doh_run_quick_cmd 8 partprobe "$disk" || true
|
||||||
|
|
||||||
|
local part
|
||||||
|
for _ in {1..15}; do
|
||||||
|
sleep 0.3
|
||||||
|
part=$(lsblk -lnpo NAME "$disk" 2>/dev/null | awk 'NR==2{print; exit}')
|
||||||
|
if [[ -n "$part" && -b "$part" ]]; then
|
||||||
|
DOH_CREATED_PARTITION="$part"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fallback: derive partition name from disk path (handles NVMe p-suffix)
|
||||||
|
local fallback
|
||||||
|
if [[ "$disk" =~ [0-9]$ ]]; then
|
||||||
|
fallback="${disk}p1"
|
||||||
|
else
|
||||||
|
fallback="${disk}1"
|
||||||
|
fi
|
||||||
|
if [[ -b "$fallback" ]]; then
|
||||||
|
DOH_CREATED_PARTITION="$fallback"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOH_PARTITION_ERROR_DETAIL+="partition node not detected after table refresh"$'\n'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# doh_format_partition <partition> <filesystem> [label] [zfs_pool_name] [zfs_mountpoint]
|
||||||
|
#
|
||||||
|
# Formats <partition> with <filesystem>.
|
||||||
|
# label : optional FS label for ext4/xfs/btrfs (ignored for ZFS)
|
||||||
|
# zfs_pool_name : required when filesystem=zfs; defaults to label if empty
|
||||||
|
# zfs_mountpoint : ZFS pool mountpoint (default: "none" — no automatic mount)
|
||||||
|
#
|
||||||
|
# On failure: sets DOH_FORMAT_ERROR_DETAIL with tool diagnostics.
|
||||||
|
# Returns 0 on success, 1 on failure.
|
||||||
|
doh_format_partition() {
|
||||||
|
local partition="$1"
|
||||||
|
local filesystem="$2"
|
||||||
|
local label="${3:-}"
|
||||||
|
local zfs_pool="${4:-}"
|
||||||
|
local zfs_mountpoint="${5:-none}"
|
||||||
|
local tmp_out rc=1
|
||||||
|
|
||||||
|
DOH_FORMAT_ERROR_DETAIL=""
|
||||||
|
tmp_out=$(mktemp)
|
||||||
|
|
||||||
|
case "$filesystem" in
|
||||||
|
ext4)
|
||||||
|
if [[ -n "$label" ]]; then
|
||||||
|
mkfs.ext4 -F -L "$label" "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
else
|
||||||
|
mkfs.ext4 -F "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
xfs)
|
||||||
|
if [[ -n "$label" ]]; then
|
||||||
|
mkfs.xfs -f -L "$label" "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
else
|
||||||
|
mkfs.xfs -f "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
exfat)
|
||||||
|
mkfs.exfat "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
;;
|
||||||
|
btrfs)
|
||||||
|
if [[ -n "$label" ]]; then
|
||||||
|
mkfs.btrfs -f -L "$label" "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
else
|
||||||
|
mkfs.btrfs -f "$partition" >"$tmp_out" 2>&1; rc=$?
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
zfs)
|
||||||
|
[[ -z "$zfs_pool" ]] && zfs_pool="${label:-pool}"
|
||||||
|
zpool labelclear -f "$partition" >/dev/null 2>&1 || true
|
||||||
|
zpool create -f -o ashift=12 \
|
||||||
|
-O compression=lz4 -O atime=off -O xattr=sa -O acltype=posixacl \
|
||||||
|
-m "$zfs_mountpoint" "$zfs_pool" "$partition" >"$tmp_out" 2>&1
|
||||||
|
rc=$?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown filesystem: $filesystem" >"$tmp_out"
|
||||||
|
rc=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ $rc -ne 0 ]]; then
|
||||||
|
DOH_FORMAT_ERROR_DETAIL=$(tr '\n' ' ' <"$tmp_out" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_out"
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
@@ -222,9 +222,9 @@ attach_proxmenux_gpu_guard_to_vm() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if qm set "$vmid" --hookscript "$PROXMENUX_GPU_HOOK_STORAGE_REF" >/dev/null 2>&1; then
|
if qm set "$vmid" --hookscript "$PROXMENUX_GPU_HOOK_STORAGE_REF" >/dev/null 2>&1; then
|
||||||
_gpu_guard_msg_ok "GPU guard hook attached to VM ${vmid}"
|
_gpu_guard_msg_ok "PCIe passthrough guard attached to VM ${vmid}"
|
||||||
else
|
else
|
||||||
_gpu_guard_msg_warn "Could not attach GPU guard hook to VM ${vmid}. Ensure 'local' storage supports snippets."
|
_gpu_guard_msg_warn "Could not attach PCIe passthrough guard to VM ${vmid}. Ensure 'local' storage supports snippets."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +239,9 @@ attach_proxmenux_gpu_guard_to_lxc() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if pct set "$ctid" -hookscript "$PROXMENUX_GPU_HOOK_STORAGE_REF" >/dev/null 2>&1; then
|
if pct set "$ctid" -hookscript "$PROXMENUX_GPU_HOOK_STORAGE_REF" >/dev/null 2>&1; then
|
||||||
_gpu_guard_msg_ok "GPU guard hook attached to LXC ${ctid}"
|
_gpu_guard_msg_ok "PCIe passthrough guard attached to LXC ${ctid}"
|
||||||
else
|
else
|
||||||
_gpu_guard_msg_warn "Could not attach GPU guard hook to LXC ${ctid}. Ensure 'local' storage supports snippets."
|
_gpu_guard_msg_warn "Could not attach PCIe passthrough guard to LXC ${ctid}. Ensure 'local' storage supports snippets."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,66 @@ function _array_contains() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _vm_boot_order_add_unique() {
|
||||||
|
local arr_name="$1"
|
||||||
|
shift
|
||||||
|
local -n arr_ref="$arr_name"
|
||||||
|
local entry
|
||||||
|
for entry in "$@"; do
|
||||||
|
[[ -z "$entry" ]] && continue
|
||||||
|
_array_contains "$entry" "${arr_ref[@]}" || arr_ref+=("$entry")
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function _vm_boot_order_join() {
|
||||||
|
local -a unique_entries=()
|
||||||
|
local entry
|
||||||
|
for entry in "$@"; do
|
||||||
|
[[ -z "$entry" ]] && continue
|
||||||
|
_array_contains "$entry" "${unique_entries[@]}" || unique_entries+=("$entry")
|
||||||
|
done
|
||||||
|
[[ ${#unique_entries[@]} -gt 0 ]] || return 0
|
||||||
|
local joined
|
||||||
|
joined=$(IFS=';'; echo "${unique_entries[*]}")
|
||||||
|
echo "$joined"
|
||||||
|
}
|
||||||
|
|
||||||
|
function _vm_boot_order_hostpci_entries_for_pcis() {
|
||||||
|
local vmid="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
local cfg
|
||||||
|
cfg=$(qm config "$vmid" 2>/dev/null || true)
|
||||||
|
[[ -n "$cfg" ]] || return 0
|
||||||
|
|
||||||
|
local -a hostpci_entries=()
|
||||||
|
local pci bdf bdf_re slot_base slot_re line entry
|
||||||
|
|
||||||
|
for pci in "$@"; do
|
||||||
|
[[ -n "$pci" ]] || continue
|
||||||
|
bdf="${pci#0000:}"
|
||||||
|
bdf_re="${bdf//./\\.}"
|
||||||
|
|
||||||
|
line=$(grep -E "^hostpci[0-9]+:.*(0000:)?${bdf_re}([,[:space:]]|$)" <<< "$cfg" | head -n1)
|
||||||
|
if [[ -z "$line" ]]; then
|
||||||
|
slot_base="${bdf%.*}"
|
||||||
|
slot_re="${slot_base//./\\.}"
|
||||||
|
line=$(grep -E "^hostpci[0-9]+:.*(0000:)?${slot_re}(\\.[0-7])?([,[:space:]]|$)" <<< "$cfg" | head -n1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$line" ]] || continue
|
||||||
|
entry="${line%%:*}"
|
||||||
|
_array_contains "$entry" "${hostpci_entries[@]}" || hostpci_entries+=("$entry")
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "${hostpci_entries[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function _vmids_scope_key() {
|
||||||
|
[[ "$#" -eq 0 ]] && { echo ""; return 0; }
|
||||||
|
printf '%s\n' "$@" | awk 'NF' | sort -u | paste -sd',' -
|
||||||
|
}
|
||||||
|
|
||||||
function _refresh_host_storage_cache() {
|
function _refresh_host_storage_cache() {
|
||||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
||||||
SWAP_DISKS=$(swapon --noheadings --raw --show=NAME 2>/dev/null)
|
SWAP_DISKS=$(swapon --noheadings --raw --show=NAME 2>/dev/null)
|
||||||
@@ -23,17 +83,24 @@ function _refresh_host_storage_cache() {
|
|||||||
|
|
||||||
ZFS_DISKS=""
|
ZFS_DISKS=""
|
||||||
local zfs_raw entry path base_disk
|
local zfs_raw entry path base_disk
|
||||||
zfs_raw=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
zfs_raw=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror' | grep -v '^raidz')
|
||||||
for entry in $zfs_raw; do
|
for entry in $zfs_raw; do
|
||||||
path=""
|
path=""
|
||||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
if [[ "$entry" == /dev/* ]]; then
|
||||||
[[ -e "/dev/disk/by-id/$entry" ]] && path=$(readlink -f "/dev/disk/by-id/$entry")
|
path=$(readlink -f "$entry" 2>/dev/null)
|
||||||
elif [[ "$entry" == /dev/* ]]; then
|
elif [[ -e "/dev/disk/by-id/$entry" ]]; then
|
||||||
path="$entry"
|
path=$(readlink -f "/dev/disk/by-id/$entry" 2>/dev/null)
|
||||||
|
elif [[ -e "/dev/$entry" ]]; then
|
||||||
|
path=$(readlink -f "/dev/$entry" 2>/dev/null)
|
||||||
fi
|
fi
|
||||||
if [[ -n "$path" ]]; then
|
if [[ -n "$path" ]]; then
|
||||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
||||||
[[ -n "$base_disk" ]] && ZFS_DISKS+="/dev/$base_disk"$'\n'
|
if [[ -n "$base_disk" ]]; then
|
||||||
|
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
||||||
|
else
|
||||||
|
# Whole-disk vdev — path is already the resolved disk itself
|
||||||
|
ZFS_DISKS+="$path"$'\n'
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
||||||
@@ -77,7 +144,7 @@ function _disk_is_host_system_used() {
|
|||||||
DISK_USAGE_REASON="$(translate "Disk is part of host LVM")"
|
DISK_USAGE_REASON="$(translate "Disk is part of host LVM")"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ -n "$ZFS_DISKS" && "$ZFS_DISKS" == *"$disk"* ]]; then
|
if [[ -n "$ZFS_DISKS" ]] && grep -qFx "$disk" <<< "$ZFS_DISKS"; then
|
||||||
DISK_USAGE_REASON="$(translate "Disk is part of a host ZFS pool")"
|
DISK_USAGE_REASON="$(translate "Disk is part of a host ZFS pool")"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -86,23 +153,181 @@ function _disk_is_host_system_used() {
|
|||||||
|
|
||||||
function _disk_used_in_guest_configs() {
|
function _disk_used_in_guest_configs() {
|
||||||
local disk="$1"
|
local disk="$1"
|
||||||
local real_path
|
local real_path escaped
|
||||||
real_path=$(readlink -f "$disk" 2>/dev/null)
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
|
||||||
if [[ -n "$real_path" ]] && grep -Fq "$real_path" <<< "$CONFIG_DATA"; then
|
# Use boundary matching: path must be followed by comma, whitespace, or EOL
|
||||||
return 0
|
# This prevents /dev/sdb from falsely matching /dev/sdb1 or /dev/sdb2
|
||||||
|
if [[ -n "$real_path" ]]; then
|
||||||
|
escaped="${real_path//./\\.}"
|
||||||
|
if grep -qE "${escaped}(,|[[:space:]]|$)" <<< "$CONFIG_DATA"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local symlink
|
local symlink symlink_escaped
|
||||||
for symlink in /dev/disk/by-id/*; do
|
for symlink in /dev/disk/by-id/*; do
|
||||||
[[ -e "$symlink" ]] || continue
|
[[ -e "$symlink" ]] || continue
|
||||||
if [[ "$(readlink -f "$symlink")" == "$real_path" ]] && grep -Fq "$symlink" <<< "$CONFIG_DATA"; then
|
[[ "$(readlink -f "$symlink")" == "$real_path" ]] || continue
|
||||||
|
symlink_escaped="${symlink//./\\.}"
|
||||||
|
if grep -qE "${symlink_escaped}(,|[[:space:]]|$)" <<< "$CONFIG_DATA"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Returns 0 if the disk is referenced in a RUNNING VM or CT config.
|
||||||
|
# Mirrors _disk_used_in_guest_configs but checks guest status per-file.
|
||||||
|
function _disk_used_in_running_guest() {
|
||||||
|
local disk="$1"
|
||||||
|
local real_path
|
||||||
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
|
||||||
|
local -a aliases=()
|
||||||
|
[[ -n "$disk" ]] && aliases+=("$disk")
|
||||||
|
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||||
|
local symlink
|
||||||
|
for symlink in /dev/disk/by-id/*; do
|
||||||
|
[[ -e "$symlink" ]] || continue
|
||||||
|
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||||
|
done
|
||||||
|
|
||||||
|
local conf vmid alias escaped
|
||||||
|
for conf in /etc/pve/qemu-server/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
vmid=$(basename "$conf" .conf)
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
escaped="${alias//./\\.}"
|
||||||
|
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||||
|
if qm status "$vmid" 2>/dev/null | grep -q "status: running"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
local ctid
|
||||||
|
for conf in /etc/pve/lxc/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
ctid=$(basename "$conf" .conf)
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
escaped="${alias//./\\.}"
|
||||||
|
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||||
|
if pct status "$ctid" 2>/dev/null | grep -q "status: running"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints "VM:VMID" or "CT:CTID" for each stopped guest that references the disk.
|
||||||
|
function _disk_guest_ids() {
|
||||||
|
local disk="$1"
|
||||||
|
local real_path
|
||||||
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
|
||||||
|
local -a aliases=()
|
||||||
|
[[ -n "$disk" ]] && aliases+=("$disk")
|
||||||
|
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||||
|
local symlink
|
||||||
|
for symlink in /dev/disk/by-id/*; do
|
||||||
|
[[ -e "$symlink" ]] || continue
|
||||||
|
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||||
|
done
|
||||||
|
|
||||||
|
local conf vmid alias escaped
|
||||||
|
for conf in /etc/pve/qemu-server/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
vmid=$(basename "$conf" .conf)
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
escaped="${alias//./\\.}"
|
||||||
|
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||||
|
echo "VM:$vmid"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
local ctid
|
||||||
|
for conf in /etc/pve/lxc/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
ctid=$(basename "$conf" .conf)
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
escaped="${alias//./\\.}"
|
||||||
|
if grep -qE "${escaped}(,|[[:space:]]|$)" "$conf" 2>/dev/null; then
|
||||||
|
echo "CT:$ctid"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print the slot names (e.g. sata0, scsi1) in a VM config that reference the disk.
|
||||||
|
function _find_disk_slots_in_vm() {
|
||||||
|
local vmid="$1"
|
||||||
|
local disk="$2"
|
||||||
|
local real_path conf
|
||||||
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
conf="/etc/pve/qemu-server/${vmid}.conf"
|
||||||
|
[[ -f "$conf" ]] || return
|
||||||
|
|
||||||
|
local -a aliases=("$disk")
|
||||||
|
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||||
|
local symlink
|
||||||
|
for symlink in /dev/disk/by-id/*; do
|
||||||
|
[[ -e "$symlink" ]] || continue
|
||||||
|
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||||
|
done
|
||||||
|
|
||||||
|
local key rest alias escaped
|
||||||
|
while IFS=: read -r key rest; do
|
||||||
|
key=$(echo "$key" | xargs)
|
||||||
|
[[ "$key" =~ ^(scsi|sata|ide|virtio)[0-9]+$ ]] || continue
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
escaped="${alias//./\\.}"
|
||||||
|
if echo "$rest" | grep -qE "${escaped}(,|[[:space:]]|$)"; then
|
||||||
|
echo "$key"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done < "$conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print the mp names (e.g. mp0, mp1) in a CT config that reference the disk.
|
||||||
|
function _find_disk_slots_in_ct() {
|
||||||
|
local ctid="$1"
|
||||||
|
local disk="$2"
|
||||||
|
local real_path conf
|
||||||
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
conf="/etc/pve/lxc/${ctid}.conf"
|
||||||
|
[[ -f "$conf" ]] || return
|
||||||
|
|
||||||
|
local -a aliases=("$disk")
|
||||||
|
[[ -n "$real_path" && "$real_path" != "$disk" ]] && aliases+=("$real_path")
|
||||||
|
local symlink
|
||||||
|
for symlink in /dev/disk/by-id/*; do
|
||||||
|
[[ -e "$symlink" ]] || continue
|
||||||
|
[[ "$(readlink -f "$symlink" 2>/dev/null)" == "$real_path" ]] && aliases+=("$symlink")
|
||||||
|
done
|
||||||
|
|
||||||
|
local key rest alias escaped
|
||||||
|
while IFS=: read -r key rest; do
|
||||||
|
key=$(echo "$key" | xargs)
|
||||||
|
[[ "$key" =~ ^mp[0-9]+$ ]] || continue
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
escaped="${alias//./\\.}"
|
||||||
|
if echo "$rest" | grep -qE "${escaped}(,|[[:space:]]|$)"; then
|
||||||
|
echo "$key"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done < "$conf"
|
||||||
|
}
|
||||||
|
|
||||||
function _controller_block_devices() {
|
function _controller_block_devices() {
|
||||||
local pci_full="$1"
|
local pci_full="$1"
|
||||||
local pci_root="/sys/bus/pci/devices/$pci_full"
|
local pci_root="/sys/bus/pci/devices/$pci_full"
|
||||||
@@ -137,6 +362,14 @@ function _vm_is_q35() {
|
|||||||
[[ "$machine_line" == *q35* ]]
|
[[ "$machine_line" == *q35* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _vm_storage_register_vfio_iommu_tool() {
|
||||||
|
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
||||||
|
command -v jq >/dev/null 2>&1 || return 0
|
||||||
|
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
||||||
|
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
||||||
|
&& mv "$tools_json.tmp" "$tools_json" || true
|
||||||
|
}
|
||||||
|
|
||||||
function _vm_storage_enable_iommu_cmdline() {
|
function _vm_storage_enable_iommu_cmdline() {
|
||||||
local cpu_vendor iommu_param
|
local cpu_vendor iommu_param
|
||||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||||
@@ -175,18 +408,28 @@ function _vm_storage_ensure_iommu_or_offer() {
|
|||||||
local reboot_policy="${VM_STORAGE_IOMMU_REBOOT_POLICY:-ask_now}"
|
local reboot_policy="${VM_STORAGE_IOMMU_REBOOT_POLICY:-ask_now}"
|
||||||
|
|
||||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
|
_vm_storage_register_vfio_iommu_tool
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
|
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
|
||||||
[[ -d /sys/kernel/iommu_groups ]] && \
|
[[ -d /sys/kernel/iommu_groups ]] && \
|
||||||
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
|
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
|
||||||
|
_vm_storage_register_vfio_iommu_tool
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Wizard flow: if IOMMU was already configured in this run and reboot is pending,
|
# Dedup: if IOMMU was already configured/announced in this wizard run, skip prompt
|
||||||
# allow the user to continue planning storage selections without re-prompting.
|
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
if [[ "$reboot_policy" == "defer" && "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect if another script already wrote IOMMU params (e.g. GPU script ran first)
|
||||||
|
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||||
|
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||||
|
_vm_storage_register_vfio_iommu_tool
|
||||||
|
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||||
|
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -206,6 +449,8 @@ function _vm_storage_ensure_iommu_or_offer() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_vm_storage_register_vfio_iommu_tool
|
||||||
|
|
||||||
if [[ "$reboot_policy" == "defer" ]]; then
|
if [[ "$reboot_policy" == "defer" ]]; then
|
||||||
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||||
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||||
@@ -230,47 +475,71 @@ function _vm_storage_confirm_controller_passthrough_risk() {
|
|||||||
local vmid="${1:-}"
|
local vmid="${1:-}"
|
||||||
local vm_name="${2:-}"
|
local vm_name="${2:-}"
|
||||||
local title="${3:-Controller + NVMe}"
|
local title="${3:-Controller + NVMe}"
|
||||||
|
local ui_mode="${4:-auto}" # wizard | standalone | auto
|
||||||
local vm_label=""
|
local vm_label=""
|
||||||
if [[ -n "$vmid" ]]; then
|
if [[ -n "$vmid" ]]; then
|
||||||
vm_label="$vmid"
|
vm_label="$vmid"
|
||||||
[[ -n "$vm_name" ]] && vm_label="${vm_label} (${vm_name})"
|
[[ -n "$vm_name" ]] && vm_label="${vm_label} (${vm_name})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local msg
|
|
||||||
msg="$(translate "Important compatibility notice")\n\n"
|
|
||||||
msg+="$(translate "Not all motherboards support physical Controller/NVMe passthrough to VMs reliably, especially systems with old platforms or limited BIOS/UEFI firmware.")\n\n"
|
|
||||||
msg+="$(translate "On some systems, the VM may fail to start or the host may freeze when the VM boots.")\n\n"
|
|
||||||
|
|
||||||
local reinforce_limited_firmware="no"
|
local reinforce_limited_firmware="no"
|
||||||
local bios_date bios_year current_year cpu_model
|
local bios_date bios_year current_year bios_age cpu_model risk_detail=""
|
||||||
bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null)
|
bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null)
|
||||||
bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1)
|
bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1)
|
||||||
current_year=$(date +%Y 2>/dev/null)
|
current_year=$(date +%Y 2>/dev/null)
|
||||||
if [[ -n "$bios_year" && -n "$current_year" ]]; then
|
if [[ -n "$bios_year" && -n "$current_year" ]]; then
|
||||||
if (( current_year - bios_year >= 7 )); then
|
bios_age=$(( current_year - bios_year ))
|
||||||
|
if (( bios_age >= 7 )); then
|
||||||
reinforce_limited_firmware="yes"
|
reinforce_limited_firmware="yes"
|
||||||
|
risk_detail="$(translate "BIOS from") ${bios_year} (${bios_age} $(translate "years old")) — $(translate "older firmware may increase passthrough instability")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs)
|
cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs)
|
||||||
if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then
|
if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then
|
||||||
reinforce_limited_firmware="yes"
|
reinforce_limited_firmware="yes"
|
||||||
|
[[ -z "$risk_detail" ]] && risk_detail="$(translate "Low-power CPU platform"): ${cpu_model}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$reinforce_limited_firmware" == "yes" ]]; then
|
if [[ "$ui_mode" == "auto" ]]; then
|
||||||
msg+="$(translate "Detected risk factor: this host may use an older or limited firmware platform, which increases passthrough instability risk.")\n\n"
|
if [[ "${PROXMENUX_UI_MODE:-}" == "wizard" || "${WIZARD_CALL:-false}" == "true" ]]; then
|
||||||
|
ui_mode="wizard"
|
||||||
|
else
|
||||||
|
ui_mode="standalone"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$vm_label" ]]; then
|
local height=20
|
||||||
msg+="$(translate "Target VM"): ${vm_label}\n\n"
|
[[ "$reinforce_limited_firmware" == "yes" ]] && height=23
|
||||||
fi
|
|
||||||
msg+="$(translate "If this happens after assignment"):\n"
|
|
||||||
msg+=" - $(translate "Power cycle the host if it is frozen.")\n"
|
|
||||||
msg+=" - $(translate "Remove the hostpci controller/NVMe entries from the VM config file.")\n"
|
|
||||||
msg+=" /etc/pve/qemu-server/${vmid:-<VMID>}.conf\n"
|
|
||||||
msg+=" - $(translate "Start the VM again without that passthrough device.")\n\n"
|
|
||||||
msg+="$(translate "Do you want to continue with this assignment?")"
|
|
||||||
|
|
||||||
whiptail --title "$title" --yesno "$msg" 21 96
|
if [[ "$ui_mode" == "wizard" ]]; then
|
||||||
|
# whiptail: plain text (no color codes)
|
||||||
|
local msg
|
||||||
|
[[ -n "$vm_label" ]] && msg+="$(translate "Target VM"): ${vm_label}\n\n"
|
||||||
|
msg+="⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\n\n"
|
||||||
|
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
|
||||||
|
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
|
||||||
|
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
|
||||||
|
msg+="\n$(translate "Detected risk factor"): ${risk_detail}\n"
|
||||||
|
fi
|
||||||
|
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${vmid:-<VMID>}.conf\n"
|
||||||
|
msg+="\n$(translate "Do you want to continue?")"
|
||||||
|
whiptail --title "$title" --yesno "$msg" $height 96
|
||||||
|
else
|
||||||
|
# dialog: colored format matching add_controller_nvme_vm.sh
|
||||||
|
local msg
|
||||||
|
[[ -n "$vm_label" ]] && msg+="\n\Zb$(translate "Target VM"): ${vm_label}\Zn\n"
|
||||||
|
msg+="\n\Zb\Z4⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\Zn\n\n"
|
||||||
|
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
|
||||||
|
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
|
||||||
|
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
|
||||||
|
msg+="\n\Z1$(translate "Detected risk factor"): ${risk_detail}\Zn\n"
|
||||||
|
fi
|
||||||
|
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${vmid:-<VMID>}.conf\n"
|
||||||
|
msg+="\n\Zb$(translate "Do you want to continue?")\Zn"
|
||||||
|
dialog --backtitle "ProxMenux" --colors \
|
||||||
|
--title "$title" \
|
||||||
|
--yesno "$msg" $height 96
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function _shorten_text() {
|
function _shorten_text() {
|
||||||
@@ -284,6 +553,30 @@ function _shorten_text() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _pci_storage_display_name() {
|
||||||
|
local pci_full="$1"
|
||||||
|
local raw_line name_part
|
||||||
|
|
||||||
|
raw_line=$(lspci -nn -s "${pci_full#0000:}" 2>/dev/null | sed 's/^[^ ]* //')
|
||||||
|
if [[ -z "$raw_line" ]]; then
|
||||||
|
translate "Unknown storage controller"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prefer the right side after class prefix (e.g. "...: Vendor Model ...").
|
||||||
|
name_part="${raw_line#*: }"
|
||||||
|
[[ "$name_part" == "$raw_line" ]] && name_part="$raw_line"
|
||||||
|
|
||||||
|
# Remove noisy suffixes while keeping the meaningful model name.
|
||||||
|
name_part="${name_part%% (rev *}"
|
||||||
|
name_part=$(echo "$name_part" | sed -E 's/\[[0-9a-fA-F]{4}:[0-9a-fA-F]{4}\]//g')
|
||||||
|
name_part=$(echo "$name_part" | sed -E 's/ Technology Inc\.?//g; s/ Corporation//g; s/ Co\., Ltd\.?//g')
|
||||||
|
name_part=$(echo "$name_part" | sed -E 's/[[:space:]]+/ /g; s/^ +| +$//g')
|
||||||
|
|
||||||
|
[[ -z "$name_part" ]] && name_part="$raw_line"
|
||||||
|
echo "$name_part"
|
||||||
|
}
|
||||||
|
|
||||||
function _pci_slot_base() {
|
function _pci_slot_base() {
|
||||||
local pci_full="$1"
|
local pci_full="$1"
|
||||||
local slot
|
local slot
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
TOOLS_JSON="$BASE_DIR/installed_tools.json"
|
||||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||||
@@ -190,12 +191,80 @@ _vm_switch_action_label() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gpu_register_vfio_iommu_tool() {
|
||||||
|
command -v jq >/dev/null 2>&1 || return 0
|
||||||
|
[[ -f "$TOOLS_JSON" ]] || echo "{}" > "$TOOLS_JSON"
|
||||||
|
jq '.vfio_iommu=true' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" \
|
||||||
|
&& mv "$TOOLS_JSON.tmp" "$TOOLS_JSON" || true
|
||||||
|
}
|
||||||
|
|
||||||
_set_wizard_result() {
|
_set_wizard_result() {
|
||||||
local result="$1"
|
local result="$1"
|
||||||
[[ -z "${GPU_WIZARD_RESULT_FILE:-}" ]] && return 0
|
[[ -z "${GPU_WIZARD_RESULT_FILE:-}" ]] && return 0
|
||||||
printf '%s\n' "$result" >"$GPU_WIZARD_RESULT_FILE" 2>/dev/null || true
|
printf '%s\n' "$result" >"$GPU_WIZARD_RESULT_FILE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# UI wrapper helpers — dialog in standalone, whiptail in wizard
|
||||||
|
# ==========================================================
|
||||||
|
# Strips dialog color sequences (\Zb, \Z1, \Zn, etc.) from a string
|
||||||
|
_strip_colors() {
|
||||||
|
printf '%s' "$1" | sed 's/\\Z[0-9a-zA-Z]//g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Msgbox: dialog in standalone mode, whiptail in wizard mode
|
||||||
|
_pmx_msgbox() {
|
||||||
|
local title="$1" msg="$2" h="${3:-10}" w="${4:-72}"
|
||||||
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||||
|
whiptail --backtitle "ProxMenux" --title "$title" \
|
||||||
|
--msgbox "$(_strip_colors "$msg")" "$h" "$w"
|
||||||
|
else
|
||||||
|
dialog --backtitle "ProxMenux" --colors \
|
||||||
|
--title "$title" --msgbox "$msg" "$h" "$w"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Yesno: dialog in standalone mode, whiptail in wizard mode
|
||||||
|
# Returns 0 for yes, 1 for no (same as dialog/whiptail)
|
||||||
|
_pmx_yesno() {
|
||||||
|
local title="$1" msg="$2" h="${3:-10}" w="${4:-72}"
|
||||||
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||||
|
whiptail --backtitle "ProxMenux" --title "$title" \
|
||||||
|
--yesno "$(_strip_colors "$msg")" "$h" "$w"
|
||||||
|
else
|
||||||
|
dialog --backtitle "ProxMenux" --colors \
|
||||||
|
--title "$title" --yesno "$msg" "$h" "$w"
|
||||||
|
fi
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Menu: dialog in standalone mode, whiptail in wizard mode
|
||||||
|
# Accepts optional --default-item VALUE before title
|
||||||
|
# Usage: _pmx_menu [--default-item VAL] title msg h w list_h item desc ...
|
||||||
|
_pmx_menu() {
|
||||||
|
local -a extra_opts=()
|
||||||
|
while [[ "${1:-}" == --* ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--default-item) extra_opts+=("--default-item" "$2"); shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
local title="$1" msg="$2" h="$3" w="$4" lh="$5"
|
||||||
|
shift 5
|
||||||
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||||
|
whiptail --backtitle "ProxMenux" "${extra_opts[@]}" \
|
||||||
|
--title "$title" \
|
||||||
|
--menu "$(_strip_colors "$msg")" "$h" "$w" "$lh" \
|
||||||
|
"$@" 3>&1 1>&2 2>&3
|
||||||
|
else
|
||||||
|
dialog --backtitle "ProxMenux" --colors "${extra_opts[@]}" \
|
||||||
|
--title "$title" \
|
||||||
|
--menu "$msg" "$h" "$w" "$lh" \
|
||||||
|
"$@" 2>&1 >/dev/tty
|
||||||
|
fi
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
_file_has_exact_line() {
|
_file_has_exact_line() {
|
||||||
local line="$1"
|
local line="$1"
|
||||||
local file="$2"
|
local file="$2"
|
||||||
@@ -398,18 +467,16 @@ ensure_selected_gpu_not_already_in_target_vm() {
|
|||||||
TARGET_VM_ALREADY_HAS_GPU=true
|
TARGET_VM_ALREADY_HAS_GPU=true
|
||||||
local popup_title
|
local popup_title
|
||||||
popup_title=$(_get_vm_run_title)
|
popup_title=$(_get_vm_run_title)
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "${popup_title}" \
|
||||||
--title "${popup_title}" \
|
"\n$(translate 'The selected GPU is already assigned to this VM, but the host is not currently using vfio-pci for this device.')\n\n$(translate 'Current driver'): ${current_driver}\n\n$(translate 'The script will continue to restore VM passthrough mode on the host and reuse existing hostpci entries.')" \
|
||||||
--msgbox "\n$(translate 'The selected GPU is already assigned to this VM, but the host is not currently using vfio-pci for this device.')\n\n$(translate 'Current driver'): ${current_driver}\n\n$(translate 'The script will continue to restore VM passthrough mode on the host and reuse existing hostpci entries.')" \
|
|
||||||
13 78
|
13 78
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Single GPU system: nothing else to choose
|
# Single GPU system: nothing else to choose
|
||||||
if [[ $GPU_COUNT -le 1 ]]; then
|
if [[ $GPU_COUNT -le 1 ]]; then
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'GPU Already Added')" \
|
||||||
--title "$(translate 'GPU Already Added')" \
|
"\n$(translate 'The selected GPU is already assigned to this VM.')\n\n$(translate 'No changes are required.')" \
|
||||||
--msgbox "\n$(translate 'The selected GPU is already assigned to this VM.')\n\n$(translate 'No changes are required.')" \
|
|
||||||
9 66
|
9 66
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -428,22 +495,18 @@ ensure_selected_gpu_not_already_in_target_vm() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [[ $available -eq 0 ]]; then
|
if [[ $available -eq 0 ]]; then
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'All GPUs Already Assigned')" \
|
||||||
--title "$(translate 'All GPUs Already Assigned')" \
|
"\n$(translate 'All detected GPUs are already assigned to this VM.')\n\n$(translate 'No additional GPU can be added.')" \
|
||||||
--msgbox "\n$(translate 'All detected GPUs are already assigned to this VM.')\n\n$(translate 'No additional GPU can be added.')" \
|
|
||||||
10 70
|
10 70
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local choice
|
local choice
|
||||||
local -a clear_opt=()
|
choice=$(_pmx_menu \
|
||||||
[[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear)
|
"$(translate 'GPU Already Assigned to This VM')" \
|
||||||
choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \
|
"\n$(translate 'The selected GPU is already present in this VM. Select another GPU to continue:')" \
|
||||||
--title "$(translate 'GPU Already Assigned to This VM')" \
|
|
||||||
--menu "\n$(translate 'The selected GPU is already present in this VM. Select another GPU to continue:')" \
|
|
||||||
18 82 10 \
|
18 82 10 \
|
||||||
"${menu_items[@]}" \
|
"${menu_items[@]}") || exit 0
|
||||||
2>&1 >/dev/tty) || exit 0
|
|
||||||
|
|
||||||
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
||||||
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
||||||
@@ -492,9 +555,8 @@ detect_host_gpus() {
|
|||||||
|
|
||||||
if [[ $GPU_COUNT -eq 0 ]]; then
|
if [[ $GPU_COUNT -eq 0 ]]; then
|
||||||
_set_wizard_result "no_gpu"
|
_set_wizard_result "no_gpu"
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'No GPU Detected')" \
|
||||||
--title "$(translate 'No GPU Detected')" \
|
"\n$(translate 'No compatible GPU was detected on this host.')" 8 60
|
||||||
--msgbox "\n$(translate 'No compatible GPU was detected on this host.')" 8 60
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -506,7 +568,16 @@ detect_host_gpus() {
|
|||||||
# Phase 1 — Step 2: Check IOMMU, offer to enable it
|
# Phase 1 — Step 2: Check IOMMU, offer to enable it
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
check_iommu_enabled() {
|
check_iommu_enabled() {
|
||||||
|
# Dedup: if IOMMU was already configured by another script in this wizard run, skip prompt
|
||||||
|
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
|
IOMMU_PENDING_REBOOT=true
|
||||||
|
HOST_CONFIG_CHANGED=true
|
||||||
|
_gpu_register_vfio_iommu_tool
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
|
_gpu_register_vfio_iommu_tool
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -519,9 +590,11 @@ check_iommu_enabled() {
|
|||||||
if [[ "$configured_next_boot" == "true" ]]; then
|
if [[ "$configured_next_boot" == "true" ]]; then
|
||||||
IOMMU_PENDING_REBOOT=true
|
IOMMU_PENDING_REBOOT=true
|
||||||
HOST_CONFIG_CHANGED=true
|
HOST_CONFIG_CHANGED=true
|
||||||
dialog --backtitle "ProxMenux" \
|
_gpu_register_vfio_iommu_tool
|
||||||
--title "$(translate 'IOMMU Pending Reboot')" \
|
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||||
--msgbox "\n$(translate 'IOMMU is already configured for next boot, but it is not active yet.')\n\n$(translate 'GPU passthrough configuration will continue now and will become effective after host reboot.')" \
|
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||||
|
_pmx_msgbox "$(translate 'IOMMU Pending Reboot')" \
|
||||||
|
"\n$(translate 'IOMMU is already configured for next boot, but it is not active yet.')\n\n$(translate 'GPU passthrough configuration will continue now and will become effective after host reboot.')" \
|
||||||
11 78
|
11 78
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -533,9 +606,7 @@ check_iommu_enabled() {
|
|||||||
msg+="$(translate 'Note: A system reboot will be required after enabling IOMMU.')\n"
|
msg+="$(translate 'Note: A system reboot will be required after enabling IOMMU.')\n"
|
||||||
msg+="$(translate 'Configuration will continue now and be effective after reboot.')"
|
msg+="$(translate 'Configuration will continue now and be effective after reboot.')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_yesno "$(translate 'IOMMU Required')" "$msg" 15 72
|
||||||
--title "$(translate 'IOMMU Required')" \
|
|
||||||
--yesno "$msg" 15 72
|
|
||||||
|
|
||||||
local response=$?
|
local response=$?
|
||||||
[[ "$WIZARD_CALL" != "true" ]] && clear
|
[[ "$WIZARD_CALL" != "true" ]] && clear
|
||||||
@@ -553,6 +624,9 @@ check_iommu_enabled() {
|
|||||||
fi
|
fi
|
||||||
IOMMU_PENDING_REBOOT=true
|
IOMMU_PENDING_REBOOT=true
|
||||||
HOST_CONFIG_CHANGED=true
|
HOST_CONFIG_CHANGED=true
|
||||||
|
_gpu_register_vfio_iommu_tool
|
||||||
|
VM_STORAGE_IOMMU_PENDING_REBOOT=1
|
||||||
|
export VM_STORAGE_IOMMU_PENDING_REBOOT
|
||||||
echo
|
echo
|
||||||
msg_success "$(translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.')"
|
msg_success "$(translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.')"
|
||||||
echo
|
echo
|
||||||
@@ -632,14 +706,11 @@ select_gpu() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
local choice
|
local choice
|
||||||
local -a clear_opt=()
|
choice=$(_pmx_menu \
|
||||||
[[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear)
|
"$(translate 'Select GPU for VM Passthrough')" \
|
||||||
choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \
|
"\n$(translate 'Select the GPU to pass through to the VM:')" \
|
||||||
--title "$(translate 'Select GPU for VM Passthrough')" \
|
|
||||||
--menu "\n$(translate 'Select the GPU to pass through to the VM:')" \
|
|
||||||
18 82 10 \
|
18 82 10 \
|
||||||
"${menu_items[@]}" \
|
"${menu_items[@]}") || exit 0
|
||||||
2>&1 >/dev/tty) || exit 0
|
|
||||||
|
|
||||||
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
||||||
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
||||||
@@ -665,9 +736,7 @@ warn_single_gpu() {
|
|||||||
msg+="$(translate 'Make sure you have SSH or Web UI access before rebooting.')\n\n"
|
msg+="$(translate 'Make sure you have SSH or Web UI access before rebooting.')\n\n"
|
||||||
msg+="$(translate 'Do you want to continue?')"
|
msg+="$(translate 'Do you want to continue?')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_yesno "$(translate 'Single GPU Warning')" "$msg" 22 76
|
||||||
--title "$(translate 'Single GPU Warning')" \
|
|
||||||
--yesno "$msg" 22 76
|
|
||||||
|
|
||||||
[[ $? -ne 0 ]] && exit 0
|
[[ $? -ne 0 ]] && exit 0
|
||||||
}
|
}
|
||||||
@@ -765,9 +834,7 @@ check_intel_vm_compatibility() {
|
|||||||
msg+="$(translate 'This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.')\n\n"
|
msg+="$(translate 'This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.')\n\n"
|
||||||
msg+="$(translate 'Recommended: use GPU with LXC workloads instead of VM passthrough on this hardware.')"
|
msg+="$(translate 'Recommended: use GPU with LXC workloads instead of VM passthrough on this hardware.')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'Blocked GPU ID')" "$msg" 20 84
|
||||||
--title "$(translate 'Blocked GPU ID')" \
|
|
||||||
--msgbox "$msg" 20 84
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -782,9 +849,7 @@ check_intel_vm_compatibility() {
|
|||||||
msg+="$(translate 'This state has a high probability of VM startup/reset failures.')\n\n"
|
msg+="$(translate 'This state has a high probability of VM startup/reset failures.')\n\n"
|
||||||
msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn"
|
msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'High-Risk GPU Power State')" "$msg" 20 80
|
||||||
--title "$(translate 'High-Risk GPU Power State')" \
|
|
||||||
--msgbox "$msg" 20 80
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -800,9 +865,7 @@ check_intel_vm_compatibility() {
|
|||||||
msg+="$(translate 'startup/restart errors are likely.')\n\n"
|
msg+="$(translate 'startup/restart errors are likely.')\n\n"
|
||||||
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'Reset Capability Blocked')" "$msg" 20 80
|
||||||
--title "$(translate 'Reset Capability Blocked')" \
|
|
||||||
--msgbox "$msg" 20 80
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -818,9 +881,7 @@ check_intel_vm_compatibility() {
|
|||||||
msg+="$(translate 'start/restart failures and reset instability.')\n\n"
|
msg+="$(translate 'start/restart failures and reset instability.')\n\n"
|
||||||
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'Reset Capability Blocked')" "$msg" 20 80
|
||||||
--title "$(translate 'Reset Capability Blocked')" \
|
|
||||||
--msgbox "$msg" 20 80
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -834,9 +895,7 @@ check_intel_vm_compatibility() {
|
|||||||
msg+="$(translate 'Passthrough may work, but startup/restart reliability is not guaranteed.')\n\n"
|
msg+="$(translate 'Passthrough may work, but startup/restart reliability is not guaranteed.')\n\n"
|
||||||
msg+="$(translate 'Do you want to continue anyway?')"
|
msg+="$(translate 'Do you want to continue anyway?')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_yesno "$(translate 'Reset Capability Warning')" "$msg" 18 78
|
||||||
--title "$(translate 'Reset Capability Warning')" \
|
|
||||||
--yesno "$msg" 18 78
|
|
||||||
[[ $? -ne 0 ]] && exit 0
|
[[ $? -ne 0 ]] && exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -872,9 +931,7 @@ check_gpu_vm_compatibility() {
|
|||||||
msg+=" • $(translate 'Potential QEMU startup/assertion failures')\n\n"
|
msg+=" • $(translate 'Potential QEMU startup/assertion failures')\n\n"
|
||||||
msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn"
|
msg+="\Zb$(translate 'Configuration has been stopped to prevent an unusable VM state.')\Zn"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'High-Risk GPU Power State')" "$msg" 22 80
|
||||||
--title "$(translate 'High-Risk GPU Power State')" \
|
|
||||||
--msgbox "$msg" 22 80
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -903,9 +960,7 @@ check_gpu_vm_compatibility() {
|
|||||||
msg+=" — QEMU IRQ assertion failure → VM does not start\n\n"
|
msg+=" — QEMU IRQ assertion failure → VM does not start\n\n"
|
||||||
msg+="\Zb$(translate 'Configuration has been stopped to prevent leaving the VM in an unusable state.')\Zn"
|
msg+="\Zb$(translate 'Configuration has been stopped to prevent leaving the VM in an unusable state.')\Zn"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'Incompatible GPU for VM Passthrough')" "$msg" 26 80
|
||||||
--title "$(translate 'Incompatible GPU for VM Passthrough')" \
|
|
||||||
--msgbox "$msg" 26 80
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -922,9 +977,7 @@ check_gpu_vm_compatibility() {
|
|||||||
msg+="$(translate 'for this policy and may fail after first use or on subsequent VM starts.')\n\n"
|
msg+="$(translate 'for this policy and may fail after first use or on subsequent VM starts.')\n\n"
|
||||||
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
msg+="\Zb$(translate 'Configuration has been stopped due to high reset risk.')\Zn"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'Reset Capability Blocked')" "$msg" 20 80
|
||||||
--title "$(translate 'Reset Capability Blocked')" \
|
|
||||||
--msgbox "$msg" 20 80
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -939,9 +992,7 @@ check_gpu_vm_compatibility() {
|
|||||||
msg+="$(translate 'Passthrough may fail depending on hardware/firmware implementation.')\n\n"
|
msg+="$(translate 'Passthrough may fail depending on hardware/firmware implementation.')\n\n"
|
||||||
msg+="$(translate 'Do you want to continue anyway?')"
|
msg+="$(translate 'Do you want to continue anyway?')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_yesno "$(translate 'Reset Capability Warning')" "$msg" 18 78
|
||||||
--title "$(translate 'Reset Capability Warning')" \
|
|
||||||
--yesno "$msg" 18 78
|
|
||||||
[[ $? -ne 0 ]] && exit 0
|
[[ $? -ne 0 ]] && exit 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -965,16 +1016,14 @@ analyze_iommu_group() {
|
|||||||
did=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//')
|
did=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//')
|
||||||
[[ -n "$vid" && -n "$did" ]] && IOMMU_VFIO_IDS+=("${vid}:${did}")
|
[[ -n "$vid" && -n "$did" ]] && IOMMU_VFIO_IDS+=("${vid}:${did}")
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
_pmx_msgbox "$(translate 'IOMMU Group Pending')" \
|
||||||
--title "$(translate 'IOMMU Group Pending')" \
|
"\n$(translate 'IOMMU groups are not available yet because reboot is pending.')\n\n$(translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.')\n\n$(translate 'Selected GPU function'):\n • ${pci_full}" \
|
||||||
--msgbox "\n$(translate 'IOMMU groups are not available yet because reboot is pending.')\n\n$(translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.')\n\n$(translate 'Selected GPU function'):\n • ${pci_full}" \
|
|
||||||
14 82
|
14 82
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'IOMMU Group Error')" \
|
||||||
--title "$(translate 'IOMMU Group Error')" \
|
"\n$(translate 'Could not determine the IOMMU group for the selected GPU.')\n\n$(translate 'Make sure IOMMU is properly enabled and the system has been rebooted after activation.')" \
|
||||||
--msgbox "\n$(translate 'Could not determine the IOMMU group for the selected GPU.')\n\n$(translate 'Make sure IOMMU is properly enabled and the system has been rebooted after activation.')" \
|
|
||||||
10 72
|
10 72
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1016,19 +1065,6 @@ analyze_iommu_group() {
|
|||||||
[[ "$dev" != "$pci_full" ]] && extra_devices=$((extra_devices + 1))
|
[[ "$dev" != "$pci_full" ]] && extra_devices=$((extra_devices + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
local msg
|
|
||||||
msg="$(translate 'IOMMU Group'): ${IOMMU_GROUP}\n\n"
|
|
||||||
msg+="$(translate 'The following devices will all be passed to the VM') "
|
|
||||||
msg+="($(translate 'IOMMU isolation rule')):\n\n"
|
|
||||||
msg+="${display_lines}"
|
|
||||||
|
|
||||||
if [[ $extra_devices -gt 0 ]]; then
|
|
||||||
msg+="\n\Z1$(translate 'All devices in the same IOMMU group must be passed together.')\Zn"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
|
||||||
--title "$(translate 'IOMMU Group') ${IOMMU_GROUP}" \
|
|
||||||
--msgbox "\n${msg}" 22 82
|
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_optional_gpu_audio() {
|
detect_optional_gpu_audio() {
|
||||||
@@ -1078,9 +1114,8 @@ select_vm() {
|
|||||||
VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}')
|
VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}')
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'Invalid VMID')" \
|
||||||
--title "$(translate 'Invalid VMID')" \
|
"\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72
|
||||||
--msgbox "\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1097,19 +1132,17 @@ select_vm() {
|
|||||||
done < <(qm list 2>/dev/null)
|
done < <(qm list 2>/dev/null)
|
||||||
|
|
||||||
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'No VMs Found')" \
|
||||||
--title "$(translate 'No VMs Found')" \
|
"\n$(translate 'No Virtual Machines found on this system.')\n\n$(translate 'Create a VM first (machine type q35 + UEFI BIOS), then run this option again.')" \
|
||||||
--msgbox "\n$(translate 'No Virtual Machines found on this system.')\n\n$(translate 'Create a VM first (machine type q35 + UEFI BIOS), then run this option again.')" \
|
|
||||||
10 68
|
10 68
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SELECTED_VMID=$(dialog --backtitle "ProxMenux" \
|
SELECTED_VMID=$(_pmx_menu \
|
||||||
--title "$(translate 'Select Virtual Machine')" \
|
"$(translate 'Select Virtual Machine')" \
|
||||||
--menu "\n$(translate 'Select the VM to add the GPU to:')" \
|
"\n$(translate 'Select the VM to add the GPU to:')" \
|
||||||
20 72 12 \
|
20 72 12 \
|
||||||
"${menu_items[@]}" \
|
"${menu_items[@]}") || exit 0
|
||||||
2>&1 >/dev/tty) || exit 0
|
|
||||||
|
|
||||||
VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}')
|
VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}')
|
||||||
}
|
}
|
||||||
@@ -1138,9 +1171,7 @@ check_vm_machine_type() {
|
|||||||
msg+=" • $(translate 'BIOS: OVMF (UEFI)')\n"
|
msg+=" • $(translate 'BIOS: OVMF (UEFI)')\n"
|
||||||
msg+=" • $(translate 'Storage controller: VirtIO SCSI')"
|
msg+=" • $(translate 'Storage controller: VirtIO SCSI')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'Incompatible Machine Type')" "$msg" 20 78
|
||||||
--title "$(translate 'Incompatible Machine Type')" \
|
|
||||||
--msgbox "$msg" 20 78
|
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1210,13 +1241,11 @@ check_switch_mode() {
|
|||||||
msg+="\Z1\Zb$(translate 'Start on boot enabled (onboot=1)'): ${onboot_count}\Zn\n"
|
msg+="\Z1\Zb$(translate 'Start on boot enabled (onboot=1)'): ${onboot_count}\Zn\n"
|
||||||
msg+="\n\Z1$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn"
|
msg+="\n\Z1$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn"
|
||||||
|
|
||||||
action_choice=$(dialog --backtitle "ProxMenux" --colors \
|
action_choice=$(_pmx_menu --default-item "2" \
|
||||||
--title "$(translate 'GPU Used in LXC Containers')" \
|
"$(translate 'GPU Used in LXC Containers')" \
|
||||||
--default-item "2" \
|
"$msg" 25 96 8 \
|
||||||
--menu "$msg" 25 96 8 \
|
|
||||||
"1" "$(translate 'Keep GPU in LXC config (disable Start on boot)')" \
|
"1" "$(translate 'Keep GPU in LXC config (disable Start on boot)')" \
|
||||||
"2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')" \
|
"2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')") || exit 0
|
||||||
2>&1 >/dev/tty) || exit 0
|
|
||||||
|
|
||||||
case "$action_choice" in
|
case "$action_choice" in
|
||||||
1) LXC_SWITCH_ACTION="keep_gpu_disable_onboot" ;;
|
1) LXC_SWITCH_ACTION="keep_gpu_disable_onboot" ;;
|
||||||
@@ -1254,9 +1283,7 @@ check_switch_mode() {
|
|||||||
msg+=" Hardware Graphics → Add GPU to VM\n"
|
msg+=" Hardware Graphics → Add GPU to VM\n"
|
||||||
msg+="$(translate 'to move the GPU safely.')"
|
msg+="$(translate 'to move the GPU safely.')"
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" \
|
_pmx_msgbox "$(translate 'GPU Busy in Running VM')" "$msg" 16 78
|
||||||
--title "$(translate 'GPU Busy in Running VM')" \
|
|
||||||
--msgbox "$msg" 16 78
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1292,13 +1319,11 @@ check_switch_mode() {
|
|||||||
msg+="$(translate 'Choose conflict policy for the source VM:')"
|
msg+="$(translate 'Choose conflict policy for the source VM:')"
|
||||||
|
|
||||||
local vm_action_choice
|
local vm_action_choice
|
||||||
vm_action_choice=$(dialog --clear --backtitle "ProxMenux" --colors \
|
vm_action_choice=$(_pmx_menu --default-item "1" \
|
||||||
--title "$(translate 'GPU Already Assigned to Another VM')" \
|
"$(translate 'GPU Already Assigned to Another VM')" \
|
||||||
--default-item "1" \
|
"$msg" 24 84 8 \
|
||||||
--menu "$msg" 24 98 8 \
|
|
||||||
"1" "$(translate 'Keep GPU in source VM config (disable Start on boot if enabled)')" \
|
"1" "$(translate 'Keep GPU in source VM config (disable Start on boot if enabled)')" \
|
||||||
"2" "$(translate 'Remove GPU from source VM config (keep Start on boot)')" \
|
"2" "$(translate 'Remove GPU from source VM config (keep Start on boot)')") || exit 0
|
||||||
2>&1 >/dev/tty) || exit 0
|
|
||||||
|
|
||||||
case "$vm_action_choice" in
|
case "$vm_action_choice" in
|
||||||
1) SWITCH_VM_ACTION="keep_gpu_disable_onboot" ;;
|
1) SWITCH_VM_ACTION="keep_gpu_disable_onboot" ;;
|
||||||
@@ -1376,9 +1401,7 @@ confirm_summary() {
|
|||||||
local run_title
|
local run_title
|
||||||
run_title=$(_get_vm_run_title)
|
run_title=$(_get_vm_run_title)
|
||||||
|
|
||||||
dialog --clear --backtitle "ProxMenux" --colors \
|
_pmx_yesno "${run_title}" "$msg" 28 78
|
||||||
--title "${run_title}" \
|
|
||||||
--yesno "$msg" 28 78
|
|
||||||
[[ $? -ne 0 ]] && exit 0
|
[[ $? -ne 0 ]] && exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1724,7 +1747,7 @@ cleanup_vm_config() {
|
|||||||
local pci_slot="${SELECTED_GPU_PCI#0000:}"
|
local pci_slot="${SELECTED_GPU_PCI#0000:}"
|
||||||
pci_slot="${pci_slot%.*}" # 01:00
|
pci_slot="${pci_slot%.*}" # 01:00
|
||||||
|
|
||||||
if [[ "$VM_SWITCH_ACTION" == "keep_gpu_disable_onboot" ]]; then
|
if [[ "$SWITCH_VM_ACTION" == "keep_gpu_disable_onboot" ]]; then
|
||||||
msg_info "$(translate 'Keeping GPU in source VM config') ${SWITCH_VM_SRC}..."
|
msg_info "$(translate 'Keeping GPU in source VM config') ${SWITCH_VM_SRC}..."
|
||||||
if _vm_onboot_enabled "$SWITCH_VM_SRC"; then
|
if _vm_onboot_enabled "$SWITCH_VM_SRC"; then
|
||||||
if qm set "$SWITCH_VM_SRC" -onboot 0 >>"$LOG_FILE" 2>&1; then
|
if qm set "$SWITCH_VM_SRC" -onboot 0 >>"$LOG_FILE" 2>&1; then
|
||||||
@@ -1916,7 +1939,6 @@ main() {
|
|||||||
if [[ "$WIZARD_CALL" == "true" ]]; then
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
||||||
echo
|
echo
|
||||||
else
|
else
|
||||||
clear
|
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "${run_title}"
|
msg_title "${run_title}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
160
scripts/gpu_tpu/gpu-tpu-manual-guide.sh
Normal file
160
scripts/gpu_tpu/gpu-tpu-manual-guide.sh
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==========================================================
|
||||||
|
# ProxMenux - GPU/TPU Manual CLI Guide
|
||||||
|
# ==========================================================
|
||||||
|
# Author : MacRimi
|
||||||
|
# Copyright : (c) 2024 MacRimi
|
||||||
|
# License : GPL-3.0
|
||||||
|
# Version : 1.0
|
||||||
|
# Last Updated: 07/04/2026
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||||
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||||
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||||
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
|
source "$UTILS_FILE"
|
||||||
|
fi
|
||||||
|
load_language
|
||||||
|
initialize_cache
|
||||||
|
|
||||||
|
GREEN=$'\033[0;32m'
|
||||||
|
NC=$'\033[0m'
|
||||||
|
|
||||||
|
_cl() {
|
||||||
|
# _cl <num> <display_cmd> <description>
|
||||||
|
# Prints a numbered command line with fixed-column alignment (separator at col 52).
|
||||||
|
local num="$1" disp="$2" desc="$3"
|
||||||
|
local pad=$((47 - ${#disp}))
|
||||||
|
[[ $pad -lt 1 ]] && pad=1
|
||||||
|
local spaces
|
||||||
|
spaces=$(printf '%*s' "$pad" '')
|
||||||
|
printf " %2d) %s%s%s%s - %s\n" "$num" "$GREEN" "$disp" "$NC" "$spaces" "$desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
clear
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "GPU/TPU - Manual CLI Guide")"
|
||||||
|
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
_cl 1 "lspci -nn | grep -iE 'VGA|3D|Display'" "$(translate 'Detect GPUs in host')"
|
||||||
|
_cl 2 "lspci -nnk | grep -A3 -Ei 'VGA|3D'" "$(translate 'Show GPU kernel driver in use')"
|
||||||
|
_cl 3 "cat /proc/cmdline" "$(translate 'Check kernel params (IOMMU flags)')"
|
||||||
|
_cl 4 "dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie'" "$(translate 'Inspect passthrough/kernel events')"
|
||||||
|
_cl 5 "find /sys/kernel/iommu_groups -type l" "$(translate 'List IOMMU group mapping')"
|
||||||
|
_cl 6 "lsmod | grep -E 'vfio|nvidia|amdgpu|apex'" "$(translate 'Check loaded GPU/TPU modules')"
|
||||||
|
_cl 7 "grep -R \"vfio-pci|blacklist\" /etc/modprobe.d" "$(translate 'Review passthrough config files')"
|
||||||
|
_cl 8 "nvidia-smi" "$(translate 'Check NVIDIA driver and devices')"
|
||||||
|
_cl 9 "qm config <vmid> | grep 'hostpci|bios'" "$(translate 'Check VM passthrough settings')"
|
||||||
|
_cl 10 "pct config <ctid> | grep 'dev|lxc.cgroup2'" "$(translate 'Check LXC GPU/TPU mapping')"
|
||||||
|
_cl 11 "ls -l /dev/dri /dev/kfd /dev/nvidia*" "$(translate 'Inspect host device nodes')"
|
||||||
|
_cl 12 "qm set <vmid> --hostpci<slot> <BDF>,pcie=1" "[T] $(translate 'Assign GPU PCI function to VM')"
|
||||||
|
_cl 13 "qm set <vmid> -delete hostpci<slot>" "[T] $(translate 'Remove passthrough device from VM')"
|
||||||
|
_cl 14 "qm set <vmid> -onboot 0" "[T] $(translate 'Disable autostart on conflicting VM')"
|
||||||
|
_cl 15 "sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s|...|'" "[T] $(translate 'Enable IOMMU in GRUB or ZFS boot')"
|
||||||
|
_cl 16 "update-initramfs -u && proxmox-boot-tool" "[T] $(translate 'Apply boot/initramfs changes')"
|
||||||
|
_cl 17 "lsusb | grep Coral ; lspci | grep Unichip" "$(translate 'Check Coral USB/M.2 detection')"
|
||||||
|
echo -e " ${DEF} 0) $(translate 'Back to previous menu or Esc + Enter')${CL}"
|
||||||
|
echo
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
||||||
|
read -r user_input
|
||||||
|
|
||||||
|
if [[ "$user_input" == $'\x1b' ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
mode="exec"
|
||||||
|
case "$user_input" in
|
||||||
|
1) cmd="lspci -nn | grep -iE 'VGA compatible|3D controller|Display controller'" ;;
|
||||||
|
2) cmd="lspci -nnk | grep -A3 -Ei 'VGA compatible|3D controller|Display controller'" ;;
|
||||||
|
3) cmd="cat /proc/cmdline" ;;
|
||||||
|
4) cmd="dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie|AER|reset'" ;;
|
||||||
|
5) cmd="find /sys/kernel/iommu_groups -type l" ;;
|
||||||
|
6) cmd="lsmod | grep -E 'vfio|nvidia|amdgpu|i915|apex|gasket'" ;;
|
||||||
|
7) cmd="grep -R \"vfio-pci\\|blacklist .*nvidia\\|blacklist .*amdgpu\\|blacklist .*radeon\" /etc/modprobe.d /etc/modules /etc/default/grub /etc/kernel/cmdline 2>/dev/null" ;;
|
||||||
|
8) cmd="nvidia-smi" ;;
|
||||||
|
9)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
|
||||||
|
read -r vmid
|
||||||
|
cmd="qm config $vmid | grep -E '^(hostpci|cpu:|machine:|bios:|args:|boot:)'"
|
||||||
|
;;
|
||||||
|
10)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
|
||||||
|
read -r ctid
|
||||||
|
cmd="pct config $ctid | grep -E '^(dev[0-9]+:|lxc\\.cgroup2\\.devices\\.allow:|lxc\\.mount\\.entry:|features:)'"
|
||||||
|
;;
|
||||||
|
11) cmd="ls -l /dev/dri /dev/kfd /dev/nvidia* /dev/apex* 2>/dev/null" ;;
|
||||||
|
12)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:01:00.0): ')${CL}"; read -r bdf
|
||||||
|
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
13)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||||
|
cmd="qm set $vmid -delete hostpci${slot}"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
14)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
cmd="qm set $vmid -onboot 0"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
15)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Boot type (grub/zfs): ')${CL}"; read -r boot_type
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'CPU vendor (intel/amd): ')${CL}"; read -r cpu_vendor
|
||||||
|
case "$cpu_vendor" in
|
||||||
|
amd|AMD) iommu_param="amd_iommu=on iommu=pt" ;;
|
||||||
|
*) iommu_param="intel_iommu=on iommu=pt" ;;
|
||||||
|
esac
|
||||||
|
case "$boot_type" in
|
||||||
|
zfs|ZFS) cmd="sed -i 's/\$/ ${iommu_param}/' /etc/kernel/cmdline" ;;
|
||||||
|
*) cmd="sed -i '/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param}\"|' /etc/default/grub" ;;
|
||||||
|
esac
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
16)
|
||||||
|
cmd="update-initramfs -u -k all && (proxmox-boot-tool refresh || update-grub)"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
17) cmd="lsusb | grep -Ei '18d1:9302|1a6e:089a' ; lspci | grep -i 'Global Unichip'" ;;
|
||||||
|
0) break ;;
|
||||||
|
*)
|
||||||
|
if [[ -n "$user_input" ]]; then
|
||||||
|
cmd="$user_input"
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$mode" == "template" ]]; then
|
||||||
|
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
msg_success "$(translate 'Press ENTER to continue...')"
|
||||||
|
read -r tmp
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}> $cmd${NC}\n"
|
||||||
|
bash -c "$cmd"
|
||||||
|
echo
|
||||||
|
msg_success "$(translate 'Press ENTER to continue...')"
|
||||||
|
read -r tmp
|
||||||
|
done
|
||||||
|
|
||||||
@@ -57,6 +57,33 @@ detect_nvidia_gpus() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_gpu_not_in_vm_passthrough() {
|
||||||
|
local dev vendor driver vfio_list=""
|
||||||
|
for dev in /sys/bus/pci/devices/*; do
|
||||||
|
vendor=$(cat "$dev/vendor" 2>/dev/null)
|
||||||
|
[[ "$vendor" != "0x10de" ]] && continue
|
||||||
|
if [[ -L "$dev/driver" ]]; then
|
||||||
|
driver=$(basename "$(readlink "$dev/driver")")
|
||||||
|
if [[ "$driver" == "vfio-pci" ]]; then
|
||||||
|
vfio_list+=" • $(basename "$dev")\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$vfio_list" ]] && return 0
|
||||||
|
|
||||||
|
local msg
|
||||||
|
msg="\n$(translate "One or more NVIDIA GPUs are currently configured for VM passthrough (vfio-pci):")\n\n"
|
||||||
|
msg+="${vfio_list}\n"
|
||||||
|
msg+="$(translate "Installing host drivers while the GPU is assigned to a VM could break passthrough and destabilize the system.")\n\n"
|
||||||
|
msg+="$(translate "To install host drivers, first remove the GPU from VM passthrough configuration and reboot.")"
|
||||||
|
|
||||||
|
dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "GPU in VM Passthrough Mode")" \
|
||||||
|
--msgbox "$msg" 16 78
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
detect_driver_status() {
|
detect_driver_status() {
|
||||||
CURRENT_DRIVER_INSTALLED=false
|
CURRENT_DRIVER_INSTALLED=false
|
||||||
CURRENT_DRIVER_VERSION=""
|
CURRENT_DRIVER_VERSION=""
|
||||||
@@ -842,6 +869,7 @@ main() {
|
|||||||
|
|
||||||
detect_nvidia_gpus
|
detect_nvidia_gpus
|
||||||
detect_driver_status
|
detect_driver_status
|
||||||
|
check_gpu_not_in_vm_passthrough
|
||||||
|
|
||||||
if ! $NVIDIA_GPU_PRESENT; then
|
if ! $NVIDIA_GPU_PRESENT; then
|
||||||
dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA GPU Driver Installation')" --msgbox \
|
dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA GPU Driver Installation')" --msgbox \
|
||||||
|
|||||||
@@ -23,6 +23,37 @@ load_language
|
|||||||
initialize_cache
|
initialize_cache
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# GPU passthrough guard — block update when GPU is in VM passthrough mode
|
||||||
|
# ============================================================
|
||||||
|
check_gpu_not_in_vm_passthrough() {
|
||||||
|
local dev vendor driver vfio_list=""
|
||||||
|
for dev in /sys/bus/pci/devices/*; do
|
||||||
|
vendor=$(cat "$dev/vendor" 2>/dev/null)
|
||||||
|
[[ "$vendor" != "0x10de" ]] && continue
|
||||||
|
if [[ -L "$dev/driver" ]]; then
|
||||||
|
driver=$(basename "$(readlink "$dev/driver")")
|
||||||
|
if [[ "$driver" == "vfio-pci" ]]; then
|
||||||
|
vfio_list+=" • $(basename "$dev")\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$vfio_list" ]] && return 0
|
||||||
|
|
||||||
|
local msg
|
||||||
|
msg="\n$(translate "One or more NVIDIA GPUs are currently configured for VM passthrough (vfio-pci):")\n\n"
|
||||||
|
msg+="${vfio_list}\n"
|
||||||
|
msg+="$(translate "Updating host drivers while the GPU is assigned to a VM could break passthrough and destabilize the system.")\n\n"
|
||||||
|
msg+="$(translate "To update host drivers, first remove the GPU from VM passthrough configuration and reboot.")"
|
||||||
|
|
||||||
|
dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "GPU in VM Passthrough Mode")" \
|
||||||
|
--msgbox "$msg" 16 78
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Host NVIDIA state detection
|
# Host NVIDIA state detection
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -436,13 +467,25 @@ show_current_state_dialog() {
|
|||||||
# Restart prompt
|
# Restart prompt
|
||||||
# ============================================================
|
# ============================================================
|
||||||
restart_prompt() {
|
restart_prompt() {
|
||||||
if whiptail --title "$(translate 'NVIDIA Update')" --yesno \
|
echo
|
||||||
"$(translate 'The host driver update requires a reboot to take effect. Reboot now?')" 10 70; then
|
msg_success "$(translate 'NVIDIA driver update completed.')"
|
||||||
msg_warn "$(translate 'Restarting the server...')"
|
echo
|
||||||
|
msg_info "$(translate 'Removing no longer required packages and purging old cached updates...')"
|
||||||
|
apt-get -y autoremove >/dev/null 2>&1
|
||||||
|
apt-get -y autoclean >/dev/null 2>&1
|
||||||
|
msg_ok "$(translate 'Cleanup finished.')"
|
||||||
|
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if whiptail --title "$(translate 'Reboot Required')" \
|
||||||
|
--yesno "$(translate 'The host driver update requires a reboot to take effect. Do you want to restart now?')" 10 70; then
|
||||||
|
msg_success "$(translate 'Press Enter to continue...')"
|
||||||
|
read -r
|
||||||
|
msg_warn "$(translate 'Rebooting the system...')"
|
||||||
reboot
|
reboot
|
||||||
else
|
else
|
||||||
msg_success "$(translate 'Update complete. Please reboot the server manually.')"
|
msg_info2 "$(translate 'You can reboot later manually.')"
|
||||||
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
|
msg_success "$(translate 'Press Enter to continue...')"
|
||||||
read -r
|
read -r
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -455,6 +498,7 @@ main() {
|
|||||||
: >"$LOG_FILE"
|
: >"$LOG_FILE"
|
||||||
|
|
||||||
# ---- Phase 1: dialogs ----
|
# ---- Phase 1: dialogs ----
|
||||||
|
check_gpu_not_in_vm_passthrough
|
||||||
detect_host_nvidia
|
detect_host_nvidia
|
||||||
show_current_state_dialog
|
show_current_state_dialog
|
||||||
select_target_version
|
select_target_version
|
||||||
|
|||||||
@@ -888,6 +888,48 @@ apply_vm_action_for_lxc_mode() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_register_iommu_tool() {
|
||||||
|
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
||||||
|
command -v jq >/dev/null 2>&1 || return 0
|
||||||
|
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
||||||
|
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
||||||
|
&& mv "$tools_json.tmp" "$tools_json" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_enable_iommu_cmdline() {
|
||||||
|
local cpu_vendor
|
||||||
|
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||||
|
|
||||||
|
local iommu_param
|
||||||
|
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||||||
|
iommu_param="intel_iommu=on"
|
||||||
|
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||||||
|
iommu_param="amd_iommu=on"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cmdline_file="/etc/kernel/cmdline"
|
||||||
|
local grub_file="/etc/default/grub"
|
||||||
|
|
||||||
|
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||||
|
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
||||||
|
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||||
|
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
|
||||||
|
fi
|
||||||
|
elif [[ -f "$grub_file" ]]; then
|
||||||
|
if ! grep -q "$iommu_param" "$grub_file"; then
|
||||||
|
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||||
|
update-grub >>"$LOG_FILE" 2>&1 || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
switch_to_vm_mode() {
|
switch_to_vm_mode() {
|
||||||
detect_affected_lxc_for_selected
|
detect_affected_lxc_for_selected
|
||||||
prompt_lxc_action_for_vm_mode
|
prompt_lxc_action_for_vm_mode
|
||||||
@@ -897,6 +939,25 @@ switch_to_vm_mode() {
|
|||||||
apply_lxc_action_for_vm_mode
|
apply_lxc_action_for_vm_mode
|
||||||
|
|
||||||
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
||||||
|
|
||||||
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
|
_register_iommu_tool
|
||||||
|
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
||||||
|
elif grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||||
|
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||||
|
_register_iommu_tool
|
||||||
|
HOST_CONFIG_CHANGED=true
|
||||||
|
msg_ok "$(translate 'IOMMU already configured in kernel parameters')" | tee -a "$screen_capture"
|
||||||
|
else
|
||||||
|
if _enable_iommu_cmdline; then
|
||||||
|
_register_iommu_tool
|
||||||
|
HOST_CONFIG_CHANGED=true
|
||||||
|
msg_ok "$(translate 'IOMMU kernel parameters configured')" | tee -a "$screen_capture"
|
||||||
|
else
|
||||||
|
msg_warn "$(translate 'Could not configure IOMMU kernel parameters automatically. Configure manually and reboot.')" | tee -a "$screen_capture"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
_add_vfio_modules
|
_add_vfio_modules
|
||||||
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
|
||||||
_configure_iommu_options
|
_configure_iommu_options
|
||||||
@@ -1011,6 +1072,45 @@ switch_to_lxc_mode() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# Send notification when GPU mode switch completes
|
||||||
|
# ==========================================================
|
||||||
|
_send_gpu_mode_notification() {
|
||||||
|
local new_mode="$1"
|
||||||
|
local old_mode="$2"
|
||||||
|
local notify_script="/usr/bin/notification_manager.py"
|
||||||
|
|
||||||
|
[[ ! -f "$notify_script" ]] && return 0
|
||||||
|
|
||||||
|
local hostname_short
|
||||||
|
hostname_short=$(hostname -s)
|
||||||
|
|
||||||
|
# Build GPU list for notification
|
||||||
|
local gpu_list=""
|
||||||
|
local idx
|
||||||
|
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
||||||
|
gpu_list+="${ALL_GPU_NAMES[$idx]} (${ALL_GPU_PCIS[$idx]}), "
|
||||||
|
done
|
||||||
|
gpu_list="${gpu_list%, }"
|
||||||
|
|
||||||
|
local mode_label details
|
||||||
|
if [[ "$new_mode" == "vm" ]]; then
|
||||||
|
mode_label="GPU -> VM (VFIO passthrough)"
|
||||||
|
details="GPU(s) ready for VM passthrough. A host reboot may be required."
|
||||||
|
else
|
||||||
|
mode_label="GPU -> LXC (native driver)"
|
||||||
|
details="GPU(s) available for LXC containers with native drivers."
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "$notify_script" --action send-raw --severity INFO \
|
||||||
|
--title "${hostname_short}: GPU mode changed to ${mode_label}" \
|
||||||
|
--message "GPU passthrough mode switched.
|
||||||
|
GPU(s): ${gpu_list}
|
||||||
|
Previous: ${old_mode}
|
||||||
|
New: ${mode_label}
|
||||||
|
${details}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
confirm_plan() {
|
confirm_plan() {
|
||||||
local msg mode_line
|
local msg mode_line
|
||||||
if [[ "$TARGET_MODE" == "vm" ]]; then
|
if [[ "$TARGET_MODE" == "vm" ]]; then
|
||||||
@@ -1079,12 +1179,22 @@ main() {
|
|||||||
_set_title
|
_set_title
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
# Determine old mode before switch for notification
|
||||||
|
local old_mode_label
|
||||||
|
if [[ "$CURRENT_MODE" == "vm" ]]; then
|
||||||
|
old_mode_label="GPU -> VM (VFIO)"
|
||||||
|
else
|
||||||
|
old_mode_label="GPU -> LXC (native)"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$TARGET_MODE" == "vm" ]]; then
|
if [[ "$TARGET_MODE" == "vm" ]]; then
|
||||||
switch_to_vm_mode
|
switch_to_vm_mode
|
||||||
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
||||||
|
_send_gpu_mode_notification "vm" "$old_mode_label"
|
||||||
else
|
else
|
||||||
switch_to_lxc_mode
|
switch_to_lxc_mode
|
||||||
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
||||||
|
_send_gpu_mode_notification "lxc" "$old_mode_label"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
final_summary
|
final_summary
|
||||||
|
|||||||
@@ -816,6 +816,48 @@ apply_vm_action_for_lxc_mode() {
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Switch Mode Functions
|
# Switch Mode Functions
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
_register_iommu_tool() {
|
||||||
|
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
||||||
|
command -v jq >/dev/null 2>&1 || return 0
|
||||||
|
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
||||||
|
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
||||||
|
&& mv "$tools_json.tmp" "$tools_json" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_enable_iommu_cmdline() {
|
||||||
|
local cpu_vendor
|
||||||
|
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||||
|
|
||||||
|
local iommu_param
|
||||||
|
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||||||
|
iommu_param="intel_iommu=on"
|
||||||
|
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||||||
|
iommu_param="amd_iommu=on"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cmdline_file="/etc/kernel/cmdline"
|
||||||
|
local grub_file="/etc/default/grub"
|
||||||
|
|
||||||
|
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||||
|
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
||||||
|
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||||
|
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
|
||||||
|
fi
|
||||||
|
elif [[ -f "$grub_file" ]]; then
|
||||||
|
if ! grep -q "$iommu_param" "$grub_file"; then
|
||||||
|
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||||
|
update-grub >>"$LOG_FILE" 2>&1 || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
switch_to_vm_mode() {
|
switch_to_vm_mode() {
|
||||||
detect_affected_lxc_for_selected
|
detect_affected_lxc_for_selected
|
||||||
prompt_lxc_action_for_vm_mode
|
prompt_lxc_action_for_vm_mode
|
||||||
@@ -825,6 +867,25 @@ switch_to_vm_mode() {
|
|||||||
apply_lxc_action_for_vm_mode
|
apply_lxc_action_for_vm_mode
|
||||||
|
|
||||||
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
||||||
|
|
||||||
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
|
_register_iommu_tool
|
||||||
|
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
||||||
|
elif grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||||
|
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||||
|
_register_iommu_tool
|
||||||
|
HOST_CONFIG_CHANGED=true
|
||||||
|
msg_ok "$(translate 'IOMMU already configured in kernel parameters')" | tee -a "$screen_capture"
|
||||||
|
else
|
||||||
|
if _enable_iommu_cmdline; then
|
||||||
|
_register_iommu_tool
|
||||||
|
HOST_CONFIG_CHANGED=true
|
||||||
|
msg_ok "$(translate 'IOMMU kernel parameters configured')" | tee -a "$screen_capture"
|
||||||
|
else
|
||||||
|
msg_warn "$(translate 'Could not configure IOMMU kernel parameters automatically. Configure manually and reboot.')" | tee -a "$screen_capture"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
_add_vfio_modules
|
_add_vfio_modules
|
||||||
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
|
||||||
_configure_iommu_options
|
_configure_iommu_options
|
||||||
@@ -986,6 +1047,39 @@ final_summary() {
|
|||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Parse Arguments (supports both CLI args and env vars)
|
# Parse Arguments (supports both CLI args and env vars)
|
||||||
|
# ==========================================================
|
||||||
|
# Send notification when GPU mode switch completes
|
||||||
|
# ==========================================================
|
||||||
|
_send_gpu_mode_notification() {
|
||||||
|
local new_mode="$1"
|
||||||
|
local gpu_name="$2"
|
||||||
|
local gpu_pci="$3"
|
||||||
|
local old_mode="$4"
|
||||||
|
local notify_script="/usr/bin/notification_manager.py"
|
||||||
|
|
||||||
|
[[ ! -f "$notify_script" ]] && return 0
|
||||||
|
|
||||||
|
local hostname_short
|
||||||
|
hostname_short=$(hostname -s)
|
||||||
|
|
||||||
|
local mode_label details
|
||||||
|
if [[ "$new_mode" == "vm" ]]; then
|
||||||
|
mode_label="GPU -> VM (VFIO passthrough)"
|
||||||
|
details="GPU is now ready for VM passthrough. A host reboot may be required."
|
||||||
|
else
|
||||||
|
mode_label="GPU -> LXC (native driver)"
|
||||||
|
details="GPU is now available for LXC containers with native drivers."
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "$notify_script" --action send-raw --severity INFO \
|
||||||
|
--title "${hostname_short}: GPU mode changed to ${mode_label}" \
|
||||||
|
--message "GPU passthrough mode switched.
|
||||||
|
GPU: ${gpu_name} (${gpu_pci})
|
||||||
|
Previous: ${old_mode}
|
||||||
|
New: ${mode_label}
|
||||||
|
${details}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
parse_arguments() {
|
parse_arguments() {
|
||||||
# First, check combined parameter (format: "SLOT|MODE")
|
# First, check combined parameter (format: "SLOT|MODE")
|
||||||
@@ -1066,13 +1160,28 @@ main() {
|
|||||||
_set_title
|
_set_title
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
# Determine old mode before switch for notification
|
||||||
|
local old_mode_label
|
||||||
|
if [[ "$CURRENT_MODE" == "vm" ]]; then
|
||||||
|
old_mode_label="GPU -> VM (VFIO)"
|
||||||
|
else
|
||||||
|
old_mode_label="GPU -> LXC (native)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get GPU info for notification
|
||||||
|
local gpu_idx="${SELECTED_GPU_IDX[0]}"
|
||||||
|
local gpu_name="${ALL_GPU_NAMES[$gpu_idx]}"
|
||||||
|
local gpu_pci="${ALL_GPU_PCIS[$gpu_idx]}"
|
||||||
|
|
||||||
# Execute the switch
|
# Execute the switch
|
||||||
if [[ "$TARGET_MODE" == "vm" ]]; then
|
if [[ "$TARGET_MODE" == "vm" ]]; then
|
||||||
switch_to_vm_mode
|
switch_to_vm_mode
|
||||||
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
||||||
|
_send_gpu_mode_notification "vm" "$gpu_name" "$gpu_pci" "$old_mode_label"
|
||||||
else
|
else
|
||||||
switch_to_lxc_mode
|
switch_to_lxc_mode
|
||||||
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
||||||
|
_send_gpu_mode_notification "lxc" "$gpu_name" "$gpu_pci" "$old_mode_label"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
final_summary
|
final_summary
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ show_storage_commands() {
|
|||||||
15) cmd="lvs" ;;
|
15) cmd="lvs" ;;
|
||||||
16) cmd="cat /etc/pve/storage.cfg" ;;
|
16) cmd="cat /etc/pve/storage.cfg" ;;
|
||||||
17) cmd="pvesm status" ;;
|
17) cmd="pvesm status" ;;
|
||||||
19)
|
18)
|
||||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter storage ID: ')${CL}"
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter storage ID: ')${CL}"
|
||||||
read -r store
|
read -r store
|
||||||
cmd="pvesm list $store"
|
cmd="pvesm list $store"
|
||||||
@@ -591,42 +591,116 @@ show_update_commands() {
|
|||||||
|
|
||||||
|
|
||||||
# ===============================================================
|
# ===============================================================
|
||||||
# 06 GPU Passthrough Commands
|
# 06 GPU/TPU Passthrough Commands
|
||||||
# ===============================================================
|
# ===============================================================
|
||||||
show_gpu_commands() {
|
show_gpu_commands() {
|
||||||
while true; do
|
while true; do
|
||||||
clear
|
clear
|
||||||
echo -e "${YELLOW}$(translate 'GPU Passthrough Commands')${NC}"
|
echo -e "${YELLOW}$(translate 'GPU/TPU Passthrough Commands')${NC}"
|
||||||
echo "------------------------------------------------"
|
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
|
||||||
echo -e " 1) ${GREEN}lspci -nn | grep -i nvidia${NC} - $(translate 'List NVIDIA PCI devices')"
|
echo "------------------------------------------------------------"
|
||||||
echo -e " 2) ${GREEN}lspci -nn | grep -i vga${NC} - $(translate 'List all VGA compatible devices')"
|
echo -e " 1) ${GREEN}lspci -nn | grep -iE 'VGA|3D|Display'${NC} - $(translate 'Detect GPUs in host')"
|
||||||
echo -e " 3) ${GREEN}dmesg | grep -i vfio${NC} - $(translate 'Check VFIO module messages')"
|
echo -e " 2) ${GREEN}lspci -nnk | grep -A3 -Ei 'VGA|3D'${NC} - $(translate 'Show GPU kernel driver in use')"
|
||||||
echo -e " 4) ${GREEN}cat /etc/modprobe.d/vfio.conf${NC} - $(translate 'Review VFIO passthrough configuration')"
|
echo -e " 3) ${GREEN}cat /proc/cmdline${NC} - $(translate 'Check kernel params (IOMMU flags)')"
|
||||||
echo -e " 5) ${GREEN}update-initramfs -u${NC} - $(translate 'Apply initramfs changes (VFIO)')"
|
echo -e " 4) ${GREEN}dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie'${NC} - $(translate 'Inspect passthrough/kernel events')"
|
||||||
echo -e " 6) ${GREEN}cat /etc/default/grub${NC} - $(translate 'Review GRUB options for IOMMU')"
|
echo -e " 5) ${GREEN}find /sys/kernel/iommu_groups -type l${NC} - $(translate 'List IOMMU group mapping')"
|
||||||
echo -e " 7) ${GREEN}update-grub${NC} - $(translate 'Apply GRUB changes')"
|
echo -e " 6) ${GREEN}lsmod | grep -E 'vfio|nvidia|amdgpu|apex'${NC} - $(translate 'Check loaded GPU/TPU modules')"
|
||||||
|
echo -e " 7) ${GREEN}grep -R \"vfio-pci|blacklist\" /etc/modprobe.d${NC} - $(translate 'Review passthrough config files')"
|
||||||
|
echo -e " 8) ${GREEN}nvidia-smi${NC} - $(translate 'Check NVIDIA driver and devices')"
|
||||||
|
echo -e " 9) ${GREEN}qm config <vmid> | grep 'hostpci|bios'${NC} - [T] $(translate 'Check VM passthrough settings')"
|
||||||
|
echo -e "10) ${GREEN}pct config <ctid> | grep 'dev|lxc.cgroup2'${NC} - [T] $(translate 'Check LXC GPU/TPU mapping')"
|
||||||
|
echo -e "11) ${GREEN}ls -l /dev/dri /dev/kfd /dev/nvidia*${NC} - $(translate 'Inspect host device nodes')"
|
||||||
|
echo -e "12) ${GREEN}qm set <vmid> --hostpci<slot> <BDF>,pcie=1${NC} - [T] $(translate 'Assign GPU PCI function to VM')"
|
||||||
|
echo -e "13) ${GREEN}qm set <vmid> -delete hostpci<slot>${NC} - [T] $(translate 'Remove passthrough device from VM')"
|
||||||
|
echo -e "14) ${GREEN}qm set <vmid> -onboot 0${NC} - [T] $(translate 'Disable autostart on conflicting VM')"
|
||||||
|
echo -e "15) ${GREEN}sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s|...|'${NC} - [T] $(translate 'Enable IOMMU in GRUB or ZFS boot')"
|
||||||
|
echo -e "16) ${GREEN}update-initramfs -u && proxmox-boot-tool${NC} - [T] $(translate 'Apply boot/initramfs changes')"
|
||||||
|
echo -e "17) ${GREEN}lsusb | grep Coral ; lspci | grep Unichip${NC} - $(translate 'Check Coral USB/M.2 detection')"
|
||||||
echo -e " ${DEF}0) $(translate ' Back to previous menu or Esc + Enter')"
|
echo -e " ${DEF}0) $(translate ' Back to previous menu or Esc + Enter')"
|
||||||
echo
|
echo
|
||||||
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
||||||
read -r user_input
|
read -r user_input
|
||||||
|
|
||||||
# Check for Esc key press
|
|
||||||
if [[ "$user_input" == $'\x1b' ]]; then
|
if [[ "$user_input" == $'\x1b' ]]; then
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
mode="exec"
|
||||||
case "$user_input" in
|
case "$user_input" in
|
||||||
1) cmd="lspci -nn | grep -i nvidia" ;;
|
1) cmd="lspci -nn | grep -iE 'VGA compatible|3D controller|Display controller'" ;;
|
||||||
2) cmd="lspci -nn | grep -i vga" ;;
|
2) cmd="lspci -nnk | grep -A3 -Ei 'VGA compatible|3D controller|Display controller'" ;;
|
||||||
3) cmd="dmesg | grep -i vfio" ;;
|
3) cmd="cat /proc/cmdline" ;;
|
||||||
4) cmd="cat /etc/modprobe.d/vfio.conf" ;;
|
4) cmd="dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie|AER|reset'" ;;
|
||||||
5) cmd="update-initramfs -u" ;;
|
5) cmd="find /sys/kernel/iommu_groups -type l" ;;
|
||||||
6) cmd="cat /etc/default/grub" ;;
|
6) cmd="lsmod | grep -E 'vfio|nvidia|amdgpu|i915|apex|gasket'" ;;
|
||||||
7) cmd="update-grub" ;;
|
7) cmd="grep -R \"vfio-pci\\|blacklist .*nvidia\\|blacklist .*amdgpu\\|blacklist .*radeon\" /etc/modprobe.d /etc/modules /etc/default/grub /etc/kernel/cmdline 2>/dev/null" ;;
|
||||||
|
8) cmd="nvidia-smi" ;;
|
||||||
|
9)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
|
||||||
|
read -r vmid
|
||||||
|
cmd="qm config $vmid | grep -E '^(hostpci|cpu:|machine:|bios:|args:|boot:)'"
|
||||||
|
;;
|
||||||
|
10)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
|
||||||
|
read -r ctid
|
||||||
|
cmd="pct config $ctid | grep -E '^(dev[0-9]+:|lxc\\.cgroup2\\.devices\\.allow:|lxc\\.mount\\.entry:|features:)'"
|
||||||
|
;;
|
||||||
|
11) cmd="ls -l /dev/dri /dev/kfd /dev/nvidia* /dev/apex* 2>/dev/null" ;;
|
||||||
|
12)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:01:00.0): ')${CL}"; read -r bdf
|
||||||
|
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
13)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot (e.g. 0): ')${CL}"; read -r slot
|
||||||
|
cmd="qm set $vmid -delete hostpci${slot}"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
14)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
cmd="qm set $vmid -onboot 0"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
15)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Boot type (grub/zfs): ')${CL}"; read -r boot_type
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'CPU vendor (intel/amd): ')${CL}"; read -r cpu_vendor
|
||||||
|
case "$cpu_vendor" in
|
||||||
|
amd|AMD) iommu_param="amd_iommu=on iommu=pt" ;;
|
||||||
|
*) iommu_param="intel_iommu=on iommu=pt" ;;
|
||||||
|
esac
|
||||||
|
case "$boot_type" in
|
||||||
|
zfs|ZFS) cmd="sed -i 's/\$/ ${iommu_param}/' /etc/kernel/cmdline" ;;
|
||||||
|
*) cmd="sed -i '/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param}\"|' /etc/default/grub" ;;
|
||||||
|
esac
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
16)
|
||||||
|
cmd="update-initramfs -u -k all && (proxmox-boot-tool refresh || update-grub)"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
17) cmd="lsusb | grep -Ei '18d1:9302|1a6e:089a' ; lspci | grep -i 'Global Unichip'" ;;
|
||||||
0) break ;;
|
0) break ;;
|
||||||
*) cmd="$user_input" ;;
|
*)
|
||||||
|
if [[ -n "$user_input" ]]; then
|
||||||
|
cmd="$user_input"
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ "$mode" == "template" ]]; then
|
||||||
|
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
msg_success "$(translate 'Press ENTER to continue...')"
|
||||||
|
read -r tmp
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\n${GREEN}> $cmd${NC}\n"
|
echo -e "\n${GREEN}> $cmd${NC}\n"
|
||||||
bash -c "$cmd"
|
bash -c "$cmd"
|
||||||
echo
|
echo
|
||||||
@@ -913,7 +987,7 @@ show_tools_commands() {
|
|||||||
while true; do
|
while true; do
|
||||||
OPTION=$(dialog --stdout \
|
OPTION=$(dialog --stdout \
|
||||||
--title "$(translate 'Help and Info')" \
|
--title "$(translate 'Help and Info')" \
|
||||||
--menu "\n$(translate 'Select a category of useful commands:')" 20 70 9 \
|
--menu "$(translate 'Select a category of useful commands:')" 20 70 9 \
|
||||||
1 "$(translate 'Useful System Commands')" \
|
1 "$(translate 'Useful System Commands')" \
|
||||||
2 "$(translate 'VM and CT Management Commands')" \
|
2 "$(translate 'VM and CT Management Commands')" \
|
||||||
3 "$(translate 'Storage and Disks Commands')" \
|
3 "$(translate 'Storage and Disks Commands')" \
|
||||||
|
|||||||
@@ -134,14 +134,21 @@ function start_vm_configuration() {
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
VM_STORAGE_IOMMU_PENDING_REBOOT=0
|
VM_STORAGE_IOMMU_PENDING_REBOOT=0
|
||||||
OS_TYPE=$(dialog --backtitle "ProxMenux" \
|
WIZARD_CONFLICT_POLICY=""
|
||||||
|
WIZARD_CONFLICT_SCOPE=""
|
||||||
|
export WIZARD_CONFLICT_POLICY WIZARD_CONFLICT_SCOPE
|
||||||
|
OS_TYPE=$(dialog --colors --backtitle "ProxMenux" \
|
||||||
--title "$(translate "Select System Type")" \
|
--title "$(translate "Select System Type")" \
|
||||||
--menu "\n$(translate "Choose the type of virtual system to install:")" 20 70 10 \
|
--menu "\n$(translate "Choose the type of virtual system to install:")" 20 70 10 \
|
||||||
1 "$(translate "Create") VM System NAS" \
|
1 "$(translate "Create") VM System NAS" \
|
||||||
2 "$(translate "Create") VM System Windows" \
|
2 "$(translate "Create") VM System Windows" \
|
||||||
3 "$(translate "Create") VM System Linux" \
|
3 "$(translate "Create") VM System Linux" \
|
||||||
|
"" "" \
|
||||||
|
"" "\Z4──────────────────────────────────────────────────\Zn" \
|
||||||
|
"" "" \
|
||||||
4 "$(translate "Create") VM System macOS (OSX-PROXMOX)" \
|
4 "$(translate "Create") VM System macOS (OSX-PROXMOX)" \
|
||||||
5 "$(translate "Create") VM System Others (based Linux)" \
|
5 "$(translate "Create") VM System Others (based Linux)" \
|
||||||
|
"" "" \
|
||||||
6 "$(translate "Return to Main Menu")" \
|
6 "$(translate "Return to Main Menu")" \
|
||||||
3>&1 1>&2 2>&3)
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
|||||||
@@ -27,20 +27,24 @@ initialize_cache
|
|||||||
while true; do
|
while true; do
|
||||||
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
||||||
--title "$(translate "GPUs and Coral-TPU Menu")" \
|
--title "$(translate "GPUs and Coral-TPU Menu")" \
|
||||||
--menu "\n$(translate "Select an option:")" 25 80 15 \
|
--menu "\n$(translate "Select an option:")" 26 78 18 \
|
||||||
"" "\Z4──────────────────────── HOST ─────────────────────────\Zn" \
|
"" "\Z4──────────────────────── HOST ─────────────────────────\Zn" \
|
||||||
"1" "$(translate "Install NVIDIA Drivers on Host")" \
|
"1" "$(translate "Install NVIDIA Drivers on Host")" \
|
||||||
"2" "$(translate "Update NVIDIA Drivers (Host + LXC)")" \
|
"2" "$(translate "Update NVIDIA Drivers (Host + LXC)")" \
|
||||||
"3" "$(translate "Install/Update Coral TPU on Host")" \
|
"3" "$(translate "Install/Update Coral TPU on Host")" \
|
||||||
|
"" "" \
|
||||||
"" "\Z4──────────────────────── LXC ──────────────────────────\Zn" \
|
"" "\Z4──────────────────────── LXC ──────────────────────────\Zn" \
|
||||||
"4" "$(translate "Add GPU to LXC (Intel | AMD | NVIDIA)") \Zb\Z4Switch Mode\Zn" \
|
"4" "$(translate "Add GPU to LXC (Intel | AMD | NVIDIA)") \Zb\Z4Switch Mode\Zn" \
|
||||||
"5" "$(translate "Add Coral TPU to LXC")" \
|
"5" "$(translate "Add Coral TPU to LXC")" \
|
||||||
|
"" "" \
|
||||||
"" "\Z4──────────────────────── VM ───────────────────────────\Zn" \
|
"" "\Z4──────────────────────── VM ───────────────────────────\Zn" \
|
||||||
"6" "$(translate "Add GPU to VM (Intel | AMD | NVIDIA)") \Zb\Z4Switch Mode\Zn" \
|
"6" "$(translate "Add GPU to VM (Intel | AMD | NVIDIA)") \Zb\Z4Switch Mode\Zn" \
|
||||||
"" "" \
|
"" "" \
|
||||||
"" "\Z4──────────────────── SWICHT MODE ───────────────────────\Zn" \
|
"" "\Z4──────────────────── SWICHT MODE ───────────────────────\Zn" \
|
||||||
"7" "$(translate "Switch GPU Mode (VM <-> LXC)")" \
|
"7" "$(translate "Switch GPU Mode (VM <-> LXC)")" \
|
||||||
"" "" \
|
"" "" \
|
||||||
|
"" "\Z4────────────────────── Utilities ───────────────────────\Zn" \
|
||||||
|
"8" "$(translate "Manual CLI Guide (GPU/TPU)")" \
|
||||||
"0" "$(translate "Return to Main Menu")" \
|
"0" "$(translate "Return to Main Menu")" \
|
||||||
2>&1 >/dev/tty
|
2>&1 >/dev/tty
|
||||||
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
||||||
@@ -67,6 +71,9 @@ while true; do
|
|||||||
7)
|
7)
|
||||||
bash "$LOCAL_SCRIPTS/gpu_tpu/switch_gpu_mode.sh"
|
bash "$LOCAL_SCRIPTS/gpu_tpu/switch_gpu_mode.sh"
|
||||||
;;
|
;;
|
||||||
|
8)
|
||||||
|
bash "$LOCAL_SCRIPTS/gpu_tpu/gpu-tpu-manual-guide.sh"
|
||||||
|
;;
|
||||||
0)
|
0)
|
||||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ show_menu() {
|
|||||||
dialog --clear \
|
dialog --clear \
|
||||||
--backtitle "ProxMenux" \
|
--backtitle "ProxMenux" \
|
||||||
--title "$(translate "$menu_title")" \
|
--title "$(translate "$menu_title")" \
|
||||||
--menu "$(translate "Select an option:")" 20 70 11 \
|
--menu "\n$(translate "Select an option:")" 20 70 11 \
|
||||||
1 "$(translate "Settings post-install Proxmox")" \
|
1 "$(translate "Settings post-install Proxmox")" \
|
||||||
2 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
2 "$(translate "Hardware: GPUs and Coral-TPU")" \
|
||||||
3 "$(translate "Create VM from template or script")" \
|
3 "$(translate "Create VM from template or script")" \
|
||||||
4 "$(translate "Disk and Storage Manager")" \
|
4 "$(translate "Disk Manager")" \
|
||||||
5 "$(translate "Mount and Share Manager")" \
|
5 "$(translate "Storage & Share Manager")" \
|
||||||
6 "$(translate "Proxmox VE Helper Scripts")" \
|
6 "$(translate "Proxmox VE Helper Scripts")" \
|
||||||
7 "$(translate "Network Management")" \
|
7 "$(translate "Network Management")" \
|
||||||
8 "$(translate "Security")" \
|
8 "$(translate "Security")" \
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ while true; do
|
|||||||
SELECTED_IDX=$(dialog --backtitle "ProxMenux" \
|
SELECTED_IDX=$(dialog --backtitle "ProxMenux" \
|
||||||
--title "Proxmox VE Helper-Scripts" \
|
--title "Proxmox VE Helper-Scripts" \
|
||||||
--menu "$(translate "Select a category or search for scripts:"):" \
|
--menu "$(translate "Select a category or search for scripts:"):" \
|
||||||
20 70 14 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
22 75 15 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3) || {
|
||||||
dialog --clear --title "ProxMenux" \
|
dialog --clear --title "ProxMenux" \
|
||||||
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
|
--msgbox "\n\n$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:")\n\nhttps://community-scripts.github.io/ProxmoxVE" 15 70
|
||||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
@@ -440,7 +440,7 @@ while true; do
|
|||||||
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \
|
SCRIPT_INDEX=$(dialog --colors --backtitle "ProxMenux" \
|
||||||
--title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \
|
--title "$(translate "Scripts in") ${CATEGORY_NAMES[$SELECTED]}" \
|
||||||
--menu "$(translate "Choose a script to execute:"):" \
|
--menu "$(translate "Choose a script to execute:"):" \
|
||||||
20 70 14 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
|
22 75 15 "${SCRIPTS[@]}" 3>&1 1>&2 2>&3) || break
|
||||||
|
|
||||||
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
|
SCRIPT_SELECTED="${INDEX_TO_SLUG[$SCRIPT_INDEX]}"
|
||||||
run_script_by_slug "$SCRIPT_SELECTED"
|
run_script_by_slug "$SCRIPT_SELECTED"
|
||||||
|
|||||||
@@ -26,13 +26,12 @@ initialize_cache
|
|||||||
security_menu() {
|
security_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
local menu_text
|
local menu_text
|
||||||
menu_text="\n$(translate 'Security tools for hardening and auditing your Proxmox VE system.')\n\n"
|
menu_text+="\n$(translate 'Select an option:')"
|
||||||
menu_text+="$(translate 'Select an option:')"
|
|
||||||
|
|
||||||
local OPTION
|
local OPTION
|
||||||
OPTION=$(dialog --backtitle "ProxMenux" \
|
OPTION=$(dialog --backtitle "ProxMenux" \
|
||||||
--title "$(translate "$SCRIPT_TITLE")" \
|
--title "$(translate "$SCRIPT_TITLE")" \
|
||||||
--menu "$menu_text" 18 70 4 \
|
--menu "$menu_text" 20 70 10 \
|
||||||
"1" "$(translate 'Fail2Ban - Intrusion Prevention')" \
|
"1" "$(translate 'Fail2Ban - Intrusion Prevention')" \
|
||||||
"2" "$(translate 'Lynis - Security Audit')" \
|
"2" "$(translate 'Lynis - Security Audit')" \
|
||||||
3>&1 1>&2 2>&3) || OPTION="0"
|
3>&1 1>&2 2>&3) || OPTION="0"
|
||||||
|
|||||||
@@ -26,21 +26,22 @@ initialize_cache
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
||||||
--title "$(translate "Mount and Share Manager")" \
|
--title "$(translate "Storage & Share Manager")" \
|
||||||
--menu "\n$(translate "Select an option:")" 25 80 15 \
|
--menu "\n$(translate "Select an option:")" 26 78 17 \
|
||||||
"" "\Z4──────────────────────── HOST ─────────────────────────\Zn" \
|
"" "\Z4──────────────────────── HOST ─────────────────────────\Zn" \
|
||||||
"1" "$(translate "Configure NFS shared on Host")" \
|
"1" "$(translate "Configure NFS shared on Host")" \
|
||||||
"2" "$(translate "Configure Samba shared on Host")" \
|
"2" "$(translate "Configure Samba shared on Host")" \
|
||||||
"3" "$(translate "Configure Local Shared on Host")" \
|
"3" "$(translate "Configure Local Shared on Host")" \
|
||||||
"9" "$(translate "Add Local Disk as Proxmox Storage")" \
|
"4" "$(translate "Add Local Disk as Proxmox Storage")" \
|
||||||
"10" "$(translate "Add iSCSI Target as Proxmox Storage")" \
|
"5" "$(translate "Add iSCSI Target as Proxmox Storage")" \
|
||||||
"" "\Z4──────────────────────── LXC ─────────────────────────\Zn" \
|
|
||||||
"4" "$(translate "Configure LXC Mount Points (Host ↔ Container)")" \
|
|
||||||
"" "" \
|
"" "" \
|
||||||
"5" "$(translate "Configure NFS Client in LXC (only privileged)")" \
|
"" "\Z4──────────────────────── LXC ─────────────────────────\Zn" \
|
||||||
"6" "$(translate "Configure Samba Client in LXC (only privileged)")" \
|
"6" "$(translate "Configure LXC Mount Points (Host ↔ Container)")" \
|
||||||
"7" "$(translate "Configure NFS Server in LXC (only privileged)")" \
|
"" "" \
|
||||||
"8" "$(translate "configure Samba Server in LXC (only privileged)")" \
|
"7" "$(translate "Configure NFS Client in LXC (only privileged)")" \
|
||||||
|
"8" "$(translate "Configure Samba Client in LXC (only privileged)")" \
|
||||||
|
"9" "$(translate "Configure NFS Server in LXC (only privileged)")" \
|
||||||
|
"10" "$(translate "configure Samba Server in LXC (only privileged)")" \
|
||||||
"" "" \
|
"" "" \
|
||||||
"h" "$(translate "Help & Info (commands)")" \
|
"h" "$(translate "Help & Info (commands)")" \
|
||||||
"0" "$(translate "Return to Main Menu")" \
|
"0" "$(translate "Return to Main Menu")" \
|
||||||
@@ -62,25 +63,25 @@ while true; do
|
|||||||
3)
|
3)
|
||||||
bash "$LOCAL_SCRIPTS/share/local-shared-manager.sh"
|
bash "$LOCAL_SCRIPTS/share/local-shared-manager.sh"
|
||||||
;;
|
;;
|
||||||
9)
|
4)
|
||||||
bash "$LOCAL_SCRIPTS/share/disk_host.sh"
|
bash "$LOCAL_SCRIPTS/share/disk_host.sh"
|
||||||
;;
|
;;
|
||||||
10)
|
5)
|
||||||
bash "$LOCAL_SCRIPTS/share/iscsi_host.sh"
|
bash "$LOCAL_SCRIPTS/share/iscsi_host.sh"
|
||||||
;;
|
;;
|
||||||
4)
|
6)
|
||||||
bash "$LOCAL_SCRIPTS/share/lxc-mount-manager_minimal.sh"
|
bash "$LOCAL_SCRIPTS/share/lxc-mount-manager_minimal.sh"
|
||||||
;;
|
;;
|
||||||
5)
|
7)
|
||||||
bash "$LOCAL_SCRIPTS/share/nfs_client.sh"
|
bash "$LOCAL_SCRIPTS/share/nfs_client.sh"
|
||||||
;;
|
;;
|
||||||
6)
|
8)
|
||||||
bash "$LOCAL_SCRIPTS/share/samba_client.sh"
|
bash "$LOCAL_SCRIPTS/share/samba_client.sh"
|
||||||
;;
|
;;
|
||||||
7)
|
9)
|
||||||
bash "$LOCAL_SCRIPTS/share/nfs_lxc_server.sh"
|
bash "$LOCAL_SCRIPTS/share/nfs_lxc_server.sh"
|
||||||
;;
|
;;
|
||||||
8)
|
10)
|
||||||
bash "$LOCAL_SCRIPTS/share/samba_lxc_server.sh"
|
bash "$LOCAL_SCRIPTS/share/samba_lxc_server.sh"
|
||||||
;;
|
;;
|
||||||
h)
|
h)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# Version : 2.0
|
# Version : 2.0
|
||||||
# Last Updated: 06/04/2026
|
# Last Updated: 07/04/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -30,15 +30,21 @@ initialize_cache
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
OPTION=$(dialog --colors --backtitle "ProxMenux" \
|
||||||
--title "$(translate "Disk and Storage Manager Menu")" \
|
--title "$(translate "Disk Manager")" \
|
||||||
--menu "\n$(translate "Select an option:")" 24 84 14 \
|
--menu "\n$(translate "Select an option:")" 24 78 16 \
|
||||||
"" "\Z4──────────────────────── VM ───────────────────────────\Zn" \
|
"" "\Z4──────────────────────── VM ───────────────────────────\Zn" \
|
||||||
"1" "$(translate "Import Disk to VM")" \
|
"1" "$(translate "Import Disk to VM")" \
|
||||||
"2" "$(translate "Import Disk Image to VM")" \
|
"2" "$(translate "Import Disk Image to VM")" \
|
||||||
"3" "$(translate "Add Controller or NVMe PCIe to VM")" \
|
"3" "$(translate "Add Controller or NVMe PCIe to VM")" \
|
||||||
|
"" "" \
|
||||||
"" "\Z4──────────────────────── LXC ──────────────────────────\Zn" \
|
"" "\Z4──────────────────────── LXC ──────────────────────────\Zn" \
|
||||||
"4" "$(translate "Import Disk to LXC")" \
|
"4" "$(translate "Import Disk to LXC")" \
|
||||||
"" "" \
|
"" "" \
|
||||||
|
"" "\Z4────────────────────── Utilities ───────────────────────\Zn" \
|
||||||
|
"5" "$(translate "Format / Wipe Physical Disk (Safe)")" \
|
||||||
|
"6" "$(translate "SMART Disk Health & Test")" \
|
||||||
|
"7" "$(translate "Manual CLI Guide (Disk and Storage Manager)")" \
|
||||||
|
"" "" \
|
||||||
"0" "$(translate "Return to Main Menu")" \
|
"0" "$(translate "Return to Main Menu")" \
|
||||||
2>&1 >/dev/tty
|
2>&1 >/dev/tty
|
||||||
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
) || { exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"; }
|
||||||
@@ -56,6 +62,15 @@ while true; do
|
|||||||
4)
|
4)
|
||||||
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
|
bash "$LOCAL_SCRIPTS/storage/disk-passthrough_ct.sh"
|
||||||
;;
|
;;
|
||||||
|
5)
|
||||||
|
bash "$LOCAL_SCRIPTS/storage/format-disk.sh"
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
bash "$LOCAL_SCRIPTS/storage/smart-disk-test.sh"
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
bash "$LOCAL_SCRIPTS/storage/disk-storage-manual-guide.sh"
|
||||||
|
;;
|
||||||
0)
|
0)
|
||||||
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ initialize_cache
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
OPTION=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Utilities Menu")" \
|
OPTION=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Utilities Menu")" \
|
||||||
--menu "$(translate "Select an option:")" 20 70 8 \
|
--menu "\n$(translate "Select an option:")" 20 70 11 \
|
||||||
"1" "$(translate "UUp Dump ISO creator Custom")" \
|
"1" "$(translate "UUp Dump ISO creator Custom")" \
|
||||||
"2" "$(translate "System Utilities Installer")" \
|
"2" "$(translate "System Utilities Installer")" \
|
||||||
"3" "$(translate "Proxmox System Update")" \
|
"3" "$(translate "Proxmox System Update")" \
|
||||||
"4" "$(translate "Upgrade PVE 8 to PVE 9")" \
|
"4" "$(translate "Upgrade PVE 8 to PVE 9")" \
|
||||||
"5" "$(translate "Return to Main Menu")" \
|
"5" "$(translate "Export VM to OVA or OVF")" \
|
||||||
|
"6" "$(translate "Import VM from OVA or OVF")" \
|
||||||
|
"7" "$(translate "Return to Main Menu")" \
|
||||||
2>&1 >/dev/tty)
|
2>&1 >/dev/tty)
|
||||||
|
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
@@ -76,8 +78,20 @@ initialize_cache
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
5) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
5)
|
||||||
|
bash "$LOCAL_SCRIPTS/utilities/export_vm_ova_ovf.sh"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
bash "$LOCAL_SCRIPTS/utilities/import_vm_ova_ovf.sh"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
7) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||||
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
*) exec bash "$LOCAL_SCRIPTS/menus/main_menu.sh" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.5
|
# Version : 1.6
|
||||||
# Last Updated: 04/08/2025
|
# Last Updated: 07/04/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
@@ -29,11 +29,14 @@ show_command() {
|
|||||||
local command="$3"
|
local command="$3"
|
||||||
local note="$4"
|
local note="$4"
|
||||||
local command_extra="$5"
|
local command_extra="$5"
|
||||||
|
|
||||||
echo -e "${BGN}${step}.${CL} ${BL}${description}${CL}"
|
echo -e " ${DARK_GRAY}────────────────────────────────────────────────${CL}"
|
||||||
|
echo -e " ${BGN}${step}.${CL} ${description}"
|
||||||
|
echo ""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo -e "${TAB}${line}"
|
||||||
|
done <<< "$(echo -e "$command")"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${TAB}${command}"
|
|
||||||
echo -e
|
|
||||||
[[ -n "$note" ]] && echo -e "${TAB}${DARK_GRAY}${note}${CL}"
|
[[ -n "$note" ]] && echo -e "${TAB}${DARK_GRAY}${note}${CL}"
|
||||||
[[ -n "$command_extra" ]] && echo -e "${TAB}${YW}${command_extra}${CL}"
|
[[ -n "$command_extra" ]] && echo -e "${TAB}${YW}${command_extra}${CL}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -43,10 +46,10 @@ show_how_to_enter_lxc() {
|
|||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "How to Access an LXC Terminal from Proxmox Host")"
|
msg_title "$(translate "How to Access an LXC Terminal from Proxmox Host")"
|
||||||
|
|
||||||
msg_info2 "$(translate "Use these commands on your Proxmox host to access an LXC container's terminal:")"
|
msg_info2 "$(translate "Use these commands on your Proxmox host to access an LXC container's terminal:")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Get a list of all your containers:")" \
|
"$(translate "Get a list of all your containers:")" \
|
||||||
"pct list" \
|
"pct list" \
|
||||||
@@ -54,93 +57,203 @@ show_how_to_enter_lxc() {
|
|||||||
""
|
""
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Enter the container's terminal")" \
|
"$(translate "Enter the container terminal:")" \
|
||||||
"pct enter ${CUS}<container-id>${CL}" \
|
"pct enter ${CUS}<container-id>${CL}" \
|
||||||
"$(translate "Replace <container-id> with the actual ID.")"\
|
"$(translate "Replace <container-id> with the actual ID.")" \
|
||||||
"$(translate "For example: pct enter 101")"
|
"$(translate "For example: pct enter 101")"
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "To exit the container's terminal, press:")" \
|
"$(translate "Exit the container terminal:")" \
|
||||||
"CTRL + D" \
|
"exit" \
|
||||||
"" \
|
"$(translate "Or press CTRL + D")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
}
|
}
|
||||||
|
|
||||||
show_host_mount_resources_help() {
|
show_host_storage_help() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "Mount Remote Resources on Proxmox Host")"
|
msg_title "$(translate "Host Storage (NFS / Samba via Proxmox)")"
|
||||||
|
|
||||||
msg_info2 "$(translate "How to mount NFS and Samba shares directly on the Proxmox host. Proxmox already has the necessary tools installed.")"
|
|
||||||
echo -e
|
|
||||||
|
|
||||||
echo -e "${BOLD}${BL}=== MOUNT NFS SHARE ===${CL}"
|
msg_info2 "$(translate "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.")"
|
||||||
|
msg_info2 "$(translate "This means Proxmox handles mount lifecycle natively (no manual /etc/fstab needed for NFS/CIFS host storages).")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
|
echo -e "${BOLD}${BL}=== NFS AS PROXMOX STORAGE ===${CL}"
|
||||||
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Create mount point:")" \
|
"$(translate "Add NFS storage:")" \
|
||||||
"mkdir -p ${CUS}/mnt/nfs_share${CL}" \
|
"pvesm add nfs ${CUS}<storage-id>${CL} --server ${CUS}<nfs-server-ip>${CL} --export ${CUS}</export/path>${CL} --content ${CUS}import,backup,iso,vztmpl,images,snippets${CL}" \
|
||||||
"$(translate "Replace with your preferred path.")" \
|
"$(translate "Use content types according to your use case.")" \
|
||||||
""
|
"$(translate "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup")"
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Mount NFS share:")" \
|
"$(translate "List configured storages:")" \
|
||||||
"mount -t nfs ${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL}" \
|
"pvesm status" \
|
||||||
"$(translate "Replace IP and paths with your values.")" \
|
"$(translate "Shows status and type (nfs/cifs/dir/iscsi...).")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "Make permanent (optional):")" \
|
"$(translate "Remove NFS storage:")" \
|
||||||
"echo '${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL} nfs4 rw,hard,intr,_netdev,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0' >> /etc/fstab" \
|
"pvesm remove ${CUS}<storage-id>${CL}" \
|
||||||
"$(translate "_netdev waits for network before mounting.")" \
|
"$(translate "Only removes storage definition, not remote data.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e "${BOLD}${BL}=== MOUNT SAMBA SHARE ===${CL}"
|
echo -e "${BOLD}${BL}=== SAMBA/CIFS AS PROXMOX STORAGE ===${CL}"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "4" \
|
show_command "4" \
|
||||||
"$(translate "Create mount point:")" \
|
"$(translate "Add CIFS storage:")" \
|
||||||
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
|
"pvesm add cifs ${CUS}<storage-id>${CL} --server ${CUS}<samba-server-ip>${CL} --share ${CUS}<share-name>${CL} --username ${CUS}<user>${CL} --password ${CUS}<pass>${CL} --content ${CUS}import,backup,iso,vztmpl,images,snippets${CL}" \
|
||||||
"$(translate "Replace with your preferred path.")" \
|
"$(translate "For guest shares add: --options guest")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "5" \
|
show_command "5" \
|
||||||
"$(translate "Mount Samba share:")" \
|
"$(translate "Inspect storage config block:")" \
|
||||||
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}sharename${CL} ${CUS}/mnt/samba_share${CL} -o username=${CUS}user${CL}" \
|
"sed -n '/^${CUS}<storage-id>${CL}:/,/^[^ ]/p' /etc/pve/storage.cfg" \
|
||||||
"$(translate "You will be prompted for password. Replace IP, share and user.")" \
|
"$(translate "Useful to verify options/content after script execution.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "6" \
|
show_command "6" \
|
||||||
"$(translate "Make permanent (optional):")" \
|
"$(translate "Remove CIFS storage:")" \
|
||||||
"echo '//${CUS}192.168.1.100${CL}/${CUS}sharename${CL} ${CUS}/mnt/samba_share${CL} cifs username=${CUS}user${CL},password=${CUS}pass${CL},_netdev 0 0' >> /etc/fstab" \
|
"pvesm remove ${CUS}<storage-id>${CL}" \
|
||||||
"$(translate "Replace with your credentials.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e "${BOLD}${BL}=== CREATE LOCAL DIRECTORY ===${CL}"
|
echo -e ""
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
|
read -r
|
||||||
|
}
|
||||||
|
|
||||||
|
show_local_share_help() {
|
||||||
|
clear
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Local Shared Directory on Host")"
|
||||||
|
|
||||||
|
msg_info2 "$(translate "Equivalent manual flow used by Local Shared Manager.")"
|
||||||
|
msg_info2 "$(translate "No group creation required — uses world-writable sticky bit permissions.")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "7" \
|
show_command "1" \
|
||||||
"$(translate "Create directory:")" \
|
"$(translate "Create shared directory:")" \
|
||||||
"mkdir -p ${CUS}/mnt/local_share${CL}" \
|
"mkdir -p ${CUS}/mnt/shared${CL}" \
|
||||||
"$(translate "Creates a local directory on Proxmox host.")" \
|
"$(translate "Choose any host path you want to share with CTs.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "8" \
|
show_command "2" \
|
||||||
"$(translate "Set permissions:")" \
|
"$(translate "Set ownership and permissions:")" \
|
||||||
"chmod 755 ${CUS}/mnt/local_share${CL}" \
|
"chown root:root ${CUS}/mnt/shared${CL}\nchmod 1777 ${CUS}/mnt/shared${CL}" \
|
||||||
"$(translate "Sets basic read/write permissions.")" \
|
"$(translate "1777 = sticky bit + rwx for all. No shared group needed.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "9" \
|
show_command "3" \
|
||||||
"$(translate "Verify mounts:")" \
|
"$(translate "Optional: apply default ACL so new files inherit permissions:")" \
|
||||||
"df -h" \
|
"setfacl -R -m d:u::rwx,d:g::rwx,d:o::rwx,m::rwx ${CUS}/mnt/shared${CL}" \
|
||||||
"$(translate "Shows all mounted filesystems.")" \
|
"$(translate "Requires acl package. Skip if setfacl is not available.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
|
show_command "4" \
|
||||||
|
"$(translate "Optional: register this path as Proxmox dir storage:")" \
|
||||||
|
"pvesm add dir ${CUS}<storage-id>${CL} --path ${CUS}/mnt/shared${CL} --content ${CUS}backup,iso,vztmpl,snippets${CL}" \
|
||||||
|
"$(translate "Use images only if the directory is on suitable storage.")" \
|
||||||
|
""
|
||||||
|
|
||||||
|
echo -e ""
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
|
read -r
|
||||||
|
}
|
||||||
|
|
||||||
|
show_disk_host_help() {
|
||||||
|
clear
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Add Local Disk as Proxmox Storage")"
|
||||||
|
|
||||||
|
msg_info2 "$(translate "Equivalent manual flow of disk_host.sh: partition, format, mount, persist, register in Proxmox.")"
|
||||||
|
echo -e
|
||||||
|
|
||||||
|
show_command "1" \
|
||||||
|
"$(translate "Identify candidate disk (never use system disk):")" \
|
||||||
|
"lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" \
|
||||||
|
"$(translate "Example target: /dev/sdb")" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "2" \
|
||||||
|
"$(translate "Wipe old signatures and partition table (DESTRUCTIVE):")" \
|
||||||
|
"wipefs -a ${CUS}/dev/sdb${CL}\nsgdisk --zap-all ${CUS}/dev/sdb${CL}" \
|
||||||
|
"$(translate "This erases existing metadata.")" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "3" \
|
||||||
|
"$(translate "Create GPT and one partition:")" \
|
||||||
|
"parted -s ${CUS}/dev/sdb${CL} mklabel gpt\nparted -s ${CUS}/dev/sdb${CL} mkpart primary 0% 100%" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "4" \
|
||||||
|
"$(translate "Format partition:")" \
|
||||||
|
"mkfs.ext4 -F ${CUS}/dev/sdb1${CL}\n# or\nmkfs.xfs -f ${CUS}/dev/sdb1${CL}" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "5" \
|
||||||
|
"$(translate "Mount and persist with UUID:")" \
|
||||||
|
"mkdir -p ${CUS}/mnt/disk-sdb${CL}\nmount ${CUS}/dev/sdb1${CL} ${CUS}/mnt/disk-sdb${CL}\nblkid ${CUS}/dev/sdb1${CL}\n# Add UUID line to /etc/fstab" \
|
||||||
|
"$(translate "Using UUID is recommended over /dev/sdX.")" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "6" \
|
||||||
|
"$(translate "Register mount path in Proxmox:")" \
|
||||||
|
"pvesm add dir ${CUS}<storage-id>${CL} --path ${CUS}/mnt/disk-sdb${CL} --content ${CUS}images,backup${CL}" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
|
echo -e ""
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
|
read -r
|
||||||
|
}
|
||||||
|
|
||||||
|
show_iscsi_host_help() {
|
||||||
|
clear
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Add iSCSI Target as Proxmox Storage")"
|
||||||
|
|
||||||
|
msg_info2 "$(translate "Equivalent manual flow of iscsi_host.sh.")"
|
||||||
|
echo -e
|
||||||
|
|
||||||
|
show_command "1" \
|
||||||
|
"$(translate "Install and start iSCSI initiator:")" \
|
||||||
|
"apt-get update && apt-get install -y open-iscsi\nsystemctl enable --now iscsid" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "2" \
|
||||||
|
"$(translate "Discover targets on portal:")" \
|
||||||
|
"iscsiadm -m discovery -t sendtargets -p ${CUS}<portal-ip>:3260${CL}" \
|
||||||
|
"$(translate "This returns available IQNs.")" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "3" \
|
||||||
|
"$(translate "Add iSCSI storage in Proxmox:")" \
|
||||||
|
"pvesm add iscsi ${CUS}<storage-id>${CL} --portal ${CUS}<portal-ip>:3260${CL} --target ${CUS}<target-iqn>${CL} --content images" \
|
||||||
|
"$(translate "Content is usually images for VM block devices.")" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "4" \
|
||||||
|
"$(translate "Verify iSCSI sessions and storage status:")" \
|
||||||
|
"iscsiadm -m session\npvesm status" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "5" \
|
||||||
|
"$(translate "Remove iSCSI storage definition:")" \
|
||||||
|
"pvesm remove ${CUS}<storage-id>${CL}" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -149,42 +262,42 @@ show_host_mount_resources_help() {
|
|||||||
show_host_to_lxc_mount_help() {
|
show_host_to_lxc_mount_help() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "Mount Host Directory to LXC Container")"
|
msg_title "$(translate "Host Directory to LXC Mount Point")"
|
||||||
|
|
||||||
msg_info2 "$(translate "How to mount a Proxmox host directory into an LXC container. Execute these commands on the Proxmox host.")"
|
msg_info2 "$(translate "Current script uses native bind mounts with pct set -mpX.")"
|
||||||
echo -e
|
msg_info2 "$(translate "Safe design: no automatic ACL/ownership mutation on host or CT.")"
|
||||||
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Add mount point to container:")" \
|
"$(translate "List containers:")" \
|
||||||
"pct set ${CUS}<container-id>${CL} -mp0 ${CUS}/host/directory${CL},mp=${CUS}/container/path${CL},backup=0,shared=1" \
|
"pct list" \
|
||||||
"$(translate "Replace container-id, host directory and container path.")" \
|
"" \
|
||||||
"$(translate "Example: pct set 101 -mp0 /mnt/shared,mp=/mnt/shared,,backup=0,shared=1")"
|
""
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Restart container:")" \
|
"$(translate "Add bind mount to container:")" \
|
||||||
"pct reboot ${CUS}<container-id>${CL}" \
|
"pct set ${CUS}<ctid>${CL} -mp0 ${CUS}/host/path${CL},mp=${CUS}/container/path${CL},backup=0,shared=1" \
|
||||||
"$(translate "Required to activate the mount point.")" \
|
"$(translate "Use mp1/mp2/... for extra mount points.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "Verify mount inside container:")" \
|
"$(translate "Check resulting config:")" \
|
||||||
"pct enter ${CUS}<container-id>${CL}
|
"pct config ${CUS}<ctid>${CL} | grep '^mp'" \
|
||||||
df -h | grep ${CUS}/container/path${CL}" \
|
"" \
|
||||||
"$(translate "Check if the directory is mounted.")" \
|
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "4" \
|
show_command "4" \
|
||||||
"$(translate "Remove mount point (if needed):")" \
|
"$(translate "Remove mount point:")" \
|
||||||
"pct set ${CUS}<container-id>${CL} --delete mp0" \
|
"pct set ${CUS}<ctid>${CL} --delete mp0" \
|
||||||
"$(translate "Removes the mount point. Use mp1, mp2, etc. for other mounts.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e "${BOR}"
|
show_command "5" \
|
||||||
echo -e "${BOLD}$(translate "Notes:")${CL}"
|
"$(translate "Verify inside container:")" \
|
||||||
echo -e "${TAB}${BGN}$(translate "Mount indices:")${CL} ${BL}Use mp0, mp1, mp2, etc. for multiple mounts${CL}"
|
"pct enter ${CUS}<ctid>${CL}\ndf -h" \
|
||||||
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}May need adjustment depending on directory type${CL}"
|
"$(translate "Confirm the mount path is visible.")" \
|
||||||
echo -e "${TAB}${BGN}$(translate "Container types:")${CL} ${BL}Works with both privileged and unprivileged containers${CL}"
|
""
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -193,67 +306,41 @@ show_host_to_lxc_mount_help() {
|
|||||||
show_nfs_server_help() {
|
show_nfs_server_help() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "NFS Server Installation")"
|
msg_title "$(translate "NFS Server in LXC (Privileged)")"
|
||||||
|
|
||||||
msg_info2 "$(translate "How to install and configure an NFS server in an LXC container.")"
|
msg_warn "$(translate "Use a privileged LXC for NFS server/client workflows.")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Update and install packages:")" \
|
"$(translate "Install server packages inside CT:")" \
|
||||||
"apt-get update && apt-get install -y nfs-kernel-server" \
|
"apt-get update && apt-get install -y nfs-kernel-server nfs-common rpcbind" \
|
||||||
"" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Create export directory:")" \
|
"$(translate "Create export directory:")" \
|
||||||
"mkdir -p ${CUS}/mnt/nfs_export${CL}" \
|
"mkdir -p ${CUS}/mnt/nfs_export${CL}\nchmod 755 ${CUS}/mnt/nfs_export${CL}" \
|
||||||
"$(translate "Replace with your preferred path.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "Set directory permissions:")" \
|
"$(translate "Add export rule:")" \
|
||||||
"chmod 755 ${CUS}/mnt/nfs_export${CL}" \
|
|
||||||
"" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "4.1" \
|
|
||||||
"$(translate "Configure exports (safe root_squash):")" \
|
|
||||||
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,root_squash)' >> /etc/exports" \
|
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,root_squash)' >> /etc/exports" \
|
||||||
"$(translate "Replace directory path and network range.")" \
|
"$(translate "Adjust network/CIDR to your environment.")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "4.2" \
|
show_command "4" \
|
||||||
"$(translate "Or Configure exports (map all users):")" \
|
"$(translate "Apply and restart services:")" \
|
||||||
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=0)' >> /etc/exports" \
|
"exportfs -ra\nsystemctl restart rpcbind nfs-kernel-server\nsystemctl enable rpcbind nfs-kernel-server" \
|
||||||
"$(translate "Replace directory path and network range.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
|
|
||||||
show_command "5" \
|
show_command "5" \
|
||||||
"$(translate "Apply configuration:")" \
|
"$(translate "Verify active exports:")" \
|
||||||
"exportfs -ra" \
|
|
||||||
"" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "6" \
|
|
||||||
"$(translate "Start and enable service:")" \
|
|
||||||
"systemctl restart nfs-kernel-server
|
|
||||||
systemctl enable nfs-kernel-server" \
|
|
||||||
"" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "7" \
|
|
||||||
"$(translate "Verify exports:")" \
|
|
||||||
"showmount -e localhost" \
|
"showmount -e localhost" \
|
||||||
"$(translate "Shows available NFS exports.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e "${BOR}"
|
|
||||||
echo -e "${BOLD}$(translate "Export Options:")${CL}"
|
|
||||||
echo -e "${TAB}${BGN}$(translate "rw:")${CL} ${BL}Read-write access${CL}"
|
|
||||||
echo -e "${TAB}${BGN}$(translate "sync:")${CL} ${BL}Synchronous writes${CL}"
|
|
||||||
echo -e "${TAB}${BGN}$(translate "no_subtree_check:")${CL} ${BL}Improves performance${CL}"
|
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -262,67 +349,47 @@ show_nfs_server_help() {
|
|||||||
show_samba_server_help() {
|
show_samba_server_help() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "Samba Server Installation")"
|
msg_title "$(translate "Samba Server in LXC (Privileged)")"
|
||||||
|
|
||||||
msg_info2 "$(translate "How to install and configure a Samba server in an LXC container.")"
|
msg_warn "$(translate "Use a privileged LXC for Samba client/server workflows.")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Update and install packages:")" \
|
"$(translate "Install Samba inside CT:")" \
|
||||||
"apt-get update && apt-get install -y samba" \
|
"apt-get update && apt-get install -y samba samba-common-bin acl" \
|
||||||
"" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Create share directory:")" \
|
"$(translate "Create share directory:")" \
|
||||||
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
|
"mkdir -p ${CUS}/mnt/samba_share${CL}\nchmod 755 ${CUS}/mnt/samba_share${CL}" \
|
||||||
"$(translate "Replace with your preferred path.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "Set directory permissions:")" \
|
|
||||||
"chmod 755 ${CUS}/mnt/samba_share${CL}" \
|
|
||||||
"" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "4" \
|
|
||||||
"$(translate "Create Samba user:")" \
|
"$(translate "Create Samba user:")" \
|
||||||
"adduser ${CUS}sambauser${CL}
|
"adduser ${CUS}sambauser${CL}\nsmbpasswd -a ${CUS}sambauser${CL}" \
|
||||||
smbpasswd -a ${CUS}sambauser${CL}" \
|
|
||||||
"$(translate "Replace with your username. You'll be prompted for password.")" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "5" \
|
|
||||||
"$(translate "Configure share:")" \
|
|
||||||
"cat >> /etc/samba/smb.conf << EOF
|
|
||||||
[shared]
|
|
||||||
comment = Shared folder
|
|
||||||
path = ${CUS}/mnt/samba_share${CL}
|
|
||||||
read only = no
|
|
||||||
browseable = yes
|
|
||||||
valid users = ${CUS}sambauser${CL}
|
|
||||||
EOF" \
|
|
||||||
"$(translate "Replace path and username.")" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "6" \
|
|
||||||
"$(translate "Restart and enable service:")" \
|
|
||||||
"systemctl restart smbd
|
|
||||||
systemctl enable smbd" \
|
|
||||||
"" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "7" \
|
show_command "4" \
|
||||||
"$(translate "Test configuration:")" \
|
"$(translate "Add share block in /etc/samba/smb.conf:")" \
|
||||||
"smbclient -L localhost -U ${CUS}sambauser${CL}" \
|
"cat >> /etc/samba/smb.conf << 'EOF'\n[shared]\n path = /mnt/samba_share\n browseable = yes\n read only = no\n valid users = sambauser\nEOF" \
|
||||||
"$(translate "Lists available shares. You'll be prompted for password.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e "${BOR}"
|
show_command "5" \
|
||||||
echo -e "${BOLD}$(translate "Connection Examples:")${CL}"
|
"$(translate "Restart and enable Samba:")" \
|
||||||
echo -e "${TAB}${BGN}$(translate "Windows:")${CL} ${YW}\\\\<server-ip>\\shared${CL}"
|
"systemctl restart smbd\nsystemctl enable smbd" \
|
||||||
echo -e "${TAB}${BGN}$(translate "Linux:")${CL} ${YW}smbclient //server-ip/shared -U sambauser${CL}"
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
|
show_command "6" \
|
||||||
|
"$(translate "Test share visibility:")" \
|
||||||
|
"smbclient -L localhost -U ${CUS}sambauser${CL}" \
|
||||||
|
"" \
|
||||||
|
""
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -331,47 +398,41 @@ EOF" \
|
|||||||
show_nfs_client_help() {
|
show_nfs_client_help() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "NFS Client Configuration")"
|
msg_title "$(translate "NFS Client in LXC (Privileged)")"
|
||||||
|
|
||||||
msg_info2 "$(translate "How to configure an NFS client in an LXC container.")"
|
msg_warn "$(translate "Current NFS client script supports privileged LXC only.")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Update and install packages:")" \
|
"$(translate "Install NFS client packages inside CT:")" \
|
||||||
"apt-get update && apt-get install -y nfs-common" \
|
"apt-get update && apt-get install -y nfs-common" \
|
||||||
"" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Create mount point:")" \
|
"$(translate "Create mount point:")" \
|
||||||
"mkdir -p ${CUS}/mnt/nfsmount${CL}" \
|
"mkdir -p ${CUS}/mnt/nfs_share${CL}" \
|
||||||
"$(translate "Replace with your preferred path.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "Mount NFS share:")" \
|
"$(translate "Mount NFS share:")" \
|
||||||
"mount -t nfs ${CUS}192.168.1.100${CL}:${CUS}/mnt/nfs_export${CL} ${CUS}/mnt/nfsmount${CL}" \
|
"mount -t nfs ${CUS}<server-ip>:/export/path${CL} ${CUS}/mnt/nfs_share${CL}" \
|
||||||
"$(translate "Replace server IP and paths.")" \
|
"$(translate "Adjust options if needed (vers=4,hard,timeo,...).")" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "4" \
|
show_command "4" \
|
||||||
"$(translate "Test access:")" \
|
"$(translate "Persist mount in CT /etc/fstab (optional):")" \
|
||||||
"ls -la ${CUS}/mnt/nfsmount${CL}" \
|
"echo '${CUS}<server-ip>:/export/path${CL} ${CUS}/mnt/nfs_share${CL} nfs defaults,_netdev,x-systemd.automount,noauto 0 0' >> /etc/fstab" \
|
||||||
"$(translate "Verify you can access the mounted share.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "5" \
|
show_command "5" \
|
||||||
"$(translate "Make permanent (optional):")" \
|
|
||||||
"echo '${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL} nfs4 rw,hard,intr,_netdev,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0' >> /etc/fstab" \
|
|
||||||
"$(translate "Replace with your server IP and paths.")" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "6" \
|
|
||||||
"$(translate "Verify mount:")" \
|
"$(translate "Verify mount:")" \
|
||||||
"df -h | grep nfs" \
|
"mount | grep nfs\ndf -h | grep nfs" \
|
||||||
"$(translate "Shows NFS mounts.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -380,63 +441,47 @@ show_nfs_client_help() {
|
|||||||
show_samba_client_help() {
|
show_samba_client_help() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "Samba Client Configuration")"
|
msg_title "$(translate "Samba Client in LXC (Privileged)")"
|
||||||
|
|
||||||
msg_info2 "$(translate "How to configure a Samba client in an LXC container.")"
|
msg_warn "$(translate "Current Samba client script supports privileged LXC only.")"
|
||||||
echo -e
|
echo -e
|
||||||
|
|
||||||
show_command "1" \
|
show_command "1" \
|
||||||
"$(translate "Update and install packages:")" \
|
"$(translate "Install CIFS client packages inside CT:")" \
|
||||||
"apt-get update && apt-get install -y cifs-utils" \
|
"apt-get update && apt-get install -y cifs-utils" \
|
||||||
"" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "2" \
|
show_command "2" \
|
||||||
"$(translate "Create mount point:")" \
|
"$(translate "Create mount point:")" \
|
||||||
"mkdir -p ${CUS}/mnt/sambamount${CL}" \
|
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
|
||||||
"$(translate "Replace with your preferred path.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "3" \
|
show_command "3" \
|
||||||
"$(translate "Mount Samba share:")" \
|
"$(translate "Create credentials file (recommended):")" \
|
||||||
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} -o username=${CUS}sambauser${CL}" \
|
"cat > /etc/samba/credentials/proxmenux.cred << 'EOF'\nusername=${CUS}<user>${CL}\npassword=${CUS}<pass>${CL}\nEOF\nchmod 600 /etc/samba/credentials/proxmenux.cred" \
|
||||||
"$(translate "Replace server IP, share name and username. You'll be prompted for password.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "4" \
|
show_command "4" \
|
||||||
"$(translate "Test access:")" \
|
"$(translate "Mount CIFS share:")" \
|
||||||
"ls -la ${CUS}/mnt/sambamount${CL}" \
|
"mount -t cifs //${CUS}<server-ip>/<share>${CL} ${CUS}/mnt/samba_share${CL} -o credentials=/etc/samba/credentials/proxmenux.cred,iocharset=utf8,file_mode=0664,dir_mode=0775" \
|
||||||
"$(translate "Verify you can access the mounted share.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "5" \
|
show_command "5" \
|
||||||
"$(translate "Create credentials file (optional):")" \
|
"$(translate "Persist mount in CT /etc/fstab (optional):")" \
|
||||||
"cat > /etc/samba/credentials << EOF
|
"echo '//${CUS}<server-ip>/<share>${CL} ${CUS}/mnt/samba_share${CL} cifs credentials=/etc/samba/credentials/proxmenux.cred,_netdev,x-systemd.automount,noauto 0 0' >> /etc/fstab" \
|
||||||
username=${CUS}sambauser${CL}
|
"" \
|
||||||
password=${CUS}your_password${CL}
|
|
||||||
EOF
|
|
||||||
chmod 600 /etc/samba/credentials" \
|
|
||||||
"$(translate "Secure way to store credentials.")" \
|
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "6" \
|
show_command "6" \
|
||||||
"$(translate "Mount with credentials file:")" \
|
"$(translate "Verify mount:")" \
|
||||||
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} -o credentials=/etc/samba/credentials" \
|
"mount -t cifs\ndf -h | grep cifs" \
|
||||||
"$(translate "No password prompt needed.")" \
|
"" \
|
||||||
""
|
""
|
||||||
|
|
||||||
show_command "7" \
|
|
||||||
"$(translate "Make permanent (optional):")" \
|
|
||||||
"echo '//${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} cifs credentials=/etc/samba/credentials,_netdev 0 0' >> /etc/fstab" \
|
|
||||||
"$(translate "Replace with your values.")" \
|
|
||||||
""
|
|
||||||
|
|
||||||
show_command "8" \
|
|
||||||
"$(translate "Verify mount:")" \
|
|
||||||
"df -h | grep cifs" \
|
|
||||||
"$(translate "Shows CIFS/Samba mounts.")" \
|
|
||||||
""
|
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
@@ -445,28 +490,35 @@ chmod 600 /etc/samba/credentials" \
|
|||||||
show_help_menu() {
|
show_help_menu() {
|
||||||
while true; do
|
while true; do
|
||||||
CHOICE=$(dialog --title "$(translate "Help & Information")" \
|
CHOICE=$(dialog --title "$(translate "Help & Information")" \
|
||||||
--menu "$(translate "Select help topic:")" 24 80 14 \
|
--menu "$(translate "Select help topic:")" 24 90 14 \
|
||||||
"0" "$(translate "How to Access an LXC Terminal")" \
|
"0" "$(translate "How to Access an LXC Terminal")" \
|
||||||
"1" "$(translate "Mount Remote Resources on Proxmox Host")" \
|
"1" "$(translate "Host NFS/Samba as Proxmox Storage (pvesm)")" \
|
||||||
"2" "$(translate "Mount Host Directory to LXC Container")" \
|
"2" "$(translate "Local Shared Directory on Host")" \
|
||||||
"3" "$(translate "NFS Server Installation")" \
|
"3" "$(translate "Add Local Disk as Proxmox Storage")" \
|
||||||
"4" "$(translate "Samba Server Installation")" \
|
"4" "$(translate "Add iSCSI Target as Proxmox Storage")" \
|
||||||
"5" "$(translate "NFS Client Configuration")" \
|
"5" "$(translate "Mount Host Directory to LXC Container")" \
|
||||||
"6" "$(translate "Samba Client Configuration")" \
|
"6" "$(translate "NFS Client in LXC (privileged)")" \
|
||||||
"7" "$(translate "Return to Main Menu")" \
|
"7" "$(translate "Samba Client in LXC (privileged)")" \
|
||||||
|
"8" "$(translate "NFS Server in LXC (privileged)")" \
|
||||||
|
"9" "$(translate "Samba Server in LXC (privileged)")" \
|
||||||
|
"10" "$(translate "Return to Share Menu")" \
|
||||||
3>&1 1>&2 2>&3)
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
case $CHOICE in
|
case "$CHOICE" in
|
||||||
0) show_how_to_enter_lxc ;;
|
0) show_how_to_enter_lxc ;;
|
||||||
1) show_host_mount_resources_help ;;
|
1) show_host_storage_help ;;
|
||||||
2) show_host_to_lxc_mount_help ;;
|
2) show_local_share_help ;;
|
||||||
3) show_nfs_server_help ;;
|
3) show_disk_host_help ;;
|
||||||
4) show_samba_server_help ;;
|
4) show_iscsi_host_help ;;
|
||||||
5) show_nfs_client_help ;;
|
5) show_host_to_lxc_mount_help ;;
|
||||||
6) show_samba_client_help ;;
|
6) show_nfs_client_help ;;
|
||||||
7) return ;;
|
7) show_samba_client_help ;;
|
||||||
|
8) show_nfs_server_help ;;
|
||||||
|
9) show_samba_server_help ;;
|
||||||
|
10) return ;;
|
||||||
*) return ;;
|
*) return ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
show_help_menu
|
show_help_menu
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,6 @@ ensure_iscsi_tools() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! systemctl is-active --quiet iscsid 2>/dev/null; then
|
if ! systemctl is-active --quiet iscsid 2>/dev/null; then
|
||||||
msg_info "$(translate "Starting iSCSI daemon...")"
|
|
||||||
systemctl start iscsid 2>/dev/null || true
|
systemctl start iscsid 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -65,10 +64,9 @@ ensure_iscsi_tools() {
|
|||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
select_iscsi_portal() {
|
select_iscsi_portal() {
|
||||||
ISCSI_PORTAL=$(whiptail --inputbox \
|
ISCSI_PORTAL=$(dialog --backtitle "ProxMenux" --title "$(translate "iSCSI Portal")" --inputbox \
|
||||||
"$(translate "Enter iSCSI target portal IP or hostname:")\n\n$(translate "Examples:")\n 192.168.1.100\n 192.168.1.100:3260\n nas.local" \
|
"$(translate "Enter iSCSI target portal IP or hostname:")\n\n$(translate "Examples:")\n 192.168.1.100\n 192.168.1.100:3260\n nas.local" \
|
||||||
14 65 \
|
14 65 3>&1 1>&2 2>&3)
|
||||||
--title "$(translate "iSCSI Portal")" 3>&1 1>&2 2>&3)
|
|
||||||
[[ $? -ne 0 || -z "$ISCSI_PORTAL" ]] && return 1
|
[[ $? -ne 0 || -z "$ISCSI_PORTAL" ]] && return 1
|
||||||
|
|
||||||
# Normalise: if no port specified, add default 3260
|
# Normalise: if no port specified, add default 3260
|
||||||
|
|||||||
@@ -6,82 +6,236 @@
|
|||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : MIT
|
# License : MIT
|
||||||
# Version : 1.0
|
# Version : 1.0
|
||||||
# Last Updated: $(date +%d/%m/%Y)
|
# Last Updated: 08/04/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
|
||||||
|
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||||
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||||
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -f "$UTILS_FILE" ]]; then
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
source "$UTILS_FILE"
|
source "$UTILS_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
SHARE_COMMON_FILE="$LOCAL_SCRIPTS/global/share-common.func"
|
SHARE_COMMON_FILE="$LOCAL_SCRIPTS/global/share-common.func"
|
||||||
if ! source "$SHARE_COMMON_FILE" 2>/dev/null; then
|
if ! source "$SHARE_COMMON_FILE" 2>/dev/null; then
|
||||||
SHARE_COMMON_LOADED=false
|
msg_error "$(translate "Could not load shared functions. Script cannot continue.")"
|
||||||
else
|
exit 1
|
||||||
SHARE_COMMON_LOADED=true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
|
|
||||||
|
if ! command -v pveversion >/dev/null 2>&1; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
create_shared_directory() {
|
lsm_apply_multi_unpriv_permissions() {
|
||||||
SHARED_DIR=$(pmx_select_host_mount_point "$(translate "Select Shared Directory Location")" "/mnt/shared")
|
local dir="$1"
|
||||||
[[ -z "$SHARED_DIR" ]] && return
|
|
||||||
|
|
||||||
|
[[ -z "$dir" || ! -d "$dir" ]] && return 1
|
||||||
|
|
||||||
if [[ -d "$SHARED_DIR" ]]; then
|
# root:root ownership — no new group needed.
|
||||||
if ! whiptail --yesno "$(translate "Directory already exists. Continue with permission setup?")" 10 70 --title "$(translate "Directory Exists")"; then
|
chown root:root "$dir" 2>/dev/null || true
|
||||||
return
|
|
||||||
|
# 1777 = sticky bit (prevents cross-container file deletion) + world-rwx.
|
||||||
|
# Unprivileged LXC UIDs (100000+) appear as 'others' on the host,
|
||||||
|
# so 'o+rwx' is what grants them read+write access.
|
||||||
|
chmod 1777 "$dir" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Ensure existing content is readable/writable regardless of UID mapping.
|
||||||
|
chmod -R a+rwX "$dir" 2>/dev/null || true
|
||||||
|
find "$dir" -type d -exec chmod 1777 {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
# Remove restrictive ACLs and enforce permissive inheritance for new files.
|
||||||
|
setfacl -b -R "$dir" 2>/dev/null || true
|
||||||
|
setfacl -R -m u::rwx,g::rwx,o::rwx,m::rwx "$dir" 2>/dev/null || true
|
||||||
|
setfacl -R -m d:u::rwx,d:g::rwx,d:o::rwx,d:m::rwx "$dir" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns a free name like /mnt/shared, /mnt/shared2, /mnt/shared3 …
|
||||||
|
lsm_next_free_name() {
|
||||||
|
local base="${1:-shared}"
|
||||||
|
local candidate="/mnt/$base"
|
||||||
|
[[ ! -d "$candidate" ]] && echo "$candidate" && return
|
||||||
|
local n=2
|
||||||
|
while [[ -d "/mnt/${base}${n}" ]]; do
|
||||||
|
((n++))
|
||||||
|
done
|
||||||
|
echo "/mnt/${base}${n}"
|
||||||
|
}
|
||||||
|
|
||||||
|
lsm_list_mnt_folders() {
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Folders in /mnt")"
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
if [[ ! -d /mnt ]] || [[ -z "$(ls -A /mnt 2>/dev/null)" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${TAB}$(translate "No folders found in /mnt.")"
|
||||||
|
else
|
||||||
|
local found=false
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
[[ ! -d "$dir" ]] && continue
|
||||||
|
found=true
|
||||||
|
local perms owner
|
||||||
|
perms=$(stat -c "%a" "$dir" 2>/dev/null)
|
||||||
|
owner=$(stat -c "%U:%G" "$dir" 2>/dev/null)
|
||||||
|
echo ""
|
||||||
|
echo -e "${TAB}${BGN}$(translate "Directory:")${CL} ${BL}$dir${CL}"
|
||||||
|
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}${perms} $(stat -c "(%A)" "$dir" 2>/dev/null)${CL}"
|
||||||
|
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}${owner}${CL}"
|
||||||
|
done < <(find /mnt -mindepth 1 -maxdepth 1 -type d | sort)
|
||||||
|
|
||||||
|
if [[ "$found" = false ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${TAB}$(translate "No folders found in /mnt.")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
SHARE_GROUP=$(pmx_choose_or_create_group "sharedfiles") || return 1
|
# Summary of /mnt available space
|
||||||
SHARE_GID=$(pmx_ensure_host_group "$SHARE_GROUP" 101000) || return 1
|
if mountpoint -q /mnt 2>/dev/null || [[ -d /mnt ]]; then
|
||||||
|
local mnt_avail mnt_total
|
||||||
|
mnt_avail=$(df -h /mnt 2>/dev/null | awk 'NR==2{print $4}')
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
mnt_total=$(df -h /mnt 2>/dev/null | awk 'NR==2{print $2}')
|
||||||
setfacl -k /mnt 2>/dev/null || true
|
if [[ -n "$mnt_avail" ]]; then
|
||||||
setfacl -b /mnt 2>/dev/null || true
|
echo -e "${TAB}${BGN}$(translate "Available space in /mnt:")${CL} ${BL}${mnt_avail} $(translate "of") ${mnt_total}${CL}"
|
||||||
fi
|
echo ""
|
||||||
chmod 755 /mnt 2>/dev/null || true
|
fi
|
||||||
|
|
||||||
|
|
||||||
pmx_prepare_host_shared_dir "$SHARED_DIR" "$SHARE_GROUP" || return 1
|
|
||||||
|
|
||||||
|
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
|
||||||
setfacl -b -R "$SHARED_DIR" 2>/dev/null || true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
|
read -r
|
||||||
|
}
|
||||||
|
|
||||||
chown root:"$SHARE_GROUP" "$SHARED_DIR"
|
# Result is stored in LSM_SELECTED_MOUNT_POINT (not echoed) to avoid subshell issues
|
||||||
chmod 2775 "$SHARED_DIR"
|
LSM_SELECTED_MOUNT_POINT=""
|
||||||
|
|
||||||
pmx_share_map_set "$SHARED_DIR" "$SHARE_GROUP"
|
lsm_select_host_mount_point_dialog() {
|
||||||
|
local title="${1:-$(translate "Select Shared Directory Location")}"
|
||||||
|
local base_name="${2:-shared}"
|
||||||
|
local choice folder_name result mount_point
|
||||||
|
LSM_SELECTED_MOUNT_POINT=""
|
||||||
|
|
||||||
|
# Auto-suggest a free name in /mnt
|
||||||
|
local suggested
|
||||||
|
suggested=$(lsm_next_free_name "$base_name")
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
choice=$(dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$title" \
|
||||||
|
--menu "\n$(translate "Where do you want the host folder?")" 16 72 4 \
|
||||||
|
"1" "$(translate "Create new folder in /mnt")" \
|
||||||
|
"2" "$(translate "Enter custom path")" \
|
||||||
|
"3" "$(translate "View existing folders in /mnt")" \
|
||||||
|
"4" "$(translate "Cancel")" \
|
||||||
|
3>&1 1>&2 2>&3) || return 1
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
folder_name=$(dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Folder Name")" \
|
||||||
|
--inputbox "\n$(translate "Enter folder name for /mnt:")" 10 70 "$(basename "$suggested")" \
|
||||||
|
3>&1 1>&2 2>&3) || continue
|
||||||
|
[[ -z "$folder_name" ]] && continue
|
||||||
|
mount_point="/mnt/$folder_name"
|
||||||
|
# Only warn if the user manually typed an existing name
|
||||||
|
if [[ -d "$mount_point" ]]; then
|
||||||
|
if ! dialog --backtitle "ProxMenux" --title "$(translate "Directory Exists")" \
|
||||||
|
--yesno "\n$(translate "Directory already exists. Continue with permission setup?")" 8 70; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
result=$(dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Custom Path")" \
|
||||||
|
--inputbox "\n$(translate "Enter full path:")" 10 80 "" \
|
||||||
|
3>&1 1>&2 2>&3) || continue
|
||||||
|
[[ -z "$result" ]] && continue
|
||||||
|
mount_point="$result"
|
||||||
|
if [[ -d "$mount_point" ]]; then
|
||||||
|
if ! dialog --backtitle "ProxMenux" --title "$(translate "Directory Exists")" \
|
||||||
|
--yesno "\n$(translate "Directory already exists. Continue with permission setup?")" 8 70; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
lsm_list_mnt_folders
|
||||||
|
# Refresh suggestion after viewing
|
||||||
|
suggested=$(lsm_next_free_name "$base_name")
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
4) return 1 ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ ! "$mount_point" =~ ^/ ]]; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Invalid Path")" \
|
||||||
|
--msgbox "\n$(translate "Path must be absolute (start with /).")" 8 60
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
LSM_SELECTED_MOUNT_POINT="$mount_point"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
create_shared_directory() {
|
||||||
|
lsm_select_host_mount_point_dialog "$(translate "Select Shared Directory Location")" "shared"
|
||||||
|
[[ -z "$LSM_SELECTED_MOUNT_POINT" ]] && return
|
||||||
|
SHARED_DIR="$LSM_SELECTED_MOUNT_POINT"
|
||||||
|
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "Create Shared Directory")"
|
msg_title "$(translate "Create Shared Directory")"
|
||||||
|
|
||||||
|
if ! mkdir -p "$SHARED_DIR" 2>/dev/null; then
|
||||||
|
msg_error "$(translate "Failed to create directory:") $SHARED_DIR"
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate "Press Enter to continue...")"
|
||||||
|
read -r
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
msg_ok "$(translate "Directory created:") $SHARED_DIR"
|
||||||
|
|
||||||
|
lsm_apply_multi_unpriv_permissions "$SHARED_DIR"
|
||||||
|
|
||||||
|
pmx_share_map_set "$SHARED_DIR" "open"
|
||||||
|
|
||||||
echo -e ""
|
echo -e ""
|
||||||
echo -e "${TAB}${BOLD}$(translate "Shared Directory Created:")${CL}"
|
echo -e "${TAB}${BOLD}$(translate "Shared Directory Ready:")${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Directory:")${CL} ${BL}$SHARED_DIR${CL}"
|
echo -e "${TAB}${BGN}$(translate "Directory:")${CL} ${BL}$SHARED_DIR${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Group:")${CL} ${BL}$SHARE_GROUP (GID: $SHARE_GID)${CL}"
|
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}1777 (rwxrwxrwt)${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}2775 (rwxrwsr-x)${CL}"
|
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}root:root${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}root:$SHARE_GROUP${CL}"
|
echo -e "${TAB}${BGN}$(translate "Access profile:")${CL} ${BL}$(translate "Compatible with privileged and unprivileged LXC containers")${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "ACL Status:")${CL} ${BL}$(translate "Cleaned and set for POSIX inheritance")${CL}"
|
echo -e "${TAB}${BGN}$(translate "ACL Status:")${CL} ${BL}$(translate "Open rwx + default inheritance for new files")${CL}"
|
||||||
echo -e ""
|
echo -e ""
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
read -r
|
read -r
|
||||||
|
|||||||
@@ -229,15 +229,13 @@ select_host_directory_unified() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Warn about CIFS Proxmox-GUI storage (read-only limitation)
|
# Store the storage type as a global so the main flow can act on it later.
|
||||||
|
# We don't block the user here — the active fix happens after we know the container type.
|
||||||
|
LMM_HOST_DIR_TYPE="local"
|
||||||
if detect_problematic_storage "$result" "Proxmox-Storage" "CIFS/SMB"; then
|
if detect_problematic_storage "$result" "Proxmox-Storage" "CIFS/SMB"; then
|
||||||
dialog --clear --title "$(translate "CIFS Storage Notice")" --yesno "\
|
LMM_HOST_DIR_TYPE="cifs"
|
||||||
$(translate "This directory is a CIFS storage managed by Proxmox.")\n\n\
|
elif detect_problematic_storage "$result" "Proxmox-Storage" "NFS"; then
|
||||||
$(translate "CIFS storage configured through Proxmox GUI applies restrictive permissions.")\n\
|
LMM_HOST_DIR_TYPE="nfs"
|
||||||
$(translate "LXC containers can usually READ but may NOT be able to WRITE.")\n\n\
|
|
||||||
$(translate "For write access, use 'Add Samba Share as Proxmox Storage' option instead.")\n\n\
|
|
||||||
$(translate "Do you want to continue anyway?")" 14 80 3>&1 1>&2 2>&3
|
|
||||||
[[ $? -ne 0 ]] && return 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$result"
|
echo "$result"
|
||||||
@@ -314,7 +312,7 @@ select_container_mount_point() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if path is already used as a mount point in this CT
|
# Check if path is already used as a mount point in this CT
|
||||||
if pct config "$ctid" 2>/dev/null | grep -q "mp=.*$mount_point"; then
|
if pct config "$ctid" 2>/dev/null | grep -qE "mp=${mount_point}(,|$)"; then
|
||||||
whiptail --msgbox "$(translate "This path is already used as a mount point in this container.")" 8 70
|
whiptail --msgbox "$(translate "This path is already used as a mount point in this container.")" 8 70
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -364,7 +362,7 @@ add_bind_mount() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if this host path is already mounted in this CT
|
# Check if this host path is already mounted in this CT
|
||||||
if pct config "$ctid" 2>/dev/null | grep -q "^mp[0-9]*:.*${host_path},"; then
|
if pct config "$ctid" 2>/dev/null | grep -qF " ${host_path},"; then
|
||||||
msg_warn "$(translate "Mount already exists for this path in container") $ctid"
|
msg_warn "$(translate "Mount already exists for this path in container") $ctid"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -555,6 +553,199 @@ $(translate "Proceed with removal")?"
|
|||||||
read -r
|
read -r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ACTIVE FIXES FOR NETWORK STORAGE (CIFS / NFS)
|
||||||
|
# These functions act on problems instead of just warning about them.
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
lmm_fix_cifs_access() {
|
||||||
|
local host_dir="$1"
|
||||||
|
local is_unprivileged="$2"
|
||||||
|
|
||||||
|
# CIFS mounted by Proxmox GUI uses uid=0/gid=0 by default (root only).
|
||||||
|
# The fix: remount with uid/gid that the LXC can access.
|
||||||
|
# We detect the current mount options and propose a corrected remount.
|
||||||
|
|
||||||
|
local mount_src mount_opts
|
||||||
|
mount_src=$(findmnt -n -o SOURCE --target "$host_dir" 2>/dev/null)
|
||||||
|
mount_opts=$(findmnt -n -o OPTIONS --target "$host_dir" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$mount_src" ]]; then
|
||||||
|
dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "CIFS Mount Not Found")" \
|
||||||
|
--msgbox "$(translate "Could not detect the CIFS mount for this directory. Try accessing it manually.")" 8 70
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine which uid/gid to use
|
||||||
|
local target_uid target_gid
|
||||||
|
if [[ "$is_unprivileged" == "1" ]]; then
|
||||||
|
# Unprivileged LXC: container root (UID 0) maps to host UID 100000.
|
||||||
|
# Use file_mode/dir_mode 0777 + uid=0/gid=0 — CIFS maps them to everyone.
|
||||||
|
target_uid=0
|
||||||
|
target_gid=0
|
||||||
|
else
|
||||||
|
target_uid=0
|
||||||
|
target_gid=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build new options: strip existing uid/gid/file_mode/dir_mode, add ours
|
||||||
|
local new_opts
|
||||||
|
new_opts=$(echo "$mount_opts" | sed -E \
|
||||||
|
's/(^|,)(uid|gid|file_mode|dir_mode)=[^,]*//g' | \
|
||||||
|
sed 's/^,//')
|
||||||
|
new_opts="${new_opts},uid=${target_uid},gid=${target_gid},file_mode=0777,dir_mode=0777"
|
||||||
|
new_opts="${new_opts/#,/}"
|
||||||
|
|
||||||
|
if dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Fix CIFS Permissions")" \
|
||||||
|
--yesno \
|
||||||
|
"$(translate "This CIFS share is mounted with restrictive permissions.")\n\n\
|
||||||
|
$(translate "ProxMenux can remount it with open permissions so any LXC can read and write.")\n\n\
|
||||||
|
$(translate "Current mount options:")\n${mount_opts}\n\n\
|
||||||
|
$(translate "New mount options to apply:")\n${new_opts}\n\n\
|
||||||
|
$(translate "Apply fix now? (The share will be briefly remounted)")" \
|
||||||
|
18 84 3>&1 1>&2 2>&3; then
|
||||||
|
|
||||||
|
msg_info "$(translate "Remounting CIFS share with open permissions...")"
|
||||||
|
if umount "$host_dir" 2>/dev/null && \
|
||||||
|
mount -t cifs "$mount_src" "$host_dir" -o "$new_opts" 2>/dev/null; then
|
||||||
|
msg_ok "$(translate "CIFS share remounted — LXC containers can now read and write")"
|
||||||
|
|
||||||
|
# Update fstab if the mount is there
|
||||||
|
if grep -qF "$host_dir" /etc/fstab 2>/dev/null; then
|
||||||
|
sed -i "s|^\(${mount_src}[[:space:]].*${host_dir}.*cifs[[:space:]]\).*|\1${new_opts} 0 0|" /etc/fstab 2>/dev/null || true
|
||||||
|
msg_ok "$(translate "/etc/fstab updated — permissions will persist after reboot")"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_warn "$(translate "Could not remount automatically. Try manually or check credentials.")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
lmm_fix_nfs_access() {
|
||||||
|
local host_dir="$1"
|
||||||
|
local is_unprivileged="$2"
|
||||||
|
local uid_shift="${3:-100000}"
|
||||||
|
|
||||||
|
# NFS: the host cannot override server-side permissions.
|
||||||
|
# BUT: if the server exports with root_squash (default), we can check
|
||||||
|
# if no_root_squash or all_squash is possible, and guide the user.
|
||||||
|
# What we CAN do on the host: apply a sticky+open directory as a cache layer
|
||||||
|
# if the NFS mount allows it.
|
||||||
|
|
||||||
|
local mount_src mount_opts
|
||||||
|
mount_src=$(findmnt -n -o SOURCE --target "$host_dir" 2>/dev/null)
|
||||||
|
mount_opts=$(findmnt -n -o OPTIONS --target "$host_dir" 2>/dev/null)
|
||||||
|
|
||||||
|
# Try to detect if we can write to the NFS share as root
|
||||||
|
local can_write=false
|
||||||
|
local testfile="${host_dir}/.proxmenux_write_test_$$"
|
||||||
|
if touch "$testfile" 2>/dev/null; then
|
||||||
|
rm -f "$testfile" 2>/dev/null
|
||||||
|
can_write=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
local server_hint=""
|
||||||
|
if [[ -n "$mount_src" ]]; then
|
||||||
|
server_hint="${mount_src%%:*}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$can_write" == "true" && "$is_unprivileged" == "1" ]]; then
|
||||||
|
# Root on host CAN write to NFS, but unprivileged LXC UIDs (100000+)
|
||||||
|
# will be squashed by the NFS server. We can set a world-writable sticky
|
||||||
|
# dir on the share itself so the container can write to it.
|
||||||
|
if dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Fix NFS Access for Unprivileged LXC")" \
|
||||||
|
--yesno \
|
||||||
|
"$(translate "NFS server export is writable from the host, but unprivileged LXC containers use mapped UIDs (${uid_shift}+) which the NFS server will squash.")\n\n\
|
||||||
|
$(translate "ProxMenux can apply open permissions on this NFS directory from the host so the container can read and write:")\n\n\
|
||||||
|
$(translate " chmod 1777 + setfacl o::rwx (applied on the NFS share from this host)")\n\n\
|
||||||
|
$(translate "Note: this only works if the NFS server does NOT use 'all_squash' for root.")\n\
|
||||||
|
$(translate "If it still fails, the NFS server export options must be changed on the server.")\n\n\
|
||||||
|
$(translate "Apply fix now?")" \
|
||||||
|
18 84 3>&1 1>&2 2>&3; then
|
||||||
|
|
||||||
|
if chmod 1777 "$host_dir" 2>/dev/null; then
|
||||||
|
msg_ok "$(translate "NFS directory permissions set — containers should now be able to write")"
|
||||||
|
else
|
||||||
|
msg_warn "$(translate "chmod failed — NFS server may be restricting changes from root")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
setfacl -m o::rwx "$host_dir" 2>/dev/null || true
|
||||||
|
setfacl -m d:o::rwx "$host_dir" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$can_write" == "false" ]]; then
|
||||||
|
# Even root cannot write — NFS server is fully restrictive
|
||||||
|
local server_msg=""
|
||||||
|
[[ -n "$server_hint" ]] && server_msg="\n$(translate "NFS server:"): ${server_hint}"
|
||||||
|
|
||||||
|
dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "NFS Access Restricted")" \
|
||||||
|
--msgbox \
|
||||||
|
"$(translate "This NFS share is fully restricted — even the host root cannot write to it.")\n\
|
||||||
|
${server_msg}\n\n\
|
||||||
|
$(translate "ProxMenux cannot override NFS server-side permissions from the host.")\n\n\
|
||||||
|
$(translate "To allow LXC write access, change the NFS export on the server to include:")\n\n\
|
||||||
|
$(translate " no_root_squash") $(translate "(if only privileged LXCs need write access)")\n\
|
||||||
|
$(translate " all_squash,anonuid=65534,anongid=65534") $(translate "(for unprivileged LXCs)")\n\n\
|
||||||
|
$(translate "You can still mount this share for READ-ONLY access.")" \
|
||||||
|
20 84 3>&1 1>&2 2>&3
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# HOST PERMISSION CHECK (host-side only, never touches the container)
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
lmm_offer_host_permissions() {
|
||||||
|
local host_dir="$1"
|
||||||
|
local is_unprivileged="$2"
|
||||||
|
|
||||||
|
# Privileged containers: UID 0 inside = UID 0 on host — always accessible
|
||||||
|
[[ "$is_unprivileged" != "1" ]] && return 0
|
||||||
|
|
||||||
|
# Check if 'others' already have r+x (minimum to traverse and read)
|
||||||
|
local stat_perms others_bits
|
||||||
|
stat_perms=$(stat -c "%a" "$host_dir" 2>/dev/null) || return 0
|
||||||
|
others_bits=$(( 8#${stat_perms} & 7 ))
|
||||||
|
|
||||||
|
# Check ACLs first if available (takes precedence over mode bits)
|
||||||
|
if command -v getfacl >/dev/null 2>&1; then
|
||||||
|
if getfacl -p "$host_dir" 2>/dev/null | grep -q "^other::.*r.*x"; then
|
||||||
|
return 0 # ACL already grants others r+x or better
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5 = r-x (bits: r=4, x=1). If already r+x or rwx we're fine.
|
||||||
|
(( (others_bits & 5) == 5 )) && return 0
|
||||||
|
|
||||||
|
# Permissions are insufficient — offer to fix HOST directory only
|
||||||
|
local current_perms
|
||||||
|
current_perms=$(stat -c "%A" "$host_dir" 2>/dev/null)
|
||||||
|
|
||||||
|
if dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Unprivileged Container Access")" \
|
||||||
|
--yesno \
|
||||||
|
"$(translate "The host directory may not be accessible from an unprivileged container.")\n\n\
|
||||||
|
$(translate "Unprivileged containers map their UIDs to high host UIDs (e.g. 100000+), which appear as 'others' on the host filesystem.")\n\n\
|
||||||
|
$(translate "Current permissions:"): ${current_perms}\n\n\
|
||||||
|
$(translate "Apply read+write access for 'others' on the host directory?")\n\n\
|
||||||
|
$(translate "(Only the host directory is modified. Nothing inside the container is changed.")" \
|
||||||
|
16 80 3>&1 1>&2 2>&3; then
|
||||||
|
|
||||||
|
chmod o+rwx "$host_dir" 2>/dev/null || true
|
||||||
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
|
setfacl -m o::rwx "$host_dir" 2>/dev/null || true
|
||||||
|
setfacl -m d:o::rwx "$host_dir" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
msg_ok "$(translate "Host directory permissions updated — unprivileged containers can now access it")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# MAIN FUNCTION — ADD MOUNT
|
# MAIN FUNCTION — ADD MOUNT
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -577,7 +768,7 @@ mount_host_directory_minimal() {
|
|||||||
|
|
||||||
# Step 4: Get container type info (for display only)
|
# Step 4: Get container type info (for display only)
|
||||||
local uid_shift container_type_display
|
local uid_shift container_type_display
|
||||||
uid_shift=$(awk -F: '/^lxc.idmap.*u 0/ {print $5}' "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | head -1)
|
uid_shift=$(awk '/^lxc.idmap.*u 0/ {print $5}' "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | head -1)
|
||||||
local is_unprivileged
|
local is_unprivileged
|
||||||
is_unprivileged=$(grep "^unprivileged:" "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | awk '{print $2}')
|
is_unprivileged=$(grep "^unprivileged:" "/etc/pve/lxc/${container_id}.conf" 2>/dev/null | awk '{print $2}')
|
||||||
if [[ "$is_unprivileged" == "1" ]]; then
|
if [[ "$is_unprivileged" == "1" ]]; then
|
||||||
@@ -588,7 +779,13 @@ mount_host_directory_minimal() {
|
|||||||
uid_shift="0"
|
uid_shift="0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 5: Confirmation
|
# Step 5: Active fix for network storage (before confirmation, while we know container type)
|
||||||
|
case "${LMM_HOST_DIR_TYPE:-local}" in
|
||||||
|
cifs) lmm_fix_cifs_access "$host_dir" "$is_unprivileged" ;;
|
||||||
|
nfs) lmm_fix_nfs_access "$host_dir" "$is_unprivileged" "$uid_shift" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Step 6: Confirmation
|
||||||
local confirm_msg
|
local confirm_msg
|
||||||
confirm_msg="$(translate "Mount Configuration Summary:")
|
confirm_msg="$(translate "Mount Configuration Summary:")
|
||||||
|
|
||||||
@@ -597,17 +794,12 @@ $(translate "Host Directory"): $host_dir
|
|||||||
$(translate "Container Mount Point"): $ct_mount_point
|
$(translate "Container Mount Point"): $ct_mount_point
|
||||||
|
|
||||||
$(translate "IMPORTANT NOTES:")
|
$(translate "IMPORTANT NOTES:")
|
||||||
- $(translate "Host directory permissions and ownership are NOT modified")
|
- $(translate "Nothing inside the container is modified")
|
||||||
- $(translate "Container filesystem is NOT modified")
|
- $(if [[ "$is_unprivileged" == "1" ]]; then
|
||||||
- $(translate "If access fails after mounting, adjust permissions manually:")
|
translate "Host directory access for unprivileged containers has been prepared above"
|
||||||
|
else
|
||||||
$(if [[ "$is_unprivileged" == "1" ]]; then
|
translate "Privileged container — host root maps directly, no permission changes needed"
|
||||||
echo " # Allow container UID ${uid_shift}+ to access host dir:"
|
fi)
|
||||||
echo " setfacl -m u:${uid_shift}:rwx \"$host_dir\""
|
|
||||||
echo " setfacl -d:m u:${uid_shift}:rwx \"$host_dir\""
|
|
||||||
else
|
|
||||||
echo " chmod 755 \"$host_dir\""
|
|
||||||
fi)
|
|
||||||
|
|
||||||
$(translate "Proceed")?"
|
$(translate "Proceed")?"
|
||||||
|
|
||||||
@@ -621,7 +813,7 @@ $(translate "Proceed")?"
|
|||||||
msg_ok "$(translate "Host directory:") $host_dir"
|
msg_ok "$(translate "Host directory:") $host_dir"
|
||||||
msg_ok "$(translate "Container mount point:") $ct_mount_point"
|
msg_ok "$(translate "Container mount point:") $ct_mount_point"
|
||||||
|
|
||||||
# Step 6: Add bind mount (the ONLY operation that changes anything)
|
# Step 7: Add bind mount
|
||||||
if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then
|
if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then
|
||||||
echo ""
|
echo ""
|
||||||
msg_success "$(translate "Press Enter to continue...")"
|
msg_success "$(translate "Press Enter to continue...")"
|
||||||
@@ -629,27 +821,25 @@ $(translate "Proceed")?"
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 7: Summary with permission hints
|
# Step 8: Host permission check for local dirs (only if not already handled above for CIFS/NFS)
|
||||||
|
if [[ "${LMM_HOST_DIR_TYPE:-local}" == "local" ]]; then
|
||||||
|
lmm_offer_host_permissions "$host_dir" "$is_unprivileged"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 9: Summary
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${TAB}${BOLD}$(translate "Mount Added Successfully:")${CL}"
|
echo -e "${TAB}${BOLD}$(translate "Mount Added Successfully:")${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}"
|
echo -e "${TAB}${BGN}$(translate "Container:")${CL} ${BL}$container_id${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Host Directory:")${CL} ${BL}$host_dir${CL}"
|
echo -e "${TAB}${BGN}$(translate "Host Directory:")${CL} ${BL}$host_dir${CL}"
|
||||||
echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$ct_mount_point${CL}"
|
echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$ct_mount_point${CL}"
|
||||||
|
if [[ "$is_unprivileged" == "1" ]]; then
|
||||||
|
echo -e "${TAB}${YW}$(translate "Unprivileged container — UID offset:") ${uid_shift}${CL}"
|
||||||
|
else
|
||||||
|
echo -e "${TAB}${DGN}$(translate "Privileged container — direct root access")${CL}"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ "$is_unprivileged" == "1" ]]; then
|
# Step 10: Offer restart
|
||||||
local mapped_uid="$uid_shift"
|
|
||||||
echo -e "${TAB}${YW}$(translate "UNPRIVILEGED container — UID mapping active:")${CL}"
|
|
||||||
echo -e "${TAB} $(translate "Container UID 0") → $(translate "Host UID") $mapped_uid"
|
|
||||||
echo -e "${TAB} $(translate "If access fails, run on the host:")"
|
|
||||||
echo -e "${TAB} ${DGN}setfacl -m u:${mapped_uid}:rwx \"$host_dir\"${CL}"
|
|
||||||
echo -e "${TAB} ${DGN}setfacl -d:m u:${mapped_uid}:rwx \"$host_dir\"${CL}"
|
|
||||||
else
|
|
||||||
echo -e "${TAB}${DGN}$(translate "PRIVILEGED container — direct UID mapping")${CL}"
|
|
||||||
echo -e "${TAB} $(translate "Ensure") $host_dir $(translate "is accessible by root (chmod 755 or wider)")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 8: Offer restart
|
|
||||||
echo ""
|
echo ""
|
||||||
if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then
|
if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then
|
||||||
msg_info "$(translate "Restarting container...")"
|
msg_info "$(translate "Restarting container...")"
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ add_proxmox_nfs_storage() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
msg_ok "$(translate "Storage ID is available")"
|
msg_ok "$(translate "Storage ID is available")"
|
||||||
|
msg_info "$(translate "NFS storage adding in progress...")"
|
||||||
if pvesm_output=$(pvesm add nfs "$storage_id" \
|
if pvesm_output=$(pvesm add nfs "$storage_id" \
|
||||||
--server "$server" \
|
--server "$server" \
|
||||||
--export "$export" \
|
--export "$export" \
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||||||
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
TOOLS_JSON="$BASE_DIR/installed_tools.json"
|
||||||
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||||
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||||
@@ -51,22 +52,45 @@ SELECTED_VMID=""
|
|||||||
SELECTED_VM_NAME=""
|
SELECTED_VM_NAME=""
|
||||||
declare -a SELECTED_CONTROLLER_PCIS=()
|
declare -a SELECTED_CONTROLLER_PCIS=()
|
||||||
IOMMU_PENDING_REBOOT=0
|
IOMMU_PENDING_REBOOT=0
|
||||||
|
IOMMU_ALREADY_ACTIVE=0
|
||||||
|
NEED_HOOK_SYNC=false
|
||||||
|
WIZARD_CONFLICT_POLICY=""
|
||||||
|
WIZARD_CONFLICT_SCOPE=""
|
||||||
|
|
||||||
set_title() {
|
set_title() {
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate "Add Controller or NVMe PCIe to VM")"
|
msg_title "$(translate "Add Controller or NVMe PCIe to VM")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_tools_json() {
|
||||||
|
[[ -f "$TOOLS_JSON" ]] || echo "{}" > "$TOOLS_JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
register_tool() {
|
||||||
|
local tool="$1"
|
||||||
|
local state="$2"
|
||||||
|
command -v jq >/dev/null 2>&1 || return 0
|
||||||
|
ensure_tools_json
|
||||||
|
jq --arg t "$tool" --argjson v "$state" \
|
||||||
|
'.[$t]=$v' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" \
|
||||||
|
&& mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
register_vfio_iommu_tool() {
|
||||||
|
register_tool "vfio_iommu" true || true
|
||||||
|
}
|
||||||
|
|
||||||
enable_iommu_cmdline() {
|
enable_iommu_cmdline() {
|
||||||
|
local silent="${1:-}"
|
||||||
local cpu_vendor iommu_param
|
local cpu_vendor iommu_param
|
||||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||||
|
|
||||||
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
||||||
iommu_param="intel_iommu=on"
|
iommu_param="intel_iommu=on"
|
||||||
msg_info "$(translate "Intel CPU detected")"
|
[[ "$silent" != "silent" ]] && msg_info "$(translate "Intel CPU detected")"
|
||||||
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
||||||
iommu_param="amd_iommu=on"
|
iommu_param="amd_iommu=on"
|
||||||
msg_info "$(translate "AMD CPU detected")"
|
[[ "$silent" != "silent" ]] && msg_info "$(translate "AMD CPU detected")"
|
||||||
else
|
else
|
||||||
msg_error "$(translate "Unknown CPU vendor. Cannot determine IOMMU parameter.")"
|
msg_error "$(translate "Unknown CPU vendor. Cannot determine IOMMU parameter.")"
|
||||||
return 1
|
return 1
|
||||||
@@ -76,22 +100,22 @@ enable_iommu_cmdline() {
|
|||||||
local grub_file="/etc/default/grub"
|
local grub_file="/etc/default/grub"
|
||||||
|
|
||||||
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||||
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
if ! grep -q "$iommu_param" "$cmdline_file" || ! grep -q "iommu=pt" "$cmdline_file"; then
|
||||||
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||||
proxmox-boot-tool refresh >/dev/null 2>&1 || true
|
proxmox-boot-tool refresh >/dev/null 2>&1 || true
|
||||||
msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
|
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
|
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
|
||||||
fi
|
fi
|
||||||
elif [[ -f "$grub_file" ]]; then
|
elif [[ -f "$grub_file" ]]; then
|
||||||
if ! grep -q "$iommu_param" "$grub_file"; then
|
if ! grep -q "$iommu_param" "$grub_file" || ! grep -q "iommu=pt" "$grub_file"; then
|
||||||
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||||
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||||
update-grub >/dev/null 2>&1 || true
|
update-grub >/dev/null 2>&1 || true
|
||||||
msg_ok "$(translate "IOMMU parameters added to GRUB")"
|
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to GRUB")"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "IOMMU already configured in GRUB")"
|
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in GRUB")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_error "$(translate "Neither /etc/kernel/cmdline nor /etc/default/grub found.")"
|
msg_error "$(translate "Neither /etc/kernel/cmdline nor /etc/default/grub found.")"
|
||||||
@@ -101,24 +125,29 @@ enable_iommu_cmdline() {
|
|||||||
|
|
||||||
check_iommu_or_offer_enable() {
|
check_iommu_or_offer_enable() {
|
||||||
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
|
register_vfio_iommu_tool
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
|
||||||
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
|
||||||
IOMMU_PENDING_REBOOT=1
|
IOMMU_PENDING_REBOOT=1
|
||||||
msg_warn "$(translate "IOMMU is configured for next boot, but not active yet.")"
|
register_vfio_iommu_tool
|
||||||
msg_info2 "$(translate "Controller/NVMe assignment can continue now and will be effective after reboot.")"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
|
IOMMU_ALREADY_ACTIVE=1
|
||||||
|
register_vfio_iommu_tool
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
|
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
|
||||||
[[ -d /sys/kernel/iommu_groups ]] && \
|
[[ -d /sys/kernel/iommu_groups ]] && \
|
||||||
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
|
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
|
||||||
|
IOMMU_ALREADY_ACTIVE=1
|
||||||
|
register_vfio_iommu_tool
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -133,13 +162,11 @@ check_iommu_or_offer_enable() {
|
|||||||
--title "$(translate "IOMMU Required")" \
|
--title "$(translate "IOMMU Required")" \
|
||||||
--yesno "$msg" 15 74
|
--yesno "$msg" 15 74
|
||||||
local response=$?
|
local response=$?
|
||||||
clear
|
|
||||||
|
|
||||||
[[ $response -ne 0 ]] && return 1
|
[[ $response -ne 0 ]] && return 1
|
||||||
|
|
||||||
set_title
|
set_title
|
||||||
msg_title "$(translate "Enabling IOMMU")"
|
msg_title "$(translate "Enabling IOMMU")"
|
||||||
echo
|
|
||||||
if ! enable_iommu_cmdline; then
|
if ! enable_iommu_cmdline; then
|
||||||
echo
|
echo
|
||||||
msg_error "$(translate "Failed to configure IOMMU automatically.")"
|
msg_error "$(translate "Failed to configure IOMMU automatically.")"
|
||||||
@@ -148,37 +175,36 @@ check_iommu_or_offer_enable() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
register_vfio_iommu_tool
|
||||||
msg_success "$(translate "IOMMU configured. Reboot required before using Controller/NVMe passthrough.")"
|
IOMMU_PENDING_REBOOT=1
|
||||||
echo
|
|
||||||
if whiptail --title "$(translate "Reboot Required")" \
|
|
||||||
--yesno "$(translate "Do you want to reboot now?")" 10 64; then
|
|
||||||
msg_warn "$(translate "Rebooting the system...")"
|
|
||||||
reboot
|
|
||||||
else
|
|
||||||
IOMMU_PENDING_REBOOT=1
|
|
||||||
msg_warn "$(translate "Reboot postponed by user.")"
|
|
||||||
msg_info2 "$(translate "You can continue assigning Controller/NVMe now, but reboot the host before starting the VM.")"
|
|
||||||
msg_success "$(translate "Press Enter to continue...")"
|
|
||||||
read -r
|
|
||||||
fi
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
select_target_vm() {
|
select_target_vm() {
|
||||||
local -a vm_menu=()
|
local -a vm_menu=()
|
||||||
local line vmid vmname vmstatus vm_machine status_label
|
local line vmid vmname vmstatus vm_machine status_label
|
||||||
|
local max_name_len=0 padded_name
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
vmid=$(awk '{print $1}' <<< "$line")
|
||||||
|
vmname=$(awk '{print $2}' <<< "$line")
|
||||||
|
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||||
|
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue
|
||||||
|
[[ ${#vmname} -gt $max_name_len ]] && max_name_len=${#vmname}
|
||||||
|
done < <(qm list 2>/dev/null)
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
vmid=$(awk '{print $1}' <<< "$line")
|
vmid=$(awk '{print $1}' <<< "$line")
|
||||||
vmname=$(awk '{print $2}' <<< "$line")
|
vmname=$(awk '{print $2}' <<< "$line")
|
||||||
vmstatus=$(awk '{print $3}' <<< "$line")
|
vmstatus=$(awk '{print $3}' <<< "$line")
|
||||||
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||||
|
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue
|
||||||
|
|
||||||
vm_machine=$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/ {print $2}')
|
vm_machine=$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/ {print $2}')
|
||||||
[[ -z "$vm_machine" ]] && vm_machine="unknown"
|
[[ -z "$vm_machine" ]] && vm_machine="unknown"
|
||||||
status_label="${vmstatus}, ${vm_machine}"
|
status_label="${vmstatus}, ${vm_machine}"
|
||||||
vm_menu+=("$vmid" "${vmname} [${status_label}]")
|
printf -v padded_name "%-${max_name_len}s" "$vmname"
|
||||||
|
vm_menu+=("$vmid" "${padded_name} [${status_label}]")
|
||||||
done < <(qm list 2>/dev/null)
|
done < <(qm list 2>/dev/null)
|
||||||
if [[ ${#vm_menu[@]} -eq 0 ]]; then
|
if [[ ${#vm_menu[@]} -eq 0 ]]; then
|
||||||
dialog --backtitle "ProxMenux" \
|
dialog --backtitle "ProxMenux" \
|
||||||
@@ -221,6 +247,10 @@ validate_vm_requirements() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select_controller_nvme() {
|
select_controller_nvme() {
|
||||||
|
# Show progress during potentially slow PCIe + disk detection
|
||||||
|
set_title
|
||||||
|
msg_info "$(translate "Analyzing system for available PCIe storage devices...")"
|
||||||
|
|
||||||
_refresh_host_storage_cache
|
_refresh_host_storage_cache
|
||||||
|
|
||||||
local -a menu_items=()
|
local -a menu_items=()
|
||||||
@@ -251,12 +281,19 @@ select_controller_nvme() {
|
|||||||
_array_contains "$disk" "${controller_disks[@]}" || controller_disks+=("$disk")
|
_array_contains "$disk" "${controller_disks[@]}" || controller_disks+=("$disk")
|
||||||
done < <(_controller_block_devices "$pci_full")
|
done < <(_controller_block_devices "$pci_full")
|
||||||
|
|
||||||
|
# blocked_reasons: system disk OR disk in RUNNING guest → hide controller
|
||||||
|
# warn_reasons: disk in STOPPED guest only → show with ⚠ but allow selection
|
||||||
local -a blocked_reasons=()
|
local -a blocked_reasons=()
|
||||||
|
local -a warn_reasons=()
|
||||||
for disk in "${controller_disks[@]}"; do
|
for disk in "${controller_disks[@]}"; do
|
||||||
if _disk_is_host_system_used "$disk"; then
|
if _disk_is_host_system_used "$disk"; then
|
||||||
blocked_reasons+=("${disk} (${DISK_USAGE_REASON})")
|
blocked_reasons+=("${disk} (${DISK_USAGE_REASON})")
|
||||||
elif _disk_used_in_guest_configs "$disk"; then
|
elif _disk_used_in_guest_configs "$disk"; then
|
||||||
blocked_reasons+=("${disk} ($(translate "In use by VM/LXC config"))")
|
if _disk_used_in_running_guest "$disk"; then
|
||||||
|
blocked_reasons+=("${disk} ($(translate "In use by running VM/LXC — stop it first"))")
|
||||||
|
else
|
||||||
|
warn_reasons+=("$disk")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -266,21 +303,30 @@ select_controller_nvme() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local short_name
|
local short_name display_name
|
||||||
short_name=$(_shorten_text "$name" 42)
|
display_name=$(_pci_storage_display_name "$pci_full")
|
||||||
|
short_name=$(_shorten_text "$display_name" 56)
|
||||||
|
|
||||||
local assigned_suffix=""
|
local assigned_suffix=""
|
||||||
if [[ -n "$(_pci_assigned_vm_ids "$pci_full" "$SELECTED_VMID" 2>/dev/null | head -1)" ]]; then
|
if [[ -n "$(_pci_assigned_vm_ids "$pci_full" "$SELECTED_VMID" 2>/dev/null | head -1)" ]]; then
|
||||||
assigned_suffix=" | $(translate "Assigned to VM")"
|
assigned_suffix=" | $(translate "Assigned to VM")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
controller_desc="${short_name}${assigned_suffix}"
|
# Warn if some disks are referenced in stopped VM/CT configs
|
||||||
|
local warn_suffix=""
|
||||||
|
if [[ ${#warn_reasons[@]} -gt 0 ]]; then
|
||||||
|
warn_suffix=" ⚠"
|
||||||
|
fi
|
||||||
|
|
||||||
|
controller_desc="${short_name}${assigned_suffix}${warn_suffix}"
|
||||||
|
|
||||||
state="off"
|
state="off"
|
||||||
menu_items+=("$pci_full" "$controller_desc" "$state")
|
menu_items+=("$pci_full" "$controller_desc" "$state")
|
||||||
safe_count=$((safe_count + 1))
|
safe_count=$((safe_count + 1))
|
||||||
done < <(ls -d /sys/bus/pci/devices/* 2>/dev/null | sort)
|
done < <(ls -d /sys/bus/pci/devices/* 2>/dev/null | sort)
|
||||||
|
|
||||||
|
stop_spinner
|
||||||
|
|
||||||
if [[ "$safe_count" -eq 0 ]]; then
|
if [[ "$safe_count" -eq 0 ]]; then
|
||||||
local msg
|
local msg
|
||||||
if [[ "$hidden_target_count" -gt 0 && "$blocked_count" -eq 0 ]]; then
|
if [[ "$hidden_target_count" -gt 0 && "$blocked_count" -eq 0 ]]; then
|
||||||
@@ -318,29 +364,100 @@ select_controller_nvme() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>&1; then
|
|
||||||
if ! _vm_storage_confirm_controller_passthrough_risk "$SELECTED_VMID" "$SELECTED_VM_NAME" "$(translate "Controller + NVMe")"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm_summary() {
|
_prompt_raw_disk_conflict_policy() {
|
||||||
local msg
|
local disk="$1"
|
||||||
msg="\n$(translate "The following devices will be added to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\n\n"
|
shift
|
||||||
local pci info
|
local -a guest_ids=("$@")
|
||||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
local msg gid gtype gid_num gname gstatus
|
||||||
info=$(lspci -nn -s "${pci#0000:}" 2>/dev/null | sed 's/^[^ ]* //')
|
|
||||||
msg+=" - ${pci}${info:+ (${info})}\n"
|
|
||||||
done
|
|
||||||
msg+="\n$(translate "Do you want to continue?")"
|
|
||||||
|
|
||||||
dialog --backtitle "ProxMenux" --colors \
|
msg="$(translate "Disk") ${disk} $(translate "is referenced in the following stopped VM(s)/CT(s):")\\n\\n"
|
||||||
|
for gid in "${guest_ids[@]}"; do
|
||||||
|
gtype="${gid%%:*}"; gid_num="${gid##*:}"
|
||||||
|
if [[ "$gtype" == "VM" ]]; then
|
||||||
|
gname=$(_vm_name_by_id "$gid_num")
|
||||||
|
gstatus=$(qm status "$gid_num" 2>/dev/null | awk '{print $2}')
|
||||||
|
msg+=" - VM $gid_num ($gname) [${gstatus}]\\n"
|
||||||
|
else
|
||||||
|
gname=$(pct config "$gid_num" 2>/dev/null | awk '/^hostname:/ {print $2}')
|
||||||
|
[[ -z "$gname" ]] && gname="CT-$gid_num"
|
||||||
|
gstatus=$(pct status "$gid_num" 2>/dev/null | awk '{print $2}')
|
||||||
|
msg+=" - CT $gid_num ($gname) [${gstatus}]\\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
msg+="\\n$(translate "Choose action:")"
|
||||||
|
|
||||||
|
local choice
|
||||||
|
choice=$(dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Disk Reference Conflict")" \
|
||||||
|
--menu "$msg" 22 84 3 \
|
||||||
|
"1" "$(translate "Disable onboot on affected VM(s)/CT(s)")" \
|
||||||
|
"2" "$(translate "Remove disk references from affected VM(s)/CT(s) config")" \
|
||||||
|
"3" "$(translate "Skip — leave as-is")" \
|
||||||
|
2>&1 >/dev/tty) || { echo "skip"; return; }
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1) echo "disable_onboot" ;;
|
||||||
|
2) echo "remove_refs" ;;
|
||||||
|
*) echo "skip" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_summary() {
|
||||||
|
# ── Risk detection ─────────────────────────────────────────────────────────
|
||||||
|
local reinforce_limited_firmware="no"
|
||||||
|
local bios_date bios_year current_year bios_age cpu_model risk_detail=""
|
||||||
|
bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null)
|
||||||
|
bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1)
|
||||||
|
current_year=$(date +%Y 2>/dev/null)
|
||||||
|
if [[ -n "$bios_year" && -n "$current_year" ]]; then
|
||||||
|
bios_age=$(( current_year - bios_year ))
|
||||||
|
if (( bios_age >= 7 )); then
|
||||||
|
reinforce_limited_firmware="yes"
|
||||||
|
risk_detail="$(translate "BIOS from") ${bios_year} (${bios_age} $(translate "years old")) — $(translate "older firmware may increase passthrough instability")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs)
|
||||||
|
if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then
|
||||||
|
reinforce_limited_firmware="yes"
|
||||||
|
[[ -z "$risk_detail" ]] && risk_detail="$(translate "Low-power CPU platform"): ${cpu_model}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Build unified message ──────────────────────────────────────────────────
|
||||||
|
local msg pci display_name
|
||||||
|
msg="\n"
|
||||||
|
|
||||||
|
# Devices to add
|
||||||
|
msg+="\Zb$(translate "Devices to add to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\Zn\n"
|
||||||
|
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||||
|
display_name=$(_pci_storage_display_name "$pci")
|
||||||
|
msg+=" \Zb•\Zn ${pci} ${display_name}\n"
|
||||||
|
done
|
||||||
|
msg+="\n"
|
||||||
|
|
||||||
|
# Compatibility notice (always shown)
|
||||||
|
msg+="\Zb\Z4⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\Zn\n\n"
|
||||||
|
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
|
||||||
|
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
|
||||||
|
|
||||||
|
# Detected risk (only when applicable)
|
||||||
|
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
|
||||||
|
msg+="\n\Z1$(translate "Detected risk factor"): ${risk_detail}\Zn\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${SELECTED_VMID}.conf\n"
|
||||||
|
msg+="\n\Zb$(translate "Do you want to continue?")\Zn"
|
||||||
|
|
||||||
|
local height=22
|
||||||
|
[[ "$reinforce_limited_firmware" == "yes" ]] && height=25
|
||||||
|
|
||||||
|
if ! dialog --backtitle "ProxMenux" --colors \
|
||||||
--title "$(translate "Confirm Controller + NVMe Assignment")" \
|
--title "$(translate "Confirm Controller + NVMe Assignment")" \
|
||||||
--yesno "$msg" 18 90
|
--yesno "$msg" $height 90; then
|
||||||
[[ $? -ne 0 ]] && return 1
|
return 1
|
||||||
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +466,7 @@ prompt_controller_conflict_policy() {
|
|||||||
shift
|
shift
|
||||||
local -a source_vms=("$@")
|
local -a source_vms=("$@")
|
||||||
local msg vmid vm_name st ob
|
local msg vmid vm_name st ob
|
||||||
msg="$(translate "Selected device is already assigned to other VM(s):")\n\n"
|
msg="\n$(translate "Selected device is already assigned to other VM(s):")\n\n"
|
||||||
for vmid in "${source_vms[@]}"; do
|
for vmid in "${source_vms[@]}"; do
|
||||||
vm_name=$(_vm_name_by_id "$vmid")
|
vm_name=$(_vm_name_by_id "$vmid")
|
||||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||||
@@ -359,11 +476,13 @@ prompt_controller_conflict_policy() {
|
|||||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||||
|
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
choice=$(dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Controller/NVMe Conflict Policy")" \
|
||||||
|
--menu "$msg" 20 80 10 \
|
||||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||||
"3" "$(translate "Skip this device")" \
|
"3" "$(translate "Skip this device")" \
|
||||||
3>&1 1>&2 2>&3) || { echo "skip"; return; }
|
2>&1 >/dev/tty) || { echo "skip"; return; }
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1) echo "keep_disable_onboot" ;;
|
1) echo "keep_disable_onboot" ;;
|
||||||
@@ -372,17 +491,143 @@ prompt_controller_conflict_policy() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── DIALOG PHASE: resolve all conflicts before terminal ───────────────────────
|
||||||
|
resolve_disk_conflicts() {
|
||||||
|
local -a new_pci_list=()
|
||||||
|
local pci vmid action slot_base scope_key has_running
|
||||||
|
|
||||||
|
# ── hostpci conflicts: controller already assigned to another VM ──────────
|
||||||
|
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||||
|
local -a source_vms=()
|
||||||
|
mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ ${#source_vms[@]} -eq 0 ]]; then
|
||||||
|
new_pci_list+=("$pci")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
has_running=false
|
||||||
|
for vmid in "${source_vms[@]}"; do
|
||||||
|
if _vm_status_is_running "$vmid"; then
|
||||||
|
has_running=true
|
||||||
|
dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Device In Use")" \
|
||||||
|
--msgbox "\n$(translate "Controller") $pci $(translate "is in use by running VM") $vmid.\n\n$(translate "Stop it first and run this option again.")" \
|
||||||
|
10 72
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
$has_running && continue
|
||||||
|
|
||||||
|
scope_key=$(printf '%s,' "${source_vms[@]}")
|
||||||
|
if [[ -n "$WIZARD_CONFLICT_POLICY" && "$WIZARD_CONFLICT_SCOPE" == "$scope_key" ]]; then
|
||||||
|
action="$WIZARD_CONFLICT_POLICY"
|
||||||
|
else
|
||||||
|
action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}")
|
||||||
|
WIZARD_CONFLICT_POLICY="$action"
|
||||||
|
WIZARD_CONFLICT_SCOPE="$scope_key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
keep_disable_onboot)
|
||||||
|
for vmid in "${source_vms[@]}"; do
|
||||||
|
_vm_onboot_is_enabled "$vmid" && qm set "$vmid" -onboot 0 >/dev/null 2>&1
|
||||||
|
done
|
||||||
|
NEED_HOOK_SYNC=true
|
||||||
|
new_pci_list+=("$pci")
|
||||||
|
;;
|
||||||
|
move_remove_source)
|
||||||
|
slot_base=$(_pci_slot_base "$pci")
|
||||||
|
for vmid in "${source_vms[@]}"; do
|
||||||
|
_remove_pci_slot_from_vm_config "$vmid" "$slot_base"
|
||||||
|
done
|
||||||
|
new_pci_list+=("$pci")
|
||||||
|
;;
|
||||||
|
*) ;; # skip — do not add to new_pci_list
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SELECTED_CONTROLLER_PCIS=("${new_pci_list[@]}")
|
||||||
|
|
||||||
|
if [[ ${#SELECTED_CONTROLLER_PCIS[@]} -eq 0 ]]; then
|
||||||
|
dialog --backtitle "ProxMenux" \
|
||||||
|
--title "$(translate "Controller + NVMe")" \
|
||||||
|
--msgbox "\n$(translate "No controllers remaining after conflict resolution.")" 8 64
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Raw disk passthrough conflicts ───────────────────────────────────────
|
||||||
|
local raw_disk_policy="" raw_disk_scope=""
|
||||||
|
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||||
|
local -a cdisks=()
|
||||||
|
while IFS= read -r disk; do
|
||||||
|
[[ -z "$disk" ]] && continue
|
||||||
|
_array_contains "$disk" "${cdisks[@]}" || cdisks+=("$disk")
|
||||||
|
done < <(_controller_block_devices "$pci")
|
||||||
|
|
||||||
|
for disk in "${cdisks[@]}"; do
|
||||||
|
_disk_used_in_guest_configs "$disk" || continue
|
||||||
|
_disk_used_in_running_guest "$disk" && continue
|
||||||
|
|
||||||
|
local -a guest_ids=()
|
||||||
|
mapfile -t guest_ids < <(_disk_guest_ids "$disk")
|
||||||
|
[[ ${#guest_ids[@]} -eq 0 ]] && continue
|
||||||
|
|
||||||
|
local gscope gaction
|
||||||
|
gscope=$(printf '%s,' "${guest_ids[@]}")
|
||||||
|
if [[ -n "$raw_disk_policy" && "$raw_disk_scope" == "$gscope" ]]; then
|
||||||
|
gaction="$raw_disk_policy"
|
||||||
|
else
|
||||||
|
gaction=$(_prompt_raw_disk_conflict_policy "$disk" "${guest_ids[@]}")
|
||||||
|
raw_disk_policy="$gaction"
|
||||||
|
raw_disk_scope="$gscope"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local gid gtype gid_num slot
|
||||||
|
case "$gaction" in
|
||||||
|
disable_onboot)
|
||||||
|
for gid in "${guest_ids[@]}"; do
|
||||||
|
gtype="${gid%%:*}"; gid_num="${gid##*:}"
|
||||||
|
if [[ "$gtype" == "VM" ]]; then
|
||||||
|
_vm_onboot_is_enabled "$gid_num" && qm set "$gid_num" -onboot 0 >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
grep -qE '^onboot:\s*1' "/etc/pve/lxc/$gid_num.conf" 2>/dev/null && \
|
||||||
|
pct set "$gid_num" -onboot 0 >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
remove_refs)
|
||||||
|
for gid in "${guest_ids[@]}"; do
|
||||||
|
gtype="${gid%%:*}"; gid_num="${gid##*:}"
|
||||||
|
if [[ "$gtype" == "VM" ]]; then
|
||||||
|
while IFS= read -r slot; do
|
||||||
|
[[ -z "$slot" ]] && continue
|
||||||
|
qm set "$gid_num" -delete "$slot" >/dev/null 2>&1
|
||||||
|
done < <(_find_disk_slots_in_vm "$gid_num" "$disk")
|
||||||
|
else
|
||||||
|
while IFS= read -r slot; do
|
||||||
|
[[ -z "$slot" ]] && continue
|
||||||
|
pct set "$gid_num" -delete "$slot" >/dev/null 2>&1
|
||||||
|
done < <(_find_disk_slots_in_ct "$gid_num" "$disk")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
apply_assignment() {
|
apply_assignment() {
|
||||||
: >"$LOG_FILE"
|
: >"$LOG_FILE"
|
||||||
set_title
|
set_title
|
||||||
echo
|
|
||||||
|
|
||||||
msg_info "$(translate "Applying Controller/NVMe passthrough to VM") ${SELECTED_VMID}..."
|
msg_info "$(translate "Applying Controller/NVMe passthrough to VM") ${SELECTED_VMID}..."
|
||||||
msg_ok "$(translate "Target VM validated") (${SELECTED_VM_NAME} / ${SELECTED_VMID})"
|
msg_ok "$(translate "Target VM validated") (${SELECTED_VM_NAME} / ${SELECTED_VMID})"
|
||||||
msg_ok "$(translate "Selected devices"): ${#SELECTED_CONTROLLER_PCIS[@]}"
|
msg_ok "$(translate "Selected devices"): ${#SELECTED_CONTROLLER_PCIS[@]}"
|
||||||
|
|
||||||
local hostpci_idx=0
|
local hostpci_idx=0
|
||||||
msg_info "$(translate "Calculating next available hostpci slot...")"
|
|
||||||
if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then
|
if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then
|
||||||
hostpci_idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0)
|
hostpci_idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0)
|
||||||
else
|
else
|
||||||
@@ -392,10 +637,8 @@ apply_assignment() {
|
|||||||
hostpci_idx=$((hostpci_idx + 1))
|
hostpci_idx=$((hostpci_idx + 1))
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
msg_ok "$(translate "Next available hostpci slot"): hostpci${hostpci_idx}"
|
|
||||||
|
|
||||||
local pci bdf assigned_count=0
|
local pci bdf assigned_count=0
|
||||||
local need_hook_sync=false
|
|
||||||
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
|
||||||
bdf="${pci#0000:}"
|
bdf="${pci#0000:}"
|
||||||
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
|
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
|
||||||
@@ -408,50 +651,11 @@ apply_assignment() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local -a source_vms=()
|
local display_name
|
||||||
mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null)
|
display_name=$(_pci_storage_display_name "$pci")
|
||||||
if [[ ${#source_vms[@]} -gt 0 ]]; then
|
msg_info "$(translate "Adding") ${display_name} (${pci}) → hostpci${hostpci_idx}..."
|
||||||
local has_running=false vmid action slot_base
|
if qm set "$SELECTED_VMID" "--hostpci${hostpci_idx}" "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then
|
||||||
for vmid in "${source_vms[@]}"; do
|
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})"
|
||||||
if _vm_status_is_running "$vmid"; then
|
|
||||||
has_running=true
|
|
||||||
msg_warn "$(translate "Controller/NVMe is in use by running VM") ${vmid} ($(translate "stop source VM first"))"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if $has_running; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}")
|
|
||||||
case "$action" in
|
|
||||||
keep_disable_onboot)
|
|
||||||
for vmid in "${source_vms[@]}"; do
|
|
||||||
if _vm_onboot_is_enabled "$vmid"; then
|
|
||||||
if qm set "$vmid" -onboot 0 >>"$LOG_FILE" 2>&1; then
|
|
||||||
msg_warn "$(translate "Start on boot disabled for VM") ${vmid}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
need_hook_sync=true
|
|
||||||
;;
|
|
||||||
move_remove_source)
|
|
||||||
slot_base=$(_pci_slot_base "$pci")
|
|
||||||
for vmid in "${source_vms[@]}"; do
|
|
||||||
if _remove_pci_slot_from_vm_config "$vmid" "$slot_base"; then
|
|
||||||
msg_ok "$(translate "Controller/NVMe removed from source VM") ${vmid} (${pci})"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
msg_info2 "$(translate "Skipped device"): ${pci}"
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if qm set "$SELECTED_VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then
|
|
||||||
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} -> ${pci})"
|
|
||||||
assigned_count=$((assigned_count + 1))
|
assigned_count=$((assigned_count + 1))
|
||||||
hostpci_idx=$((hostpci_idx + 1))
|
hostpci_idx=$((hostpci_idx + 1))
|
||||||
else
|
else
|
||||||
@@ -459,33 +663,50 @@ apply_assignment() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if $need_hook_sync && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
|
if $NEED_HOOK_SYNC && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
|
||||||
ensure_proxmenux_gpu_guard_hookscript
|
ensure_proxmenux_gpu_guard_hookscript
|
||||||
sync_proxmenux_gpu_guard_hooks
|
sync_proxmenux_gpu_guard_hooks
|
||||||
msg_ok "$(translate "VM hook guard synced for shared controller/NVMe protection")"
|
msg_ok "$(translate "VM hook guard synced for shared controller/NVMe protection")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo ""
|
||||||
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
|
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
|
||||||
|
|
||||||
if [[ "$assigned_count" -gt 0 ]]; then
|
if [[ "$assigned_count" -gt 0 ]]; then
|
||||||
msg_success "$(translate "Completed. Controller/NVMe passthrough configured for VM") ${SELECTED_VMID}."
|
msg_ok "$(translate "Completed.") $assigned_count $(translate "device(s) added to VM") ${SELECTED_VMID}."
|
||||||
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
|
||||||
msg_warn "$(translate "IOMMU was configured during this run. Reboot the host before starting the VM.")"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
msg_warn "$(translate "No new Controller/NVMe entries were added.")"
|
msg_warn "$(translate "No new Controller/NVMe entries were added.")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${IOMMU_ALREADY_ACTIVE:-0}" == "1" ]]; then
|
||||||
|
msg_ok "$(translate "IOMMU is enabled on the system")"
|
||||||
|
elif [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
|
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||||
|
echo ""
|
||||||
|
if whiptail --title "$(translate "Reboot Required")" \
|
||||||
|
--yesno "\n$(translate "IOMMU has been enabled on this system. A reboot is required to apply the changes. Reboot now?")" 11 64; then
|
||||||
|
msg_success "$(translate "Press Enter to continue...")"
|
||||||
|
read -r
|
||||||
|
msg_warn "$(translate "Rebooting the system...")"
|
||||||
|
reboot
|
||||||
|
else
|
||||||
|
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||||
|
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
msg_success "$(translate "Press Enter to continue...")"
|
msg_success "$(translate "Press Enter to continue...")"
|
||||||
read -r
|
read -r
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
select_target_vm || exit 0
|
export WIZARD_CONFLICT_POLICY
|
||||||
|
export WIZARD_CONFLICT_SCOPE
|
||||||
|
select_target_vm || exit 0
|
||||||
validate_vm_requirements || exit 0
|
validate_vm_requirements || exit 0
|
||||||
select_controller_nvme || exit 0
|
select_controller_nvme || exit 0
|
||||||
confirm_summary || exit 0
|
resolve_disk_conflicts || exit 0
|
||||||
clear
|
confirm_summary || exit 0
|
||||||
apply_assignment
|
apply_assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.0
|
# Version : 1.2
|
||||||
# Last Updated: 28/01/2025
|
# Last Updated: 12/04/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# This script allows users to assign physical disks to existing
|
# This script allows users to assign physical disks to existing
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
# - Ensures that disks are not already assigned to active VMs.
|
# - Ensures that disks are not already assigned to active VMs.
|
||||||
# - Warns about disk sharing between multiple VMs to avoid data corruption.
|
# - Warns about disk sharing between multiple VMs to avoid data corruption.
|
||||||
# - Configures the selected disks for the VM and verifies the assignment.
|
# - Configures the selected disks for the VM and verifies the assignment.
|
||||||
|
# - Prefers persistent /dev/disk/by-id paths for assignment when available.
|
||||||
#
|
#
|
||||||
# The goal of this script is to simplify the process of assigning
|
# The goal of this script is to simplify the process of assigning
|
||||||
# physical disks to Proxmox VMs, reducing manual configurations
|
# physical disks to Proxmox VMs, reducing manual configurations
|
||||||
@@ -28,134 +29,181 @@
|
|||||||
|
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
|
||||||
|
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||||
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||||
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
if [[ -f "$UTILS_FILE" ]]; then
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
source "$UTILS_FILE"
|
source "$UTILS_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh"
|
||||||
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKTITLE="ProxMenux"
|
||||||
|
UI_MENU_H=20
|
||||||
|
UI_MENU_W=84
|
||||||
|
UI_MENU_LIST_H=10
|
||||||
|
UI_SHORT_MENU_H=16
|
||||||
|
UI_SHORT_MENU_W=72
|
||||||
|
UI_SHORT_MENU_LIST_H=6
|
||||||
|
UI_MSG_H=10
|
||||||
|
UI_MSG_W=72
|
||||||
|
UI_YESNO_H=18
|
||||||
|
UI_YESNO_W=86
|
||||||
|
UI_RESULT_H=18
|
||||||
|
UI_RESULT_W=86
|
||||||
|
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
# ==========================================================
|
|
||||||
|
|
||||||
|
if ! command -v pveversion >/dev/null 2>&1; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
get_disk_info() {
|
get_disk_info() {
|
||||||
local disk=$1
|
local disk=$1
|
||||||
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
|
local model size
|
||||||
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
|
model=$(lsblk -dn -o MODEL "$disk" | xargs)
|
||||||
echo "$MODEL" "$SIZE"
|
size=$(lsblk -dn -o SIZE "$disk" | xargs)
|
||||||
|
[[ -z "$model" ]] && model="Unknown"
|
||||||
|
printf '%s\t%s\n' "$model" "$size"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_all_disk_paths() {
|
||||||
|
local disk="$1"
|
||||||
|
local real_path
|
||||||
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
|
||||||
|
[[ -n "$disk" ]] && echo "$disk"
|
||||||
|
[[ -n "$real_path" ]] && echo "$real_path"
|
||||||
|
|
||||||
|
local link
|
||||||
|
for link in /dev/disk/by-id/* /dev/disk/by-path/*; do
|
||||||
|
[[ -e "$link" ]] || continue
|
||||||
|
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
||||||
|
echo "$link"
|
||||||
|
done | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
get_preferred_disk_path() {
|
||||||
|
local disk="$1"
|
||||||
|
local real_path
|
||||||
|
real_path=$(readlink -f "$disk" 2>/dev/null)
|
||||||
|
[[ -z "$real_path" ]] && { echo "$disk"; return 0; }
|
||||||
|
|
||||||
|
local best="" best_score=99999
|
||||||
|
local link name score
|
||||||
|
for link in /dev/disk/by-id/*; do
|
||||||
|
[[ -e "$link" ]] || continue
|
||||||
|
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
|
||||||
|
name=$(basename "$link")
|
||||||
|
[[ "$name" == *-part* ]] && continue
|
||||||
|
|
||||||
|
case "$name" in
|
||||||
|
ata-*|scsi-*|nvme-*) score=100 ;;
|
||||||
|
wwn-*) score=200 ;;
|
||||||
|
*) score=300 ;;
|
||||||
|
esac
|
||||||
|
score=$((score + ${#name}))
|
||||||
|
if (( score < best_score )); then
|
||||||
|
best="$link"
|
||||||
|
best_score=$score
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$best" ]]; then
|
||||||
|
echo "$best"
|
||||||
|
else
|
||||||
|
echo "$disk"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
disk_referenced_in_config() {
|
||||||
|
local config_text="$1"
|
||||||
|
local disk="$2"
|
||||||
|
local alias
|
||||||
|
while read -r alias; do
|
||||||
|
[[ -z "$alias" ]] && continue
|
||||||
|
if grep -Fq "$alias" <<< "$config_text"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(get_all_disk_paths "$disk")
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── DIALOG PHASE ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
VM_LIST=$(qm list | awk 'NR>1 {print $1, $2}')
|
VM_LIST=$(qm list | awk 'NR>1 {print $1, $2}')
|
||||||
if [ -z "$VM_LIST" ]; then
|
if [ -z "$VM_LIST" ]; then
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No VMs available in the system.")" 8 40
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "No VMs available in the system.")" $UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
VMID=$(whiptail --title "$(translate "Select VM")" --menu "$(translate "Select the VM to which you want to add disks:")" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
|
VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Select VM")" \
|
||||||
|
--menu "$(translate "Select the VM to which you want to add disks:")" $UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
$VM_LIST \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
|
||||||
if [ -z "$VMID" ]; then
|
if [ -z "$VMID" ]; then
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No VM was selected.")" 8 40
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "No VM was selected.")" $UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VMID=$(echo "$VMID" | tr -d '"')
|
VMID=$(echo "$VMID" | tr -d '"')
|
||||||
|
|
||||||
clear
|
|
||||||
show_proxmenux_logo
|
|
||||||
echo -e
|
|
||||||
msg_title "$(translate "Import Disk to VM")"
|
|
||||||
echo -e
|
|
||||||
|
|
||||||
msg_ok "$(translate "VM selected successfully.")"
|
|
||||||
|
|
||||||
|
|
||||||
VM_STATUS=$(qm status "$VMID" | awk '{print $2}')
|
VM_STATUS=$(qm status "$VMID" | awk '{print $2}')
|
||||||
if [ "$VM_STATUS" == "running" ]; then
|
if [ "$VM_STATUS" == "running" ]; then
|
||||||
whiptail --title "$(translate "Warning")" --msgbox "$(translate "The VM is powered on. Turn it off before adding disks.")" 12 60
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Warning")" \
|
||||||
|
--msgbox "$(translate "The VM is powered on. Turn it off before adding disks.")" $UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
##########################################
|
# ── TERMINAL PHASE 1: detect disks ────────────────────────────────────────────
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Import Disk to VM")"
|
||||||
|
msg_ok "$(translate "VM $VMID selected successfully.")"
|
||||||
msg_info "$(translate "Detecting available disks...")"
|
msg_info "$(translate "Detecting available disks...")"
|
||||||
|
|
||||||
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
|
_refresh_host_storage_cache
|
||||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
VM_CONFIG=$(qm config "$VMID" 2>/dev/null | grep -vE '^\s*#|^description:')
|
||||||
|
|
||||||
ZFS_DISKS=""
|
|
||||||
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
|
||||||
|
|
||||||
for entry in $ZFS_RAW; do
|
|
||||||
|
|
||||||
path=""
|
|
||||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
|
||||||
if [ -e "/dev/disk/by-id/$entry" ]; then
|
|
||||||
path=$(readlink -f "/dev/disk/by-id/$entry")
|
|
||||||
fi
|
|
||||||
elif [[ "$entry" == /dev/* ]]; then
|
|
||||||
path="$entry"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -n "$path" ]; then
|
|
||||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
|
||||||
if [ -n "$base_disk" ]; then
|
|
||||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
|
||||||
|
|
||||||
|
|
||||||
is_disk_in_use() {
|
|
||||||
local disk="$1"
|
|
||||||
|
|
||||||
|
|
||||||
while read -r part fstype; do
|
|
||||||
case "$fstype" in
|
|
||||||
zfs_member|linux_raid_member)
|
|
||||||
return 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
|
|
||||||
|
|
||||||
|
|
||||||
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
FREE_DISKS=()
|
FREE_DISKS=()
|
||||||
|
|
||||||
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
|
|
||||||
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
|
|
||||||
|
|
||||||
while read -r DISK; do
|
while read -r DISK; do
|
||||||
|
|
||||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
[[ "$DISK" =~ /dev/zd ]] && continue
|
||||||
|
|
||||||
INFO=($(get_disk_info "$DISK"))
|
IFS=$'\t' read -r MODEL SIZE < <(get_disk_info "$DISK")
|
||||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
|
||||||
SIZE="${INFO[-1]}"
|
|
||||||
LABEL=""
|
LABEL=""
|
||||||
SHOW_DISK=true
|
SHOW_DISK=true
|
||||||
|
|
||||||
IS_MOUNTED=false
|
IS_MOUNTED=false
|
||||||
IS_RAID=false
|
IS_RAID=false
|
||||||
IS_ZFS=false
|
IS_ZFS=false
|
||||||
@@ -175,46 +223,34 @@ while read -r DISK; do
|
|||||||
IS_MOUNTED=true
|
IS_MOUNTED=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
USED_BY=""
|
USED_BY=""
|
||||||
REAL_PATH=$(readlink -f "$DISK")
|
if _disk_used_in_guest_configs "$DISK"; then
|
||||||
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
|
|
||||||
|
|
||||||
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
|
|
||||||
USED_BY="⚠ $(translate "In use")"
|
USED_BY="⚠ $(translate "In use")"
|
||||||
else
|
|
||||||
for SYMLINK in /dev/disk/by-id/*; do
|
|
||||||
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
|
|
||||||
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
|
|
||||||
USED_BY="⚠ $(translate "In use")"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
||||||
if grep -q "active raid" /proc/mdstat; then
|
if grep -q "active raid" /proc/mdstat; then
|
||||||
SHOW_DISK=false
|
SHOW_DISK=false
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if $IS_ZFS; then
|
if $IS_ZFS; then
|
||||||
SHOW_DISK=false
|
SHOW_DISK=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Catch whole-disk ZFS vdevs with no partitions (e.g. bare NVMe ZFS)
|
||||||
|
# The tail -n +2 trick misses them; ZFS_DISKS from _refresh_host_storage_cache covers them.
|
||||||
|
if [[ -n "$ZFS_DISKS" ]] && \
|
||||||
|
{ grep -qFx "$DISK" <<< "$ZFS_DISKS" || \
|
||||||
|
{ [[ -n "$REAL_PATH" ]] && grep -qFx "$REAL_PATH" <<< "$ZFS_DISKS"; }; }; then
|
||||||
|
SHOW_DISK=false
|
||||||
|
fi
|
||||||
|
|
||||||
if $IS_MOUNTED; then
|
if $IS_MOUNTED; then
|
||||||
SHOW_DISK=false
|
SHOW_DISK=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if disk_referenced_in_config "$VM_CONFIG" "$DISK"; then
|
||||||
if qm config "$VMID" | grep -vE '^\s*#|^description:' | grep -q "$DISK"; then
|
|
||||||
SHOW_DISK=false
|
SHOW_DISK=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -229,144 +265,188 @@ while read -r DISK; do
|
|||||||
fi
|
fi
|
||||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
done < <(lsblk -dn -e 7,11 -o PATH)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
||||||
cleanup
|
stop_spinner
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks available for this VM.")" 8 40
|
dialog --backtitle "$BACKTITLE" \
|
||||||
clear
|
--title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "No disks available for this VM.")" $UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
stop_spinner
|
||||||
msg_ok "$(translate "Available disks detected.")"
|
msg_ok "$(translate "Available disks detected.")"
|
||||||
|
|
||||||
|
|
||||||
|
# ── DIALOG PHASE: select disks + interface ────────────────────────────────────
|
||||||
######################################################
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
|
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
|
||||||
TOTAL_WIDTH=$((MAX_WIDTH + 20))
|
TOTAL_WIDTH=$((MAX_WIDTH + 20))
|
||||||
|
if [ $TOTAL_WIDTH -lt $UI_MENU_W ]; then
|
||||||
if [ $TOTAL_WIDTH -lt 50 ]; then
|
TOTAL_WIDTH=$UI_MENU_W
|
||||||
TOTAL_WIDTH=50
|
fi
|
||||||
|
if [ $TOTAL_WIDTH -gt 116 ]; then
|
||||||
|
TOTAL_WIDTH=116
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
SELECTED=$(dialog --backtitle "$BACKTITLE" \
|
||||||
SELECTED=$(whiptail --title "$(translate "Select Disks")" --checklist \
|
--title "$(translate "Select Disks")" \
|
||||||
"$(translate "Select the disks you want to add:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3)
|
--checklist "\n$(translate "Select the disks you want to add:")" $UI_MENU_H $TOTAL_WIDTH $UI_MENU_LIST_H \
|
||||||
|
"${FREE_DISKS[@]}" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
|
||||||
if [ -z "$SELECTED" ]; then
|
if [ -z "$SELECTED" ]; then
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks were selected.")" 10 64
|
dialog --backtitle "$BACKTITLE" \
|
||||||
clear
|
--title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "No disks were selected.")" $UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_ok "$(translate "Disks selected successfully.")"
|
INTERFACE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Interface Type")" \
|
||||||
|
--menu "$(translate "Select the interface type for all disks:")" $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
|
||||||
INTERFACE=$(whiptail --title "$(translate "Interface Type")" --menu "$(translate "Select the interface type for all disks:")" 15 40 4 \
|
"sata" "$(translate "Add as SATA")" \
|
||||||
"sata" "$(translate "Add as SATA")" \
|
"scsi" "$(translate "Add as SCSI")" \
|
||||||
"scsi" "$(translate "Add as SCSI")" \
|
|
||||||
"virtio" "$(translate "Add as VirtIO")" \
|
"virtio" "$(translate "Add as VirtIO")" \
|
||||||
"ide" "$(translate "Add as IDE")" 3>&1 1>&2 2>&3)
|
"ide" "$(translate "Add as IDE")" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
|
||||||
if [ -z "$INTERFACE" ]; then
|
if [ -z "$INTERFACE" ]; then
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No interface type was selected for the disks.")" 8 40
|
dialog --backtitle "$BACKTITLE" \
|
||||||
clear
|
--title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "No interface type was selected for the disks.")" $UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_ok "$(translate "Interface type selected: $INTERFACE")"
|
|
||||||
|
|
||||||
DISKS_ADDED=0
|
# ── DIALOG PHASE: per-disk pre-check ──────────────────────────────────────────
|
||||||
ERROR_MESSAGES=""
|
declare -a DISK_LIST=()
|
||||||
SUCCESS_MESSAGES=""
|
declare -a DISK_DESCRIPTIONS=()
|
||||||
|
declare -a DISK_ASSIGNED_TOS=()
|
||||||
|
declare -a NVME_SKIPPED=()
|
||||||
|
|
||||||
msg_info "$(translate "Processing selected disks...")"
|
|
||||||
|
|
||||||
for DISK in $SELECTED; do
|
for DISK in $SELECTED; do
|
||||||
DISK=$(echo "$DISK" | tr -d '"')
|
DISK="${DISK//\"/}"
|
||||||
DISK_INFO=$(get_disk_info "$DISK")
|
DISK_INFO=$(get_disk_info "$DISK")
|
||||||
|
|
||||||
ASSIGNED_TO=""
|
ASSIGNED_TO=""
|
||||||
RUNNING_VMS=""
|
RUNNING_VMS=""
|
||||||
RUNNING_CTS=""
|
RUNNING_CTS=""
|
||||||
|
|
||||||
|
|
||||||
while read -r VM_ID VM_NAME; do
|
while read -r VM_ID VM_NAME; do
|
||||||
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then
|
VM_CONFIG_RAW=$(qm config "$VM_ID" 2>/dev/null)
|
||||||
|
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$VM_CONFIG_RAW" "$DISK"; then
|
||||||
ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
|
ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
|
||||||
VM_STATUS=$(qm status "$VM_ID" | awk '{print $2}')
|
VM_STATUS_CHK=$(qm status "$VM_ID" | awk '{print $2}')
|
||||||
if [ "$VM_STATUS" == "running" ]; then
|
[[ "$VM_STATUS_CHK" == "running" ]] && RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
|
||||||
RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done < <(qm list | awk 'NR>1 {print $1, $2}')
|
done < <(qm list | awk 'NR>1 {print $1, $2}')
|
||||||
|
|
||||||
|
|
||||||
while read -r CT_ID CT_NAME; do
|
while read -r CT_ID CT_NAME; do
|
||||||
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && pct config "$CT_ID" | grep -q "$DISK"; then
|
CT_CONFIG_RAW=$(pct config "$CT_ID" 2>/dev/null)
|
||||||
|
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$CT_CONFIG_RAW" "$DISK"; then
|
||||||
ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
|
ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
|
||||||
CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}')
|
CT_STATUS_CHK=$(pct status "$CT_ID" | awk '{print $2}')
|
||||||
if [ "$CT_STATUS" == "running" ]; then
|
[[ "$CT_STATUS_CHK" == "running" ]] && RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
|
||||||
RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done < <(pct list | awk 'NR>1 {print $1, $2}')
|
done < <(pct list | awk 'NR>1 {print $1, $3}')
|
||||||
|
|
||||||
if [ -n "$RUNNING_VMS" ] || [ -n "$RUNNING_CTS" ]; then
|
if [ -n "$RUNNING_VMS" ] || [ -n "$RUNNING_CTS" ]; then
|
||||||
ERROR_MESSAGES+="$(translate "The disk") $DISK_INFO $(translate "is currently in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "You cannot add this disk while the VM or CT is running.")\\n$(translate "Please shut it down first and run this script again to add the disk.")\\n\\n"
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Disk In Use")" \
|
||||||
|
--msgbox "$(translate "The disk") $DISK_INFO $(translate "is in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "Stop them first and run this script again.")" $UI_RESULT_H $UI_RESULT_W
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$ASSIGNED_TO" ]; then
|
if [ -n "$ASSIGNED_TO" ]; then
|
||||||
cleanup
|
if ! dialog --backtitle "$BACKTITLE" \
|
||||||
whiptail --title "$(translate "Disk Already Assigned")" --yesno "$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" 15 70
|
--title "$(translate "Disk Already Assigned")" \
|
||||||
if [ $? -ne 0 ]; then
|
--yesno "\n\n$(translate "The disk") $DISK_INFO $(translate "is already assigned to the following VM(s) or CT(s):")\\n$ASSIGNED_TO\\n\\n$(translate "Do you want to continue anyway?")" $UI_YESNO_H $UI_YESNO_W; then
|
||||||
sleep 1
|
continue
|
||||||
exec "$0"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# NVMe: suggest PCIe passthrough for better performance
|
||||||
|
if [[ "$DISK" =~ /dev/nvme ]] || \
|
||||||
|
[[ "$(lsblk -dn -o TRAN "$DISK" 2>/dev/null | xargs)" == "nvme" ]]; then
|
||||||
|
NVME_CHOICE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "NVMe Disk Detected")" \
|
||||||
|
--default-item "disk" \
|
||||||
|
--menu "\n$(translate "Adding this NVMe as a PCIe device (via 'Add Controller or NVMe PCIe to VM') gives better performance.")\n\n$(translate "How do you want to add it?")" \
|
||||||
|
$UI_YESNO_H $UI_YESNO_W 2 \
|
||||||
|
"disk" "$(translate "Add as disk (standard)")" \
|
||||||
|
"pci" "$(translate "Skip — I will add it as PCIe device")" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
if [[ "$NVME_CHOICE" == "pci" ]]; then
|
||||||
|
NVME_SKIPPED+=("$DISK")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
DISK_LIST+=("$DISK")
|
||||||
|
DISK_DESCRIPTIONS+=("$DISK_INFO")
|
||||||
|
DISK_ASSIGNED_TOS+=("$ASSIGNED_TO")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#DISK_LIST[@]}" -eq 0 ]; then
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Import Disk to VM")"
|
||||||
|
msg_warn "$(translate "No disks were configured for processing.")"
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
|
read -r
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ── TERMINAL PHASE: execute all disk operations ───────────────────────────────
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Import Disk to VM")"
|
||||||
|
msg_ok "$(translate "VM $VMID selected successfully.")"
|
||||||
|
msg_ok "$(translate "Disks to process:") ${#DISK_LIST[@]}"
|
||||||
|
for i in "${!DISK_LIST[@]}"; do
|
||||||
|
IFS=$'\t' read -r _desc_model _desc_size <<< "${DISK_DESCRIPTIONS[$i]}"
|
||||||
|
echo -e "${TAB}${BL}${DISK_LIST[$i]} $_desc_model $_desc_size${CL}"
|
||||||
|
done
|
||||||
|
if [[ ${#NVME_SKIPPED[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
msg_warn "$(translate "NVMe skipped (to add as PCIe use 'Add Controller or NVMe PCIe to VM'):")"
|
||||||
|
for _nvme in "${NVME_SKIPPED[@]}"; do
|
||||||
|
echo -e "${TAB}${BL}${_nvme}${CL}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
msg_ok "$(translate "Interface type:") $INTERFACE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
DISKS_ADDED=0
|
||||||
|
|
||||||
|
for i in "${!DISK_LIST[@]}"; do
|
||||||
|
DISK="${DISK_LIST[$i]}"
|
||||||
|
ASSIGNED_TO="${DISK_ASSIGNED_TOS[$i]}"
|
||||||
|
IFS=$'\t' read -r _model _size <<< "${DISK_DESCRIPTIONS[$i]}"
|
||||||
|
|
||||||
INDEX=0
|
INDEX=0
|
||||||
while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do
|
while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do
|
||||||
((INDEX++))
|
((INDEX++))
|
||||||
done
|
done
|
||||||
|
|
||||||
RESULT=$(qm set "$VMID" -${INTERFACE}${INDEX} "$DISK" 2>&1)
|
ASSIGN_PATH=$(get_preferred_disk_path "$DISK")
|
||||||
|
msg_info "$(translate "Adding") $_model $_size $(translate "as") ${INTERFACE}${INDEX}..."
|
||||||
if [ $? -eq 0 ]; then
|
if RESULT=$(qm set "$VMID" "-${INTERFACE}${INDEX}" "$ASSIGN_PATH" 2>&1); then
|
||||||
MESSAGE="$(translate "The disk") $DISK_INFO $(translate "has been successfully added to VM") $VMID."
|
msg_ok "$(translate "Disk added as") ${INTERFACE}${INDEX} $(translate "using") $ASSIGN_PATH"
|
||||||
if [ -n "$ASSIGNED_TO" ]; then
|
[[ -n "$ASSIGNED_TO" ]] && msg_warn "$(translate "WARNING: This disk is also assigned to:") $(echo -e "$ASSIGNED_TO" | tr '\n' ' ')"
|
||||||
MESSAGE+="\\n\\n$(translate "WARNING: This disk is also assigned to the following VM(s):")\\n$ASSIGNED_TO"
|
|
||||||
MESSAGE+="\\n$(translate "Make sure not to start VMs that share this disk at the same time to avoid data corruption.")"
|
|
||||||
fi
|
|
||||||
SUCCESS_MESSAGES+="$MESSAGE\\n\\n"
|
|
||||||
((DISKS_ADDED++))
|
((DISKS_ADDED++))
|
||||||
else
|
else
|
||||||
ERROR_MESSAGES+="$(translate "Could not add disk") $DISK_INFO $(translate "to VM") $VMID.\\n$(translate "Error:") $RESULT\\n\\n"
|
msg_error "$(translate "Could not add") $_model $_size: $RESULT"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
msg_ok "$(translate "Disk processing completed.")"
|
echo ""
|
||||||
|
if [ "$DISKS_ADDED" -gt 0 ]; then
|
||||||
|
msg_ok "$(translate "Completed.") $DISKS_ADDED $(translate "disk(s) added to VM") $VMID."
|
||||||
|
else
|
||||||
if [ -n "$SUCCESS_MESSAGES" ]; then
|
msg_warn "$(translate "No disks were added.")"
|
||||||
MSG_LINES=$(echo "$SUCCESS_MESSAGES" | wc -l)
|
|
||||||
whiptail --title "$(translate "Successful Operations")" --msgbox "$SUCCESS_MESSAGES" 16 70
|
|
||||||
fi
|
fi
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
if [ -n "$ERROR_MESSAGES" ]; then
|
read -r
|
||||||
whiptail --title "$(translate "Warnings and Errors")" --msgbox "$ERROR_MESSAGES" 16 70
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
183
scripts/storage/disk-storage-manual-guide.sh
Normal file
183
scripts/storage/disk-storage-manual-guide.sh
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==========================================================
|
||||||
|
# ProxMenux - Disk and Storage Manager Manual CLI Guide
|
||||||
|
# ==========================================================
|
||||||
|
# Author : MacRimi
|
||||||
|
# Copyright : (c) 2024 MacRimi
|
||||||
|
# License : GPL-3.0
|
||||||
|
# Version : 1.0
|
||||||
|
# Last Updated: 07/04/2026
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
||||||
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
||||||
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
||||||
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
||||||
|
elif [[ ! -f "$UTILS_FILE" ]]; then
|
||||||
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
|
source "$UTILS_FILE"
|
||||||
|
fi
|
||||||
|
load_language
|
||||||
|
initialize_cache
|
||||||
|
|
||||||
|
GREEN=$'\033[0;32m'
|
||||||
|
NC=$'\033[0m'
|
||||||
|
|
||||||
|
_cl() {
|
||||||
|
local num="$1" disp="$2" desc="$3"
|
||||||
|
local pad=$((47 - ${#disp}))
|
||||||
|
[[ $pad -lt 1 ]] && pad=1
|
||||||
|
local spaces
|
||||||
|
spaces=$(printf '%*s' "$pad" '')
|
||||||
|
printf " %2d) %s%s%s%s - %s\n" "$num" "$GREEN" "$disp" "$NC" "$spaces" "$desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
clear
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Disk and Storage Manager - Manual CLI Guide")"
|
||||||
|
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
_cl 1 "lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" "$(translate 'Inspect disks before any action')"
|
||||||
|
_cl 2 "ls -lh /dev/disk/by-id/" "$(translate 'Identify persistent disk paths')"
|
||||||
|
_cl 3 "qm list && pct list" "$(translate 'List VM/CT IDs to operate on')"
|
||||||
|
_cl 4 "qm config <vmid> | grep 'sata|scsi|hostpci'" "$(translate 'Check VM disk/PCI slots')"
|
||||||
|
_cl 5 "pvesm status -content images" "$(translate 'List storages valid for image import')"
|
||||||
|
_cl 6 "qm importdisk <vmid> <image_path> <storage>" "[T] $(translate 'Import disk image to VM')"
|
||||||
|
_cl 7 "qm set <vmid> --<iface><slot> <imported-disk>" "[T] $(translate 'Attach imported disk to VM')"
|
||||||
|
_cl 8 "qm set <vmid> --boot order=<iface><slot>" "[T] $(translate 'Set VM boot order')"
|
||||||
|
_cl 9 "lspci -nn | grep -Ei 'SATA|RAID|NVMe'" "$(translate 'Detect controller/NVMe BDF')"
|
||||||
|
_cl 10 "find /sys/kernel/iommu_groups -type l | grep BDF" "$(translate 'Verify IOMMU group for PCI device')"
|
||||||
|
_cl 11 "qm set <vmid> --hostpci<slot> <BDF>,pcie=1" "[T] $(translate 'Assign controller/NVMe passthrough')"
|
||||||
|
_cl 12 "pct config <ctid> | grep '^mp'" "$(translate 'Check container mount points')"
|
||||||
|
_cl 13 "pct set <ctid> -mp<slot> <disk>,mp=<path>" "[T] $(translate 'Add disk to LXC container')"
|
||||||
|
_cl 14 "wipefs -a -f /dev/sdX && sgdisk --zap-all /dev/sdX" "[T] $(translate 'Clean disk metadata')"
|
||||||
|
_cl 15 "parted -s /dev/sdX mklabel gpt mkpart primary" "[T] $(translate 'Create GPT partition')"
|
||||||
|
_cl 16 "mkfs.ext4 -F /dev/sdX1 (or mkfs.xfs / mkfs.btrfs)" "[T] $(translate 'Format filesystem')"
|
||||||
|
_cl 17 "pvesm status && zpool status" "$(translate 'Final storage health/status check')"
|
||||||
|
echo -e " ${DEF} 0) $(translate 'Back to previous menu or Esc + Enter')${CL}"
|
||||||
|
echo
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
|
||||||
|
read -r user_input
|
||||||
|
|
||||||
|
if [[ "$user_input" == $'\x1b' ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
mode="exec"
|
||||||
|
case "$user_input" in
|
||||||
|
1) cmd="lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" ;;
|
||||||
|
2) cmd="ls -lh /dev/disk/by-id/" ;;
|
||||||
|
3) cmd="qm list && pct list" ;;
|
||||||
|
4)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
|
||||||
|
read -r vmid
|
||||||
|
cmd="qm config $vmid | grep -E '^(sata|scsi|virtio|ide|hostpci|boot:)'"
|
||||||
|
;;
|
||||||
|
5) cmd="pvesm status -content images" ;;
|
||||||
|
6)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter image full path: ')${CL}"; read -r image_path
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target storage: ')${CL}"; read -r storage
|
||||||
|
cmd="qm importdisk $vmid $image_path $storage"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter interface (sata/scsi/virtio/ide): ')${CL}"; read -r iface
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter slot number (e.g. 0): ')${CL}"; read -r slot
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter imported disk reference (e.g. local-lvm:vm-100-disk-0): ')${CL}"; read -r imported_disk
|
||||||
|
cmd="qm set $vmid --${iface}${slot} $imported_disk"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter boot target (e.g. scsi0, sata0, ide0): ')${CL}"; read -r boot_target
|
||||||
|
cmd="qm set $vmid --boot order=$boot_target"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
9) cmd="lspci -nn | grep -Ei 'SATA|RAID|Non-Volatile|NVMe'" ;;
|
||||||
|
10)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:04:00.0): ')${CL}"
|
||||||
|
read -r bdf
|
||||||
|
cmd="find /sys/kernel/iommu_groups -type l | grep $bdf"
|
||||||
|
;;
|
||||||
|
11)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot number (e.g. 0): ')${CL}"; read -r slot
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:04:00.0): ')${CL}"; read -r bdf
|
||||||
|
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
12)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
|
||||||
|
read -r ctid
|
||||||
|
cmd="pct config $ctid | grep '^mp'"
|
||||||
|
;;
|
||||||
|
13)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"; read -r ctid
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter mp slot number (e.g. 0): ')${CL}"; read -r mpslot
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter disk or partition path (prefer /dev/disk/by-id/...): ')${CL}"; read -r disk_part
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter mount point in CT (e.g. /mnt/data): ')${CL}"; read -r mount_point
|
||||||
|
cmd="pct set $ctid -mp${mpslot} ${disk_part},mp=${mount_point},backup=0,ro=0"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
14)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target disk (e.g. /dev/sdX): ')${CL}"
|
||||||
|
read -r disk
|
||||||
|
cmd="wipefs -a -f $disk && sgdisk --zap-all $disk"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
15)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target disk (e.g. /dev/sdX): ')${CL}"
|
||||||
|
read -r disk
|
||||||
|
cmd="parted -s -f $disk mklabel gpt mkpart primary 1MiB 100%"
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
16)
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter partition path (e.g. /dev/sdX1): ')${CL}"; read -r part
|
||||||
|
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter filesystem (ext4/xfs/btrfs): ')${CL}"; read -r fs
|
||||||
|
case "$fs" in
|
||||||
|
ext4) cmd="mkfs.ext4 -F $part" ;;
|
||||||
|
xfs) cmd="mkfs.xfs -f $part" ;;
|
||||||
|
btrfs) cmd="mkfs.btrfs -f $part" ;;
|
||||||
|
*) cmd="mkfs.ext4 -F $part" ;;
|
||||||
|
esac
|
||||||
|
mode="template"
|
||||||
|
;;
|
||||||
|
17) cmd="pvesm status && zpool status" ;;
|
||||||
|
0) break ;;
|
||||||
|
*)
|
||||||
|
if [[ -n "$user_input" ]]; then
|
||||||
|
cmd="$user_input"
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$mode" == "template" ]]; then
|
||||||
|
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
|
||||||
|
echo "$cmd"
|
||||||
|
echo
|
||||||
|
msg_success "$(translate 'Press ENTER to continue...')"
|
||||||
|
read -r tmp
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}> $cmd${NC}\n"
|
||||||
|
bash -c "$cmd"
|
||||||
|
echo
|
||||||
|
msg_success "$(translate 'Press ENTER to continue...')"
|
||||||
|
read -r tmp
|
||||||
|
done
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,24 +6,14 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
# Version : 1.1
|
# Version : 1.3
|
||||||
# Last Updated: 29/05/2025
|
# Last Updated: 12/04/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# This script automates the process of importing disk images into Proxmox VE virtual machines (VMs),
|
# Imports disk images (.img, .qcow2, .vmdk, .raw) into Proxmox VE VMs.
|
||||||
# making it easy to attach pre-existing disk files without manual configuration.
|
# Supports the default system ISO directory and custom paths.
|
||||||
#
|
# All user decisions are collected in Phase 1 (dialogs) before
|
||||||
# Before running the script, ensure that disk images are available in /var/lib/vz/template/images/.
|
# any operation is executed in Phase 2 (terminal output).
|
||||||
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk, .raw) and lists the available files.
|
|
||||||
#
|
|
||||||
# Using an interactive menu, you can:
|
|
||||||
# - Select a VM to attach the imported disk.
|
|
||||||
# - Choose one or multiple disk images for import.
|
|
||||||
# - Pick a storage volume in Proxmox for disk placement.
|
|
||||||
# - Assign a suitable interface (SATA, SCSI, VirtIO, or IDE).
|
|
||||||
# - Enable optional settings like SSD emulation or bootable disk configuration.
|
|
||||||
#
|
|
||||||
# Once completed, the script ensures the selected images are correctly attached and ready to use.
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
@@ -31,256 +21,340 @@ LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
|||||||
BASE_DIR="/usr/local/share/proxmenux"
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
VENV_PATH="/opt/googletrans-env"
|
VENV_PATH="/opt/googletrans-env"
|
||||||
|
BACKTITLE="ProxMenux"
|
||||||
|
UI_MENU_H=20
|
||||||
|
UI_MENU_W=84
|
||||||
|
UI_MENU_LIST_H=10
|
||||||
|
UI_SHORT_MENU_H=16
|
||||||
|
UI_SHORT_MENU_W=72
|
||||||
|
UI_SHORT_MENU_LIST_H=6
|
||||||
|
UI_MSG_H=10
|
||||||
|
UI_MSG_W=72
|
||||||
|
UI_YESNO_H=10
|
||||||
|
UI_YESNO_W=72
|
||||||
|
UI_RESULT_H=14
|
||||||
|
UI_RESULT_W=86
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
# Configuration ============================================
|
# Configuration ============================================
|
||||||
|
|
||||||
|
|
||||||
|
_get_default_images_dir() {
|
||||||
detect_image_dir() {
|
for dir in /var/lib/vz/template/iso /var/lib/vz/template/images; do
|
||||||
for store in $(pvesm status -content images | awk 'NR>1 {print $1}'); do
|
[[ -d "$dir" ]] && echo "$dir" && return 0
|
||||||
|
done
|
||||||
|
local store path
|
||||||
|
for store in $(pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}'); do
|
||||||
path=$(pvesm path "${store}:template" 2>/dev/null)
|
path=$(pvesm path "${store}:template" 2>/dev/null)
|
||||||
if [[ -d "$path" ]]; then
|
[[ -d "$path" ]] && echo "$path" && return 0
|
||||||
for ext in raw img qcow2 vmdk; do
|
|
||||||
if compgen -G "$path/*.$ext" > /dev/null; then
|
|
||||||
echo "$path"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
for sub in images iso; do
|
|
||||||
dir="$path/$sub"
|
|
||||||
if [[ -d "$dir" ]]; then
|
|
||||||
for ext in raw img qcow2 vmdk; do
|
|
||||||
if compgen -G "$dir/*.$ext" > /dev/null; then
|
|
||||||
echo "$dir"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
for fallback in /var/lib/vz/template/images /var/lib/vz/template/iso; do
|
echo "/var/lib/vz/template/iso"
|
||||||
if [[ -d "$fallback" ]]; then
|
|
||||||
for ext in raw img qcow2 vmdk; do
|
|
||||||
if compgen -G "$fallback/*.$ext" > /dev/null; then
|
|
||||||
echo "$fallback"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
IMAGES_DIR=$(detect_image_dir)
|
# ==========================================================
|
||||||
if [[ -z "$IMAGES_DIR" ]]; then
|
# PHASE 1 — SELECTION
|
||||||
dialog --title "$(translate 'No Images Found')" \
|
# All dialogs run here. No execution, no show_proxmenux_logo.
|
||||||
--msgbox "$(translate 'Could not find any directory containing disk images')\n\n$(translate 'Make sure there is at least one file with extension .img, .qcow2, .vmdk or .raw')" 15 60
|
# ==========================================================
|
||||||
|
|
||||||
|
# ── Step 1: Select VM ─────────────────────────────────────
|
||||||
|
VM_OPTIONS=()
|
||||||
|
while read -r vmid vmname _rest; do
|
||||||
|
VM_OPTIONS+=("$vmid" "${vmname:-VM-$vmid}")
|
||||||
|
done < <(qm list 2>/dev/null | awk 'NR>1')
|
||||||
|
stop_spinner
|
||||||
|
|
||||||
|
if [[ ${#VM_OPTIONS[@]} -eq 0 ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'No VMs Found')" \
|
||||||
|
--msgbox "\n$(translate 'No VMs available in the system.')" \
|
||||||
|
$UI_MSG_H $UI_MSG_W
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk|raw)$")
|
VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||||
if [ -z "$IMAGES" ]; then
|
--title "$(translate 'Select VM')" \
|
||||||
dialog --title "$(translate 'No Disk Images Found')" \
|
--menu "$(translate 'Select the VM where you want to import the disk image:')" \
|
||||||
--msgbox "$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" 15 60
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
exit 1
|
"${VM_OPTIONS[@]}" \
|
||||||
fi
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$VMID" ]] && exit 0
|
||||||
|
|
||||||
# 1. Select VM
|
|
||||||
msg_info "$(translate 'Getting VM list')"
|
|
||||||
VM_LIST=$(qm list | awk 'NR>1 {print $1" "$2}')
|
|
||||||
if [ -z "$VM_LIST" ]; then
|
|
||||||
msg_error "$(translate 'No VMs available in the system')"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
msg_ok "$(translate 'VM list obtained')"
|
|
||||||
|
|
||||||
VMID=$(whiptail --title "$(translate 'Select VM')" --menu "$(translate 'Select the VM where you want to import the disk image:')" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
if [ -z "$VMID" ]; then
|
|
||||||
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 2. Select storage volume
|
|
||||||
msg_info "$(translate 'Getting storage volumes')"
|
|
||||||
STORAGE_LIST=$(pvesm status -content images | awk 'NR>1 {print $1}')
|
|
||||||
if [ -z "$STORAGE_LIST" ]; then
|
|
||||||
msg_error "$(translate 'No storage volumes available')"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
msg_ok "$(translate 'Storage volumes obtained')"
|
|
||||||
|
|
||||||
|
|
||||||
|
# ── Step 2: Select storage ────────────────────────────────
|
||||||
STORAGE_OPTIONS=()
|
STORAGE_OPTIONS=()
|
||||||
while read -r storage; do
|
while read -r storage type _rest; do
|
||||||
STORAGE_OPTIONS+=("$storage" "")
|
STORAGE_OPTIONS+=("$storage" "$type")
|
||||||
done <<< "$STORAGE_LIST"
|
done < <(pvesm status -content images 2>/dev/null | awk 'NR>1')
|
||||||
|
stop_spinner
|
||||||
|
|
||||||
STORAGE=$(whiptail --title "$(translate 'Select Storage')" --menu "$(translate 'Select the storage volume for disk import:')" 15 60 8 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
if [[ ${#STORAGE_OPTIONS[@]} -eq 0 ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'No Storage Found')" \
|
||||||
|
--msgbox "\n$(translate 'No storage volumes available for disk images.')" \
|
||||||
|
$UI_MSG_H $UI_MSG_W
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$STORAGE" ]; then
|
if [[ ${#STORAGE_OPTIONS[@]} -eq 2 ]]; then
|
||||||
|
# Only one storage available — auto-select it
|
||||||
exit 1
|
STORAGE="${STORAGE_OPTIONS[0]}"
|
||||||
|
else
|
||||||
|
STORAGE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Select Storage')" \
|
||||||
|
--menu "$(translate 'Select the storage volume for disk import:')" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"${STORAGE_OPTIONS[@]}" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$STORAGE" ]] && exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ── Step 3: Select image source directory ────────────────
|
||||||
|
ISO_DIR="/var/lib/vz/template/iso"
|
||||||
|
|
||||||
# 3. Select disk images
|
DIR_CHOICE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
msg_info "$(translate 'Scanning disk images')"
|
--title "$(translate 'Image Source Directory')" \
|
||||||
if [ -z "$IMAGES" ]; then
|
--menu "$(translate 'Select the directory containing disk images:')" \
|
||||||
msg_warn "$(translate 'No compatible disk images found in') $IMAGES_DIR"
|
$UI_SHORT_MENU_H $UI_MENU_W $UI_SHORT_MENU_LIST_H \
|
||||||
exit 0
|
"$ISO_DIR" "$(translate 'Default ISO directory')" \
|
||||||
|
"custom" "$(translate 'Custom path...')" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$DIR_CHOICE" ]] && exit 0
|
||||||
|
|
||||||
|
if [[ "$DIR_CHOICE" == "custom" ]]; then
|
||||||
|
IMAGES_DIR=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Custom Directory')" \
|
||||||
|
--inputbox "\n$(translate 'Enter the full path to the directory containing disk images:')\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" \
|
||||||
|
10 $UI_RESULT_W "" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$IMAGES_DIR" ]] && exit 0
|
||||||
|
else
|
||||||
|
IMAGES_DIR="$ISO_DIR"
|
||||||
fi
|
fi
|
||||||
msg_ok "$(translate 'Disk images found')"
|
|
||||||
|
|
||||||
|
if [[ ! -d "$IMAGES_DIR" ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Directory Not Found')" \
|
||||||
|
--msgbox "\n$(translate 'The specified directory does not exist:')\n\n$IMAGES_DIR" \
|
||||||
|
$UI_MSG_H $UI_MSG_W
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGES=$(find "$IMAGES_DIR" -maxdepth 1 -type f \
|
||||||
|
\( -name "*.img" -o -name "*.qcow2" -o -name "*.vmdk" -o -name "*.raw" \) \
|
||||||
|
-printf '%f\n' 2>/dev/null | sort)
|
||||||
|
|
||||||
|
if [[ -z "$IMAGES" ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'No Disk Images Found')" \
|
||||||
|
--msgbox "\n$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" \
|
||||||
|
$UI_RESULT_H $UI_RESULT_W
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ── Step 4: Select images ─────────────────────────────────
|
||||||
IMAGE_OPTIONS=()
|
IMAGE_OPTIONS=()
|
||||||
while read -r img; do
|
while IFS= read -r img; do
|
||||||
IMAGE_OPTIONS+=("$img" "" "OFF")
|
IMAGE_OPTIONS+=("$img" "" "OFF")
|
||||||
done <<< "$IMAGES"
|
done <<< "$IMAGES"
|
||||||
|
|
||||||
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" --checklist "$(translate 'Select the disk images to import:')" 20 60 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
SELECTED_IMAGES_STR=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Select Disk Images')" \
|
||||||
|
--checklist "$(translate 'Select one or more disk images to import:')" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"${IMAGE_OPTIONS[@]}" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$SELECTED_IMAGES_STR" ]] && exit 0
|
||||||
|
|
||||||
if [ -z "$SELECTED_IMAGES" ]; then
|
eval "declare -a SELECTED_ARRAY=($SELECTED_IMAGES_STR)"
|
||||||
|
|
||||||
exit 1
|
|
||||||
|
# ── Step 5: Per-image options ─────────────────────────────
|
||||||
|
declare -a IMG_NAMES=()
|
||||||
|
declare -a IMG_INTERFACES=()
|
||||||
|
declare -a IMG_SSD_OPTIONS=()
|
||||||
|
declare -a IMG_BOOTABLE=()
|
||||||
|
|
||||||
|
for IMAGE in "${SELECTED_ARRAY[@]}"; do
|
||||||
|
IMAGE="${IMAGE//\"/}"
|
||||||
|
|
||||||
|
INTERFACE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Interface Type') — $IMAGE" \
|
||||||
|
--default-item "scsi" \
|
||||||
|
--menu "$(translate 'Select the interface type for:') $IMAGE" \
|
||||||
|
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
|
||||||
|
"scsi" "SCSI $(translate '(recommended)')" \
|
||||||
|
"virtio" "VirtIO" \
|
||||||
|
"sata" "SATA" \
|
||||||
|
"ide" "IDE" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$INTERFACE" ]] && continue
|
||||||
|
|
||||||
|
SSD_OPTION=""
|
||||||
|
if [[ "$INTERFACE" != "virtio" ]]; then
|
||||||
|
if dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'SSD Emulation') — $IMAGE" \
|
||||||
|
--yesno "\n$(translate 'Enable SSD emulation for this disk?')" \
|
||||||
|
$UI_YESNO_H $UI_YESNO_W; then
|
||||||
|
SSD_OPTION=",ssd=1"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
BOOTABLE="no"
|
||||||
|
if dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Boot Disk') — $IMAGE" \
|
||||||
|
--yesno "\n$(translate 'Set this disk as the primary boot disk?')" \
|
||||||
|
$UI_YESNO_H $UI_YESNO_W; then
|
||||||
|
BOOTABLE="yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMG_NAMES+=("$IMAGE")
|
||||||
|
IMG_INTERFACES+=("$INTERFACE")
|
||||||
|
IMG_SSD_OPTIONS+=("$SSD_OPTION")
|
||||||
|
IMG_BOOTABLE+=("$BOOTABLE")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#IMG_NAMES[@]} -eq 0 ]]; then
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# PHASE 2 — EXECUTION
|
||||||
|
# show_proxmenux_logo appears here exactly once.
|
||||||
|
# No dialogs from this point on.
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
# 4. Import each selected image
|
show_proxmenux_logo
|
||||||
for IMAGE in $SELECTED_IMAGES; do
|
msg_title "$(translate 'Import Disk Image to VM')"
|
||||||
|
|
||||||
|
VM_NAME=$(qm config "$VMID" 2>/dev/null | awk '/^name:/ {print $2}')
|
||||||
|
msg_ok "$(translate 'VM:') ${VM_NAME:-VM-$VMID} (${VMID})"
|
||||||
|
msg_ok "$(translate 'Storage:') $STORAGE"
|
||||||
|
msg_ok "$(translate 'Image directory:') $IMAGES_DIR"
|
||||||
|
msg_ok "$(translate 'Images to import:') ${#IMG_NAMES[@]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
IMAGE=$(echo "$IMAGE" | tr -d '"')
|
PROCESSED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for i in "${!IMG_NAMES[@]}"; do
|
||||||
|
IMAGE="${IMG_NAMES[$i]}"
|
||||||
|
INTERFACE="${IMG_INTERFACES[$i]}"
|
||||||
|
SSD_OPTION="${IMG_SSD_OPTIONS[$i]}"
|
||||||
|
BOOTABLE="${IMG_BOOTABLE[$i]}"
|
||||||
|
FULL_PATH="$IMAGES_DIR/$IMAGE"
|
||||||
|
|
||||||
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
|
if [[ ! -f "$FULL_PATH" ]]; then
|
||||||
"sata" "SATA" \
|
msg_error "$(translate 'Image file not found:') $FULL_PATH"
|
||||||
"scsi" "SCSI" \
|
FAILED=$((FAILED + 1))
|
||||||
"virtio" "VirtIO" \
|
continue
|
||||||
"ide" "IDE" 3>&1 1>&2 2>&3)
|
fi
|
||||||
|
|
||||||
if [ -z "$INTERFACE" ]; then
|
# Snapshot of unused entries before import for reliable detection
|
||||||
msg_error "$(translate 'No interface type selected for') $IMAGE"
|
BEFORE_UNUSED=$(qm config "$VMID" 2>/dev/null | grep -E '^unused[0-9]+:' || true)
|
||||||
continue
|
|
||||||
|
TEMP_STATUS_FILE=$(mktemp)
|
||||||
|
TEMP_DISK_FILE=$(mktemp)
|
||||||
|
|
||||||
|
msg_info "$(translate 'Importing') $IMAGE..."
|
||||||
|
|
||||||
|
(
|
||||||
|
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1
|
||||||
|
echo $? > "$TEMP_STATUS_FILE"
|
||||||
|
) | while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ [0-9]+\.[0-9]+% ]]; then
|
||||||
|
echo -ne "\r${TAB}${BL}$(translate 'Importing') ${IMAGE}${CL} ${BASH_REMATCH[0]} "
|
||||||
|
fi
|
||||||
|
if echo "$line" | grep -qiF "successfully imported disk"; then
|
||||||
|
echo "$line" | sed -n "s/.*successfully imported disk as '\\([^']*\\)'.*/\\1/p" > "$TEMP_DISK_FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo -ne "\n"
|
||||||
|
|
||||||
|
IMPORT_STATUS=$(cat "$TEMP_STATUS_FILE" 2>/dev/null)
|
||||||
|
rm -f "$TEMP_STATUS_FILE"
|
||||||
|
[[ -z "$IMPORT_STATUS" ]] && IMPORT_STATUS=1
|
||||||
|
|
||||||
|
if [[ "$IMPORT_STATUS" -ne 0 ]]; then
|
||||||
|
msg_error "$(translate 'Failed to import') $IMAGE"
|
||||||
|
rm -f "$TEMP_DISK_FILE"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_ok "$(translate 'Image imported:') $IMAGE"
|
||||||
|
|
||||||
|
# Primary: parse disk name from qm importdisk output
|
||||||
|
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE" 2>/dev/null | xargs)
|
||||||
|
rm -f "$TEMP_DISK_FILE"
|
||||||
|
|
||||||
|
# Fallback: compare unused entries before/after import
|
||||||
|
if [[ -z "$IMPORTED_DISK" ]]; then
|
||||||
|
AFTER_UNUSED=$(qm config "$VMID" 2>/dev/null | grep -E '^unused[0-9]+:' || true)
|
||||||
|
NEW_LINE=$(comm -13 \
|
||||||
|
<(echo "$BEFORE_UNUSED" | sort) \
|
||||||
|
<(echo "$AFTER_UNUSED" | sort) | head -1)
|
||||||
|
if [[ -n "$NEW_LINE" ]]; then
|
||||||
|
IMPORTED_DISK=$(echo "$NEW_LINE" | cut -d':' -f2- | xargs)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$IMPORTED_DISK" ]]; then
|
||||||
|
msg_error "$(translate 'Could not identify the imported disk in VM config')"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the unusedN key that holds this disk (needed to clean it up after assignment)
|
||||||
|
IMPORTED_ID=$(qm config "$VMID" 2>/dev/null | grep -F "$IMPORTED_DISK" | cut -d':' -f1 | head -1)
|
||||||
|
|
||||||
|
# Find next available slot for the chosen interface
|
||||||
|
LAST_SLOT=$(qm config "$VMID" 2>/dev/null | grep -oE "^${INTERFACE}[0-9]+" | grep -oE '[0-9]+' | sort -n | tail -1)
|
||||||
|
if [[ -z "$LAST_SLOT" ]]; then
|
||||||
|
NEXT_SLOT=0
|
||||||
|
else
|
||||||
|
NEXT_SLOT=$((LAST_SLOT + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_info "$(translate 'Configuring disk as') ${INTERFACE}${NEXT_SLOT}..."
|
||||||
|
if qm set "$VMID" "--${INTERFACE}${NEXT_SLOT}" "${IMPORTED_DISK}${SSD_OPTION}" >/dev/null 2>&1; then
|
||||||
|
msg_ok "$(translate 'Disk configured as') ${INTERFACE}${NEXT_SLOT}${SSD_OPTION:+ (SSD)}"
|
||||||
|
|
||||||
|
# Remove the unusedN entry now that the disk is properly assigned
|
||||||
|
if [[ -n "$IMPORTED_ID" ]]; then
|
||||||
|
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FULL_PATH="$IMAGES_DIR/$IMAGE"
|
if [[ "$BOOTABLE" == "yes" ]]; then
|
||||||
|
msg_info "$(translate 'Setting boot order...')"
|
||||||
|
if qm set "$VMID" --boot "order=${INTERFACE}${NEXT_SLOT}" >/dev/null 2>&1; then
|
||||||
msg_info "$(translate 'Importing image:')"
|
msg_ok "$(translate 'Boot order set to') ${INTERFACE}${NEXT_SLOT}"
|
||||||
|
else
|
||||||
|
msg_error "$(translate 'Could not set boot order for') ${INTERFACE}${NEXT_SLOT}"
|
||||||
TEMP_DISK_FILE=$(mktemp)
|
fi
|
||||||
|
|
||||||
|
|
||||||
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
|
|
||||||
if [[ "$line" =~ transferred ]]; then
|
|
||||||
|
|
||||||
PERCENT=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
|
|
||||||
|
|
||||||
echo -ne "\r${TAB}${BL}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
|
|
||||||
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
|
|
||||||
|
|
||||||
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo -ne "\n"
|
|
||||||
|
|
||||||
IMPORT_STATUS=${PIPESTATUS[0]}
|
|
||||||
|
|
||||||
if [ $IMPORT_STATUS -eq 0 ]; then
|
|
||||||
msg_ok "$(translate 'Image imported successfully')"
|
|
||||||
|
|
||||||
|
|
||||||
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
|
|
||||||
rm -f "$TEMP_DISK_FILE"
|
|
||||||
|
|
||||||
|
|
||||||
if [ -z "$IMPORTED_DISK" ]; then
|
|
||||||
|
|
||||||
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
|
|
||||||
|
|
||||||
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
|
|
||||||
|
|
||||||
UNUSED_LINE=$(qm config "$VMID" | grep -E '^unused[0-9]+:')
|
|
||||||
IMPORTED_ID=$(echo "$UNUSED_LINE" | cut -d: -f1)
|
|
||||||
IMPORTED_DISK=$(echo "$UNUSED_LINE" | cut -d: -f2- | xargs)
|
|
||||||
else
|
|
||||||
|
|
||||||
IMPORTED_DISK=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f2- | xargs)
|
|
||||||
IMPORTED_ID=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f1)
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$IMPORTED_DISK" ]; then
|
|
||||||
|
|
||||||
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
|
|
||||||
if [ -z "$EXISTING_DISKS" ]; then
|
|
||||||
NEXT_SLOT=0
|
|
||||||
else
|
|
||||||
LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//")
|
|
||||||
NEXT_SLOT=$((LAST_SLOT + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$INTERFACE" != "virtio" ]; then
|
|
||||||
if (whiptail --title "$(translate 'SSD Emulation')" --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60); then
|
|
||||||
SSD_OPTION=",ssd=1"
|
|
||||||
else
|
|
||||||
SSD_OPTION=""
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
SSD_OPTION=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_info "$(translate 'Configuring disk')"
|
|
||||||
|
|
||||||
|
|
||||||
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
|
|
||||||
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
|
|
||||||
|
|
||||||
|
|
||||||
if [[ -n "$IMPORTED_ID" ]]; then
|
|
||||||
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if (whiptail --title "$(translate 'Make Bootable')" --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60); then
|
|
||||||
msg_info "$(translate 'Configuring disk as bootable')"
|
|
||||||
|
|
||||||
if qm set "$VMID" --boot c --bootdisk ${INTERFACE}${NEXT_SLOT} &>/dev/null; then
|
|
||||||
msg_ok "$(translate 'Disk configured as bootable')"
|
|
||||||
else
|
|
||||||
msg_error "$(translate 'Could not configure the disk as bootable')"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
|
|
||||||
echo "DEBUG: Tried to configure: --${INTERFACE}${NEXT_SLOT} \"$IMPORTED_DISK${SSD_OPTION}\""
|
|
||||||
echo "DEBUG: VM config after import:"
|
|
||||||
qm config "$VMID" | grep -E "(unused|${INTERFACE})"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
msg_error "$(translate 'Could not find the imported disk')"
|
|
||||||
echo "DEBUG: VM config after import:"
|
|
||||||
qm config "$VMID"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
msg_error "$(translate 'Could not import') $IMAGE"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
PROCESSED=$((PROCESSED + 1))
|
||||||
|
else
|
||||||
|
msg_error "$(translate 'Could not assign disk') ${INTERFACE}${NEXT_SLOT} $(translate 'to VM') $VMID"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [[ $FAILED -eq 0 ]]; then
|
||||||
|
msg_ok "$(translate 'All images imported and configured successfully')"
|
||||||
|
elif [[ $PROCESSED -gt 0 ]]; then
|
||||||
|
msg_warn "$(translate 'Completed with errors —') $(translate 'imported:') $PROCESSED, $(translate 'failed:') $FAILED"
|
||||||
|
else
|
||||||
|
msg_error "$(translate 'All imports failed')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_success "$(translate 'Press Enter to return to menu...')"
|
||||||
msg_ok "$(translate 'All selected images have been processed')"
|
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
|
||||||
read -r
|
read -r
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# ProxMenux - Mount independent disk on Proxmox host
|
|
||||||
# ==========================================================
|
|
||||||
# Author : MacRimi
|
|
||||||
# Copyright : (c) 2024 MacRimi
|
|
||||||
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
|
||||||
# Version : 1.0
|
|
||||||
# Last Updated: 08/04/2025
|
|
||||||
# ==========================================================
|
|
||||||
# Description:
|
|
||||||
# This script detects unassigned physical disks and allows
|
|
||||||
# the user to mount one of them on the host Proxmox system.
|
|
||||||
# - Detects unmounted and unassigned disks.
|
|
||||||
# - Filters out ZFS, LVM, RAID and system disks.
|
|
||||||
# - Allows selecting a disk.
|
|
||||||
# - Prepares partition and filesystem if needed.
|
|
||||||
# - Mounts the disk in the host at a defined mount point.
|
|
||||||
# ==========================================================
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# ==========================================================
|
|
||||||
|
|
||||||
get_disk_info() {
|
|
||||||
local disk=$1
|
|
||||||
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
|
|
||||||
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
|
|
||||||
echo "$MODEL" "$SIZE"
|
|
||||||
}
|
|
||||||
|
|
||||||
msg_info "$(translate "Detecting available disks...")"
|
|
||||||
|
|
||||||
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
|
|
||||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
|
||||||
|
|
||||||
ZFS_DISKS=""
|
|
||||||
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
|
||||||
|
|
||||||
for entry in $ZFS_RAW; do
|
|
||||||
path=""
|
|
||||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
|
||||||
if [ -e "/dev/disk/by-id/$entry" ]; then
|
|
||||||
path=$(readlink -f "/dev/disk/by-id/$entry")
|
|
||||||
fi
|
|
||||||
elif [[ "$entry" == /dev/* ]]; then
|
|
||||||
path="$entry"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$path" ]; then
|
|
||||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
|
||||||
if [ -n "$base_disk" ]; then
|
|
||||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
|
||||||
|
|
||||||
is_disk_in_use() {
|
|
||||||
local disk="$1"
|
|
||||||
|
|
||||||
while read -r part fstype; do
|
|
||||||
case "$fstype" in
|
|
||||||
zfs_member|linux_raid_member)
|
|
||||||
return 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
|
|
||||||
|
|
||||||
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
FREE_DISKS=()
|
|
||||||
|
|
||||||
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
|
|
||||||
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
|
|
||||||
|
|
||||||
while read -r DISK; do
|
|
||||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
|
||||||
|
|
||||||
INFO=($(get_disk_info "$DISK"))
|
|
||||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
|
||||||
SIZE="${INFO[-1]}"
|
|
||||||
LABEL=""
|
|
||||||
SHOW_DISK=true
|
|
||||||
|
|
||||||
IS_MOUNTED=false
|
|
||||||
IS_RAID=false
|
|
||||||
IS_ZFS=false
|
|
||||||
IS_LVM=false
|
|
||||||
|
|
||||||
while read -r part fstype; do
|
|
||||||
[[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
|
|
||||||
[[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
|
|
||||||
[[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
|
|
||||||
if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
|
|
||||||
IS_MOUNTED=true
|
|
||||||
fi
|
|
||||||
done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)
|
|
||||||
|
|
||||||
REAL_PATH=$(readlink -f "$DISK")
|
|
||||||
if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
|
|
||||||
IS_MOUNTED=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
USED_BY=""
|
|
||||||
REAL_PATH=$(readlink -f "$DISK")
|
|
||||||
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
|
|
||||||
|
|
||||||
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
|
|
||||||
USED_BY="⚠ $(translate "In use")"
|
|
||||||
else
|
|
||||||
for SYMLINK in /dev/disk/by-id/*; do
|
|
||||||
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
|
|
||||||
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
|
|
||||||
USED_BY="⚠ $(translate "In use")"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
|
||||||
if grep -q "active raid" /proc/mdstat; then
|
|
||||||
SHOW_DISK=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $IS_ZFS; then
|
|
||||||
SHOW_DISK=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $IS_MOUNTED; then
|
|
||||||
SHOW_DISK=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $SHOW_DISK; then
|
|
||||||
[[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
|
|
||||||
[[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
|
|
||||||
[[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
|
|
||||||
[[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"
|
|
||||||
|
|
||||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
|
||||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
|
||||||
fi
|
|
||||||
|
|
||||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
|
||||||
|
|
||||||
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
|
||||||
cleanup
|
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 50
|
|
||||||
clear
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_ok "$(translate "Available disks detected.")"
|
|
||||||
|
|
||||||
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
|
|
||||||
TOTAL_WIDTH=$((MAX_WIDTH + 20))
|
|
||||||
TOTAL_WIDTH=$((TOTAL_WIDTH < 50 ? 50 : TOTAL_WIDTH))
|
|
||||||
|
|
||||||
SELECTED=$(whiptail --title "$(translate "Select Disk")" --radiolist \
|
|
||||||
"$(translate "Select the disk you want to mount on the host:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
if [ -z "$SELECTED" ]; then
|
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 10 50
|
|
||||||
clear
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
################################################################
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
|
||||||
|
|
||||||
SKIP_FORMAT=false
|
|
||||||
DEFAULT_MOUNT="/mnt/data_shared"
|
|
||||||
|
|
||||||
if [ -n "$PARTITION" ]; then
|
|
||||||
PARTITION="/dev/$PARTITION"
|
|
||||||
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
|
||||||
|
|
||||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
|
||||||
SKIP_FORMAT=true
|
|
||||||
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
|
|
||||||
else
|
|
||||||
whiptail --title "$(translate "Unsupported Filesystem")" --yesno "$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\\nDo you want to format it?")" 10 70
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
|
|
||||||
|
|
||||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
|
||||||
SKIP_FORMAT=true
|
|
||||||
PARTITION="$SELECTED"
|
|
||||||
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
|
|
||||||
else
|
|
||||||
whiptail --title "$(translate "No Valid Partitions")" --yesno "$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "$(translate "Creating partition table and partition...")"
|
|
||||||
parted -s "$SELECTED" mklabel gpt
|
|
||||||
parted -s "$SELECTED" mkpart primary 0% 100%
|
|
||||||
sleep 2
|
|
||||||
partprobe "$SELECTED"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
|
||||||
if [ -n "$PARTITION" ]; then
|
|
||||||
PARTITION="/dev/$PARTITION"
|
|
||||||
else
|
|
||||||
whiptail --title "$(translate "Partition Error")" --msgbox "$(translate "Failed to create partition on disk") $SELECTED." 8 70
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SKIP_FORMAT" != true ]; then
|
|
||||||
FORMAT_TYPE=$(whiptail --title "$(translate "Select Format Type")" --menu "$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
|
|
||||||
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
|
|
||||||
"xfs" "XFS" \
|
|
||||||
"btrfs" "Btrfs" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
if [ -z "$FORMAT_TYPE" ]; then
|
|
||||||
whiptail --title "$(translate "Format Cancelled")" --msgbox "$(translate "Format operation cancelled. The disk will not be added.")" 8 60
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
whiptail --title "$(translate "WARNING")" --yesno "$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\\n\\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\\n\\n$(translate "Are you sure you want to continue")" 15 70
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
|
|
||||||
case "$FORMAT_TYPE" in
|
|
||||||
"ext4") mkfs.ext4 -F "$PARTITION" ;;
|
|
||||||
"xfs") mkfs.xfs -f "$PARTITION" ;;
|
|
||||||
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
cleanup
|
|
||||||
whiptail --title "$(translate "Format Failed")" --msgbox "$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
|
|
||||||
partprobe "$SELECTED"
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
################################################################
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" \
|
|
||||||
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/data_shared):")" \
|
|
||||||
10 60 "$DEFAULT_MOUNT" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
if [ -z "$MOUNT_POINT" ]; then
|
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
|
|
||||||
|
|
||||||
mkdir -p "$MOUNT_POINT"
|
|
||||||
|
|
||||||
UUID=$(blkid -s UUID -o value "$PARTITION")
|
|
||||||
|
|
||||||
# Obtener sistema de archivos real
|
|
||||||
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
|
||||||
|
|
||||||
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
|
|
||||||
|
|
||||||
if grep -q "UUID=$UUID" /etc/fstab; then
|
|
||||||
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
|
|
||||||
msg_ok "$(translate "fstab entry updated for") $UUID"
|
|
||||||
else
|
|
||||||
echo "$FSTAB_ENTRY" >> /etc/fstab
|
|
||||||
msg_ok "$(translate "fstab entry added for") $UUID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
##################################################################
|
|
||||||
|
|
||||||
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
|
|
||||||
|
|
||||||
##################################################################
|
|
||||||
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
if ! getent group sharedfiles >/dev/null; then
|
|
||||||
groupadd sharedfiles
|
|
||||||
msg_ok "$(translate "Group 'sharedfiles' created")"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate "Group 'sharedfiles' already exists")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown root:sharedfiles "$MOUNT_POINT"
|
|
||||||
chmod 2775 "$MOUNT_POINT"
|
|
||||||
|
|
||||||
whiptail --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
|
|
||||||
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
|
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
|
||||||
read -r
|
|
||||||
else
|
|
||||||
whiptail --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
|
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
|
||||||
read -r
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# ProxMenux - Mount point from host into LXC container (CT)
|
|
||||||
# ==========================================================
|
|
||||||
# Author : MacRimi
|
|
||||||
# License : MIT
|
|
||||||
# Description : Mount a folder from /mnt on the host to a mount point in a CT
|
|
||||||
# ==========================================================
|
|
||||||
|
|
||||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
|
||||||
|
|
||||||
if [[ -f "$UTILS_FILE" ]]; then
|
|
||||||
source "$UTILS_FILE"
|
|
||||||
fi
|
|
||||||
load_language
|
|
||||||
initialize_cache
|
|
||||||
|
|
||||||
#######################################################
|
|
||||||
|
|
||||||
CT_LIST=($(pct list | awk 'NR>1 {print $1":"$3}'))
|
|
||||||
|
|
||||||
if [[ ${#CT_LIST[@]} -eq 0 ]]; then
|
|
||||||
whiptail --title "$(translate "No CTs")" --msgbox "$(translate "No containers found.")" 8 40
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
CT_OPTIONS=()
|
|
||||||
for entry in "${CT_LIST[@]}"; do
|
|
||||||
ID="${entry%%:*}"
|
|
||||||
NAME="${entry##*:}"
|
|
||||||
CT_OPTIONS+=("$ID" "$NAME")
|
|
||||||
done
|
|
||||||
|
|
||||||
CTID=$(whiptail --title "$(translate "Select CT")" --menu "$(translate "Select the container:")" 20 60 10 "${CT_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
|
||||||
[[ -z "$CTID" ]] && exit 0
|
|
||||||
|
|
||||||
CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
|
|
||||||
if [ "$CT_STATUS" != "running" ]; then
|
|
||||||
msg_info "$(translate "Starting CT") $CTID..."
|
|
||||||
pct start "$CTID"
|
|
||||||
sleep 2
|
|
||||||
if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then
|
|
||||||
msg_error "$(translate "Failed to start the CT.")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
msg_ok "$(translate "CT started successfully.")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#######################################################
|
|
||||||
|
|
||||||
select_origin_path() {
|
|
||||||
METHOD=$(whiptail --title "$(translate "Select Host Folder")" --menu "$(translate "How do you want to select the host folder to mount?")" 15 60 5 \
|
|
||||||
"auto" "$(translate "Select from /mnt")" \
|
|
||||||
"manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
case "$METHOD" in
|
|
||||||
auto)
|
|
||||||
HOST_DIRS=(/mnt/*)
|
|
||||||
OPTIONS=()
|
|
||||||
for dir in "${HOST_DIRS[@]}"; do
|
|
||||||
[[ -d "$dir" ]] && OPTIONS+=("$dir" "")
|
|
||||||
done
|
|
||||||
|
|
||||||
ORIGIN=$(whiptail --title "$(translate "Select Host Folder")" --menu "$(translate "Select the folder to mount:")" 20 60 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
|
||||||
[[ -z "$ORIGIN" ]] && return 1
|
|
||||||
;;
|
|
||||||
|
|
||||||
manual)
|
|
||||||
ORIGIN=$(whiptail --title "$(translate "Enter Path")" --inputbox "$(translate "Enter the full path to the host folder:")" 10 60 "/mnt/" 3>&1 1>&2 2>&3)
|
|
||||||
[[ -z "$ORIGIN" ]] && return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ ! -d "$ORIGIN" ]]; then
|
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "The selected path is not a valid directory:")\n$ORIGIN" 8 60
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Preparar permisos en el host para uso compartido
|
|
||||||
SHARE_GID=999
|
|
||||||
if ! getent group sharedfiles >/dev/null; then
|
|
||||||
groupadd -g "$SHARE_GID" sharedfiles
|
|
||||||
msg_ok "$(translate "Group 'sharedfiles' created in the host with GID $SHARE_GID")"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate "Group 'sharedfiles' already exists in the host")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown root:sharedfiles "$ORIGIN"
|
|
||||||
chmod 2775 "$ORIGIN"
|
|
||||||
setfacl -d -m g:sharedfiles:rwx "$ORIGIN"
|
|
||||||
setfacl -m g:sharedfiles:rwx "$ORIGIN"
|
|
||||||
|
|
||||||
msg_ok "$(translate "Host folder prepared with shared group and permissions.")"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
select_origin_path || exit 0
|
|
||||||
|
|
||||||
#######################################################
|
|
||||||
|
|
||||||
CT_NAME=$(pct config "$CTID" | awk -F: '/hostname/ {print $2}' | xargs)
|
|
||||||
DEFAULT_MOUNT_POINT="/mnt/host_share"
|
|
||||||
|
|
||||||
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point to CT")" \
|
|
||||||
--inputbox "$(translate "Enter the mount point inside the CT (e.g., /mnt/host_share):")" \
|
|
||||||
10 70 "$DEFAULT_MOUNT_POINT" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
if [[ -z "$MOUNT_POINT" ]]; then
|
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point specified.")" 8 60
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! pct exec "$CTID" -- test -d "$MOUNT_POINT"; then
|
|
||||||
if whiptail --yesno "$(translate "Directory does not exist in the CT.")\n\n$MOUNT_POINT\n\n$(translate "Do you want to create it?")" 12 70 --title "$(translate "Create Directory")"; then
|
|
||||||
pct exec "$CTID" -- mkdir -p "$MOUNT_POINT"
|
|
||||||
msg_ok "$(translate "Directory created inside CT:") $MOUNT_POINT"
|
|
||||||
else
|
|
||||||
msg_error "$(translate "Directory not created. Operation cancelled.")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
INDEX=0
|
|
||||||
while pct config "$CTID" | grep -q "mp${INDEX}:"; do
|
|
||||||
((INDEX++))
|
|
||||||
[[ $INDEX -ge 100 ]] && msg_error "Too many mount points." && exit 1
|
|
||||||
done
|
|
||||||
|
|
||||||
msg_info "$(translate "Mounting folder from host to CT...")"
|
|
||||||
RESULT=$(pct set "$CTID" -mp${INDEX} "$ORIGIN,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1)
|
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
msg_ok "$(translate "Successfully mounted:")\n$ORIGIN → $CT_NAME:$MOUNT_POINT"
|
|
||||||
else
|
|
||||||
msg_error "$(translate "Error mounting folder:")\n$RESULT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
|
||||||
read -r
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ==========================================================
|
|
||||||
# ProxMenux - Mount independent disk on Proxmox host
|
|
||||||
# ==========================================================
|
|
||||||
# Author : MacRimi
|
|
||||||
# Copyright : (c) 2024 MacRimi
|
|
||||||
# License : MIT
|
|
||||||
# Version : 1.3-dialog
|
|
||||||
# Last Updated: 13/12/2024
|
|
||||||
# ==========================================================
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
mount_disk_host_bk() {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
get_disk_info() {
|
|
||||||
local disk=$1
|
|
||||||
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
|
|
||||||
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
|
|
||||||
echo "$MODEL" "$SIZE"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
is_usb_disk() {
|
|
||||||
local disk=$1
|
|
||||||
local disk_name=$(basename "$disk")
|
|
||||||
|
|
||||||
|
|
||||||
if readlink -f "/sys/block/$disk_name/device" 2>/dev/null | grep -q "usb"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if udevadm info --query=property --name="$disk" 2>/dev/null | grep -q "ID_BUS=usb"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
is_system_disk() {
|
|
||||||
local disk=$1
|
|
||||||
local disk_name=$(basename "$disk")
|
|
||||||
|
|
||||||
|
|
||||||
local system_mounts=$(df -h | grep -E '^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/|/boot|/usr|/var|/home)$' | awk '{print $1}')
|
|
||||||
|
|
||||||
|
|
||||||
for mount_dev in $system_mounts; do
|
|
||||||
|
|
||||||
local mount_disk=""
|
|
||||||
if [[ "$mount_dev" =~ ^/dev/mapper/ ]]; then
|
|
||||||
|
|
||||||
local vg_name=$(lvs --noheadings -o vg_name "$mount_dev" 2>/dev/null | xargs)
|
|
||||||
if [[ -n "$vg_name" ]]; then
|
|
||||||
local pvs_list=$(pvs --noheadings -o pv_name -S vg_name="$vg_name" 2>/dev/null | xargs)
|
|
||||||
for pv in $pvs_list; do
|
|
||||||
if [[ -n "$pv" && -e "$pv" ]]; then
|
|
||||||
mount_disk=$(lsblk -no PKNAME "$pv" 2>/dev/null)
|
|
||||||
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
elif [[ "$mount_dev" =~ ^/dev/[hsv]d[a-z][0-9]* || "$mount_dev" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+ ]]; then
|
|
||||||
|
|
||||||
mount_disk=$(lsblk -no PKNAME "$mount_dev" 2>/dev/null)
|
|
||||||
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
local fs_type=$(lsblk -no FSTYPE "$disk" 2>/dev/null | head -1)
|
|
||||||
if [[ "$fs_type" == "btrfs" ]]; then
|
|
||||||
|
|
||||||
local temp_mount=$(mktemp -d)
|
|
||||||
if mount -o ro "$disk" "$temp_mount" 2>/dev/null; then
|
|
||||||
|
|
||||||
if btrfs subvolume list "$temp_mount" 2>/dev/null | grep -qE '(@|@home|@var|@boot|@root|root)'; then
|
|
||||||
umount "$temp_mount" 2>/dev/null
|
|
||||||
rmdir "$temp_mount" 2>/dev/null
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
umount "$temp_mount" 2>/dev/null
|
|
||||||
fi
|
|
||||||
rmdir "$temp_mount" 2>/dev/null
|
|
||||||
|
|
||||||
|
|
||||||
while read -r part; do
|
|
||||||
if [[ -n "$part" ]]; then
|
|
||||||
local part_fs=$(lsblk -no FSTYPE "/dev/$part" 2>/dev/null)
|
|
||||||
if [[ "$part_fs" == "btrfs" ]]; then
|
|
||||||
local mount_point=$(lsblk -no MOUNTPOINT "/dev/$part" 2>/dev/null)
|
|
||||||
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
local disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
|
|
||||||
local part_uuids=()
|
|
||||||
while read -r part; do
|
|
||||||
if [[ -n "$part" ]]; then
|
|
||||||
local uuid=$(blkid -s UUID -o value "/dev/$part" 2>/dev/null)
|
|
||||||
if [[ -n "$uuid" ]]; then
|
|
||||||
part_uuids+=("$uuid")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
|
|
||||||
|
|
||||||
|
|
||||||
for uuid in "${part_uuids[@]}" "$disk_uuid"; do
|
|
||||||
if [[ -n "$uuid" ]] && grep -q "UUID=$uuid" /etc/fstab; then
|
|
||||||
local mount_point=$(grep "UUID=$uuid" /etc/fstab | awk '{print $2}')
|
|
||||||
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
if grep -q "$disk" /etc/fstab; then
|
|
||||||
local mount_point=$(grep "$disk" /etc/fstab | awk '{print $2}')
|
|
||||||
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
local disk_count=$(lsblk -dn -e 7,11 -o PATH | wc -l)
|
|
||||||
if [[ "$disk_count" -eq 1 ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
msg_info "$(translate "Detecting available disks...")"
|
|
||||||
|
|
||||||
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
|
|
||||||
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
|
|
||||||
|
|
||||||
ZFS_DISKS=""
|
|
||||||
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
|
|
||||||
|
|
||||||
for entry in $ZFS_RAW; do
|
|
||||||
path=""
|
|
||||||
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
|
|
||||||
if [ -e "/dev/disk/by-id/$entry" ]; then
|
|
||||||
path=$(readlink -f "/dev/disk/by-id/$entry")
|
|
||||||
fi
|
|
||||||
elif [[ "$entry" == /dev/* ]]; then
|
|
||||||
path="$entry"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$path" ]; then
|
|
||||||
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
|
|
||||||
if [ -n "$base_disk" ]; then
|
|
||||||
ZFS_DISKS+="/dev/$base_disk"$'\n'
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
|
|
||||||
|
|
||||||
LVM_DEVICES=$(
|
|
||||||
pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') |
|
|
||||||
while read -r dev; do
|
|
||||||
[[ -n "$dev" && -e "$dev" ]] && readlink -f "$dev"
|
|
||||||
done | sort -u
|
|
||||||
)
|
|
||||||
|
|
||||||
FREE_DISKS=()
|
|
||||||
|
|
||||||
while read -r DISK; do
|
|
||||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
|
||||||
|
|
||||||
INFO=($(get_disk_info "$DISK"))
|
|
||||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
|
||||||
SIZE="${INFO[-1]}"
|
|
||||||
LABEL=""
|
|
||||||
SHOW_DISK=true
|
|
||||||
|
|
||||||
IS_MOUNTED=false
|
|
||||||
IS_RAID=false
|
|
||||||
IS_ZFS=false
|
|
||||||
IS_LVM=false
|
|
||||||
IS_SYSTEM=false
|
|
||||||
IS_USB=false
|
|
||||||
|
|
||||||
|
|
||||||
if is_system_disk "$DISK"; then
|
|
||||||
IS_SYSTEM=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if is_usb_disk "$DISK"; then
|
|
||||||
IS_USB=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
while read -r part fstype; do
|
|
||||||
[[ "$fstype" == "zfs_member" ]] && IS_ZFS=true
|
|
||||||
[[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true
|
|
||||||
[[ "$fstype" == "LVM2_member" ]] && IS_LVM=true
|
|
||||||
if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then
|
|
||||||
IS_MOUNTED=true
|
|
||||||
fi
|
|
||||||
done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2)
|
|
||||||
|
|
||||||
REAL_PATH=""
|
|
||||||
if [[ -n "$DISK" && -e "$DISK" ]]; then
|
|
||||||
REAL_PATH=$(readlink -f "$DISK")
|
|
||||||
fi
|
|
||||||
if [[ -n "$REAL_PATH" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
|
|
||||||
IS_MOUNTED=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
USED_BY=""
|
|
||||||
REAL_PATH=""
|
|
||||||
if [[ -n "$DISK" && -e "$DISK" ]]; then
|
|
||||||
REAL_PATH=$(readlink -f "$DISK")
|
|
||||||
fi
|
|
||||||
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
|
|
||||||
|
|
||||||
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
|
|
||||||
USED_BY="⚠ $(translate "In use")"
|
|
||||||
else
|
|
||||||
for SYMLINK in /dev/disk/by-id/*; do
|
|
||||||
[[ -e "$SYMLINK" ]] || continue
|
|
||||||
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
|
|
||||||
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
|
|
||||||
USED_BY="⚠ $(translate "In use")"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)"; then
|
|
||||||
if grep -q "active raid" /proc/mdstat; then
|
|
||||||
SHOW_DISK=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if $IS_ZFS; then SHOW_DISK=false; fi
|
|
||||||
if $IS_MOUNTED; then SHOW_DISK=false; fi
|
|
||||||
if $IS_SYSTEM; then SHOW_DISK=false; fi
|
|
||||||
|
|
||||||
if $SHOW_DISK; then
|
|
||||||
[[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]"
|
|
||||||
[[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID"
|
|
||||||
[[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM"
|
|
||||||
[[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS"
|
|
||||||
|
|
||||||
|
|
||||||
if $IS_USB; then
|
|
||||||
LABEL+=" USB"
|
|
||||||
else
|
|
||||||
LABEL+=" $(translate "Internal")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
|
||||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "off")
|
|
||||||
fi
|
|
||||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
|
||||||
|
|
||||||
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
|
|
||||||
dialog --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 60
|
|
||||||
clear
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_ok "$(translate "Available disks detected.")"
|
|
||||||
|
|
||||||
# Building the array for dialog (format: tag item on/off tag item on/off...)
|
|
||||||
DLG_LIST=()
|
|
||||||
for ((i=0; i<${#FREE_DISKS[@]}; i+=3)); do
|
|
||||||
DLG_LIST+=("${FREE_DISKS[i]}" "${FREE_DISKS[i+1]}" "${FREE_DISKS[i+2]}")
|
|
||||||
done
|
|
||||||
|
|
||||||
SELECTED=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
|
|
||||||
--radiolist "\n$(translate "Select the disk you want to mount on the host:")" 20 90 10 \
|
|
||||||
"${DLG_LIST[@]}" 2>&1 >/dev/tty)
|
|
||||||
|
|
||||||
if [ -z "$SELECTED" ]; then
|
|
||||||
dialog --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 8 50
|
|
||||||
clear
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
|
|
||||||
|
|
||||||
# ------------------- Partitions and formatting ------------------------
|
|
||||||
|
|
||||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
|
||||||
SKIP_FORMAT=false
|
|
||||||
DEFAULT_MOUNT="/mnt/backup"
|
|
||||||
|
|
||||||
if [ -n "$PARTITION" ]; then
|
|
||||||
PARTITION="/dev/$PARTITION"
|
|
||||||
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
|
||||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
|
||||||
SKIP_FORMAT=true
|
|
||||||
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
|
|
||||||
else
|
|
||||||
dialog --title "$(translate "Unsupported Filesystem")" --yesno \
|
|
||||||
"$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\nDo you want to format it?")" 10 70
|
|
||||||
if [ $? -ne 0 ]; then exit 0; fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
|
|
||||||
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
|
|
||||||
SKIP_FORMAT=true
|
|
||||||
PARTITION="$SELECTED"
|
|
||||||
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
|
|
||||||
else
|
|
||||||
dialog --title "$(translate "No Valid Partitions")" --yesno \
|
|
||||||
"$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
|
|
||||||
if [ $? -ne 0 ]; then exit 0; fi
|
|
||||||
|
|
||||||
echo -e "$(translate "Creating partition table and partition...")"
|
|
||||||
parted -s "$SELECTED" mklabel gpt
|
|
||||||
parted -s "$SELECTED" mkpart primary 0% 100%
|
|
||||||
sleep 2
|
|
||||||
partprobe "$SELECTED"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
|
|
||||||
if [ -n "$PARTITION" ]; then
|
|
||||||
PARTITION="/dev/$PARTITION"
|
|
||||||
else
|
|
||||||
dialog --title "$(translate "Partition Error")" --msgbox \
|
|
||||||
"$(translate "Failed to create partition on disk") $SELECTED." 8 70
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SKIP_FORMAT" != true ]; then
|
|
||||||
FORMAT_TYPE=$(dialog --title "$(translate "Select Format Type")" --menu \
|
|
||||||
"$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
|
|
||||||
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
|
|
||||||
"xfs" "XFS" \
|
|
||||||
"btrfs" "Btrfs" 2>&1 >/dev/tty)
|
|
||||||
if [ -z "$FORMAT_TYPE" ]; then
|
|
||||||
dialog --title "$(translate "Format Cancelled")" --msgbox \
|
|
||||||
"$(translate "Format operation cancelled. The disk will not be added.")" 8 60
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
dialog --title "$(translate "WARNING")" --yesno \
|
|
||||||
"$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\n\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\n\n$(translate "Are you sure you want to continue")" 15 70
|
|
||||||
if [ $? -ne 0 ]; then exit 0; fi
|
|
||||||
|
|
||||||
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
|
|
||||||
case "$FORMAT_TYPE" in
|
|
||||||
"ext4") mkfs.ext4 -F "$PARTITION" ;;
|
|
||||||
"xfs") mkfs.xfs -f "$PARTITION" ;;
|
|
||||||
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
cleanup
|
|
||||||
dialog --title "$(translate "Format Failed")" --msgbox \
|
|
||||||
"$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
|
|
||||||
partprobe "$SELECTED"
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ------------------- Mount point and permissions -------------------
|
|
||||||
|
|
||||||
MOUNT_POINT=$(dialog --title "$(translate "Mount Point")" \
|
|
||||||
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/backup):")" \
|
|
||||||
10 60 "$DEFAULT_MOUNT" 2>&1 >/dev/tty)
|
|
||||||
if [ -z "$MOUNT_POINT" ]; then
|
|
||||||
dialog --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
|
|
||||||
|
|
||||||
mkdir -p "$MOUNT_POINT"
|
|
||||||
|
|
||||||
UUID=$(blkid -s UUID -o value "$PARTITION")
|
|
||||||
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
|
|
||||||
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
|
|
||||||
|
|
||||||
if grep -q "UUID=$UUID" /etc/fstab; then
|
|
||||||
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
|
|
||||||
msg_ok "$(translate "fstab entry updated for") $UUID"
|
|
||||||
else
|
|
||||||
echo "$FSTAB_ENTRY" >> /etc/fstab
|
|
||||||
msg_ok "$(translate "fstab entry added for") $UUID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
if ! getent group sharedfiles >/dev/null; then
|
|
||||||
groupadd sharedfiles
|
|
||||||
msg_ok "$(translate "Group 'sharedfiles' created")"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate "Group 'sharedfiles' already exists")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown root:sharedfiles "$MOUNT_POINT"
|
|
||||||
chmod 2775 "$MOUNT_POINT"
|
|
||||||
|
|
||||||
dialog --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
|
|
||||||
echo "$MOUNT_POINT" > /usr/local/share/proxmenux/last_backup_mount.txt
|
|
||||||
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
|
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
|
||||||
read -r
|
|
||||||
else
|
|
||||||
dialog --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
|
|
||||||
msg_success "$(translate "Press Enter to return to menu...")"
|
|
||||||
read -r
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
399
scripts/storage/smart-disk-test.sh
Normal file
399
scripts/storage/smart-disk-test.sh
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# ProxMenux - SMART Disk Health & Test Tool
|
||||||
|
# ==========================================================
|
||||||
|
# Author : MacRimi
|
||||||
|
# Copyright : (c) 2024 MacRimi
|
||||||
|
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
|
# Version : 1.0
|
||||||
|
# Last Updated: 12/04/2026
|
||||||
|
# ==========================================================
|
||||||
|
# Description:
|
||||||
|
# SMART health check and disk testing tool for Proxmox VE.
|
||||||
|
# Supports SATA/SAS disks (smartmontools) and NVMe drives (nvme-cli).
|
||||||
|
# Exports results as JSON to /usr/local/share/proxmenux/smart/
|
||||||
|
# for ProxMenux Monitor integration.
|
||||||
|
# Long tests run on the drive hardware and persist after terminal close.
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
BACKTITLE="ProxMenux"
|
||||||
|
SMART_DIR="$BASE_DIR/smart"
|
||||||
|
UI_MENU_H=22
|
||||||
|
UI_MENU_W=84
|
||||||
|
UI_MENU_LIST_H=12
|
||||||
|
UI_SHORT_MENU_H=16
|
||||||
|
UI_SHORT_MENU_W=72
|
||||||
|
UI_SHORT_MENU_LIST_H=6
|
||||||
|
UI_MSG_H=10
|
||||||
|
UI_MSG_W=72
|
||||||
|
UI_RESULT_H=14
|
||||||
|
UI_RESULT_W=86
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||||
|
load_language
|
||||||
|
initialize_cache
|
||||||
|
|
||||||
|
SCRIPT_DIR_SMART="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR_SMART/.." && pwd)"
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/utils-install-functions.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_LOCAL/global/utils-install-functions.sh"
|
||||||
|
elif [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
|
||||||
|
fi
|
||||||
|
# Configuration ============================================
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# Helpers
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
_smart_is_nvme() {
|
||||||
|
[[ "$1" == *nvme* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
_smart_disk_label() {
|
||||||
|
local disk="$1"
|
||||||
|
local model size
|
||||||
|
model=$(lsblk -dn -o MODEL "$disk" 2>/dev/null | xargs)
|
||||||
|
size=$(lsblk -dn -o SIZE "$disk" 2>/dev/null | xargs)
|
||||||
|
[[ -z "$model" ]] && model="Unknown"
|
||||||
|
[[ -z "$size" ]] && size="?"
|
||||||
|
printf '%-8s — %s' "$size" "$model"
|
||||||
|
}
|
||||||
|
|
||||||
|
_smart_json_path() {
|
||||||
|
local disk="$1"
|
||||||
|
echo "${SMART_DIR}/$(basename "$disk").json"
|
||||||
|
}
|
||||||
|
|
||||||
|
_smart_ensure_packages() {
|
||||||
|
local need_smartctl=0 need_nvme=0
|
||||||
|
command -v smartctl >/dev/null 2>&1 || need_smartctl=1
|
||||||
|
command -v nvme >/dev/null 2>&1 || need_nvme=1
|
||||||
|
if [[ $need_smartctl -eq 1 || $need_nvme -eq 1 ]]; then
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate 'SMART Disk Health & Test')"
|
||||||
|
ensure_repositories
|
||||||
|
[[ $need_smartctl -eq 1 ]] && install_single_package "smartmontools" "smartctl" "SMART monitoring tools"
|
||||||
|
[[ $need_nvme -eq 1 ]] && install_single_package "nvme-cli" "nvme" "NVMe management tools"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# PHASE 1 — SELECTION
|
||||||
|
# All dialogs run here. No execution, no show_proxmenux_logo.
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
# ── Install packages if missing ───────────────────────────
|
||||||
|
_smart_ensure_packages
|
||||||
|
|
||||||
|
# ── Step 1: Detect disks ──────────────────────────────────
|
||||||
|
DISK_OPTIONS=()
|
||||||
|
while read -r disk; do
|
||||||
|
[[ -z "$disk" ]] && continue
|
||||||
|
[[ "$disk" =~ ^/dev/zd ]] && continue
|
||||||
|
label=$(_smart_disk_label "$disk")
|
||||||
|
DISK_OPTIONS+=("$disk" "$label")
|
||||||
|
done < <(lsblk -dn -e 7,11 -o PATH 2>/dev/null | grep -E '^/dev/(sd|nvme|vd|hd)')
|
||||||
|
stop_spinner
|
||||||
|
|
||||||
|
if [[ ${#DISK_OPTIONS[@]} -eq 0 ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'No Disks Found')" \
|
||||||
|
--msgbox "\n$(translate 'No physical disks detected for SMART testing.')" \
|
||||||
|
$UI_MSG_H $UI_MSG_W
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Step 2: Select disk ───────────────────────────────────
|
||||||
|
SELECTED_DISK=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Select Disk')" \
|
||||||
|
--menu "\n$(translate 'Select the disk to test or inspect:')" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"${DISK_OPTIONS[@]}" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$SELECTED_DISK" ]] && exit 0
|
||||||
|
|
||||||
|
# ── Steps 3+: Action loop for the selected disk ───────────
|
||||||
|
DISK_LABEL=$(_smart_disk_label "$SELECTED_DISK")
|
||||||
|
mkdir -p "$SMART_DIR"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
|
||||||
|
# ── Select action ───────────────────────────────────────
|
||||||
|
ACTION=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'SMART Action') — $(basename "$SELECTED_DISK") (${DISK_LABEL})" \
|
||||||
|
--menu "\n$(translate 'Select what to do with this disk:')" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"status" "$(translate 'Quick health status — overall SMART result + key attributes')" \
|
||||||
|
"report" "$(translate 'Full report — complete SMART data (scrollable)')" \
|
||||||
|
"short" "$(translate 'Short test — ~2 minutes, basic surface check')" \
|
||||||
|
"long" "$(translate 'Long test — full scan, runs in background if closed')" \
|
||||||
|
"progress" "$(translate 'Check test progress — show active or last test result')" \
|
||||||
|
2>&1 >/dev/tty)
|
||||||
|
[[ -z "$ACTION" ]] && exit 0
|
||||||
|
|
||||||
|
# ── Long test confirmation ───────────────────────────────
|
||||||
|
if [[ "$ACTION" == "long" ]]; then
|
||||||
|
DISK_SIZE=$(lsblk -dn -o SIZE "$SELECTED_DISK" 2>/dev/null | xargs)
|
||||||
|
if ! dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Long Test — Background')" \
|
||||||
|
--yesno "\n$(translate 'The long test runs directly on the disk hardware.')\n\n$(translate 'Disk:') $SELECTED_DISK ($DISK_SIZE)\n\n$(translate 'The test will continue even if you close this terminal.')\n$(translate 'Results will be saved automatically to:')\n$(_smart_json_path "$SELECTED_DISK")\n\n$(translate 'Start long test now?')" \
|
||||||
|
16 $UI_RESULT_W; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# PHASE 2 — EXECUTION
|
||||||
|
# show_proxmenux_logo appears here exactly once per action.
|
||||||
|
# No dialogs from this point until "Press Enter".
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate 'SMART Disk Health & Test')"
|
||||||
|
msg_ok "$(translate 'Disk:') ${BL}${SELECTED_DISK} — ${DISK_LABEL}${CL}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
|
||||||
|
# ── Quick status ────────────────────────────────────────
|
||||||
|
status)
|
||||||
|
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||||
|
msg_info "$(translate 'Reading NVMe SMART data...')"
|
||||||
|
OUTPUT=$(nvme smart-log "$SELECTED_DISK" 2>/dev/null)
|
||||||
|
stop_spinner
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
|
||||||
|
else
|
||||||
|
HEALTH=$(echo "$OUTPUT" | grep -i "critical_warning" | awk '{print $NF}')
|
||||||
|
if [[ "$HEALTH" == "0" ]]; then
|
||||||
|
msg_ok "$(translate 'NVMe health status: PASSED')"
|
||||||
|
else
|
||||||
|
msg_warn "$(translate 'NVMe health status: WARNING (critical_warning =') $HEALTH)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "$OUTPUT" | head -20
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_info "$(translate 'Reading SMART data...')"
|
||||||
|
HEALTH=$(smartctl -H "$SELECTED_DISK" 2>/dev/null | grep -i "overall-health")
|
||||||
|
ATTRS=$(smartctl -A "$SELECTED_DISK" 2>/dev/null)
|
||||||
|
stop_spinner
|
||||||
|
if [[ -z "$HEALTH" ]]; then
|
||||||
|
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
|
||||||
|
else
|
||||||
|
if echo "$HEALTH" | grep -qi "PASSED"; then
|
||||||
|
msg_ok "$(translate 'SMART health status: PASSED')"
|
||||||
|
else
|
||||||
|
msg_warn "$HEALTH"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "$ATTRS" | awk 'NR==1 || /Reallocated_Sector|Current_Pending|Uncorrectable|Temperature_Celsius|Power_On_Hours|Wear_Leveling|Media_Wearout/'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
# ── Full report (scrollable) ────────────────────────────
|
||||||
|
report)
|
||||||
|
msg_info "$(translate 'Reading full SMART report...')"
|
||||||
|
TMPFILE=$(mktemp)
|
||||||
|
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||||
|
nvme smart-log "$SELECTED_DISK" > "$TMPFILE" 2>/dev/null
|
||||||
|
nvme id-ctrl "$SELECTED_DISK" >> "$TMPFILE" 2>/dev/null
|
||||||
|
else
|
||||||
|
smartctl -x "$SELECTED_DISK" > "$TMPFILE" 2>/dev/null
|
||||||
|
fi
|
||||||
|
stop_spinner
|
||||||
|
if [[ -s "$TMPFILE" ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate 'Full SMART Report') — $SELECTED_DISK" \
|
||||||
|
--textbox "$TMPFILE" 40 $UI_RESULT_W
|
||||||
|
else
|
||||||
|
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
|
||||||
|
fi
|
||||||
|
rm -f "$TMPFILE"
|
||||||
|
;;
|
||||||
|
|
||||||
|
# ── Short test ──────────────────────────────────────────
|
||||||
|
short)
|
||||||
|
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||||
|
msg_info "$(translate 'Starting NVMe short self-test...')"
|
||||||
|
if nvme device-self-test "$SELECTED_DISK" --self-test-code=1 >/dev/null 2>&1; then
|
||||||
|
stop_spinner
|
||||||
|
msg_ok "$(translate 'Short self-test started on') $SELECTED_DISK"
|
||||||
|
msg_ok "$(translate 'Test typically completes in ~2 minutes.')"
|
||||||
|
msg_ok "$(translate 'Use "Check test progress" to see results.')"
|
||||||
|
else
|
||||||
|
stop_spinner
|
||||||
|
msg_error "$(translate 'Failed to start self-test on') $SELECTED_DISK"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_info "$(translate 'Starting SMART short self-test...')"
|
||||||
|
OUTPUT=$(smartctl -t short "$SELECTED_DISK" 2>/dev/null)
|
||||||
|
stop_spinner
|
||||||
|
if echo "$OUTPUT" | grep -qi "Test will complete"; then
|
||||||
|
msg_ok "$(translate 'Short self-test started on') $SELECTED_DISK"
|
||||||
|
ESTIMATE=$(echo "$OUTPUT" | grep -i "complete after" | head -1)
|
||||||
|
[[ -n "$ESTIMATE" ]] && msg_ok "$ESTIMATE"
|
||||||
|
msg_ok "$(translate 'Use "Check test progress" to see results.')"
|
||||||
|
else
|
||||||
|
msg_error "$(translate 'Failed to start self-test on') $SELECTED_DISK"
|
||||||
|
echo "$OUTPUT" | tail -5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
# ── Long test (background) ──────────────────────────────
|
||||||
|
long)
|
||||||
|
JSON_PATH=$(_smart_json_path "$SELECTED_DISK")
|
||||||
|
DISK_SAFE=$(printf '%q' "$SELECTED_DISK")
|
||||||
|
JSON_SAFE=$(printf '%q' "$JSON_PATH")
|
||||||
|
|
||||||
|
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||||
|
msg_info "$(translate 'Starting NVMe long self-test...')"
|
||||||
|
if nvme device-self-test "$SELECTED_DISK" --self-test-code=2 >/dev/null 2>&1; then
|
||||||
|
stop_spinner
|
||||||
|
msg_ok "$(translate 'Long self-test started on') $SELECTED_DISK"
|
||||||
|
DISK_LABEL_SAFE=$(printf '%q' "$DISK_LABEL")
|
||||||
|
NOTIFY_SCRIPT="/usr/bin/notification_manager.py"
|
||||||
|
nohup bash -c "
|
||||||
|
while nvme device-self-test ${DISK_SAFE} --self-test-code=0 2>/dev/null | grep -qi 'in progress'; do
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
|
nvme smart-log -o json ${DISK_SAFE} > ${JSON_SAFE} 2>/dev/null
|
||||||
|
|
||||||
|
# Send notification when test completes
|
||||||
|
if [[ -f \"${NOTIFY_SCRIPT}\" ]]; then
|
||||||
|
HOSTNAME=\$(hostname -s)
|
||||||
|
TEST_RESULT=\$(nvme self-test-log ${DISK_SAFE} 2>/dev/null | head -20)
|
||||||
|
if echo \"\$TEST_RESULT\" | grep -qi 'completed without error\|success'; then
|
||||||
|
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
|
||||||
|
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||||
|
--message \"NVMe disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed successfully.\" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity WARNING \
|
||||||
|
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||||
|
--message \"NVMe disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed. Check results for details.\" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
" >/dev/null 2>&1 &
|
||||||
|
disown $!
|
||||||
|
else
|
||||||
|
stop_spinner
|
||||||
|
msg_error "$(translate 'Failed to start long self-test on') $SELECTED_DISK"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_info "$(translate 'Starting SMART long self-test...')"
|
||||||
|
OUTPUT=$(smartctl -t long "$SELECTED_DISK" 2>/dev/null)
|
||||||
|
stop_spinner
|
||||||
|
if echo "$OUTPUT" | grep -qi "Test will complete"; then
|
||||||
|
msg_ok "$(translate 'Long self-test started on') $SELECTED_DISK"
|
||||||
|
ESTIMATE=$(echo "$OUTPUT" | grep -i "complete after" | head -1)
|
||||||
|
[[ -n "$ESTIMATE" ]] && msg_ok "$ESTIMATE"
|
||||||
|
echo ""
|
||||||
|
msg_ok "$(translate 'Test runs on the drive hardware — safe to close this terminal.')"
|
||||||
|
msg_ok "$(translate 'Results will be saved to:') $JSON_PATH"
|
||||||
|
DISK_LABEL_SAFE=$(printf '%q' "$DISK_LABEL")
|
||||||
|
NOTIFY_SCRIPT="/usr/bin/notification_manager.py"
|
||||||
|
nohup bash -c "
|
||||||
|
while smartctl -c ${DISK_SAFE} 2>/dev/null | grep -qiE 'Self-test routine in progress|[1-9][0-9]?% of test remaining'; do
|
||||||
|
sleep 60
|
||||||
|
done
|
||||||
|
smartctl --json=c ${DISK_SAFE} > ${JSON_SAFE} 2>/dev/null
|
||||||
|
|
||||||
|
# Send notification when test completes
|
||||||
|
if [[ -f \"${NOTIFY_SCRIPT}\" ]]; then
|
||||||
|
HOSTNAME=\$(hostname -s)
|
||||||
|
TEST_RESULT=\$(smartctl -l selftest ${DISK_SAFE} 2>/dev/null | grep -E '^# ?1')
|
||||||
|
if echo \"\$TEST_RESULT\" | grep -qi 'Completed without error'; then
|
||||||
|
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
|
||||||
|
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||||
|
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed successfully.\" 2>/dev/null || true
|
||||||
|
elif echo \"\$TEST_RESULT\" | grep -qi 'error\|fail'; then
|
||||||
|
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity CRITICAL \
|
||||||
|
--title \"\${HOSTNAME}: SMART Long Test FAILED\" \
|
||||||
|
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed with ERRORS. Check disk health immediately.\" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
|
||||||
|
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
|
||||||
|
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed. Check results for details.\" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
" >/dev/null 2>&1 &
|
||||||
|
disown $!
|
||||||
|
else
|
||||||
|
msg_error "$(translate 'Failed to start long self-test on') $SELECTED_DISK"
|
||||||
|
echo "$OUTPUT" | tail -5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
# ── Check progress ──────────────────────────────────────
|
||||||
|
progress)
|
||||||
|
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||||
|
msg_info "$(translate 'Reading NVMe self-test log...')"
|
||||||
|
OUTPUT=$(nvme self-test-log "$SELECTED_DISK" 2>/dev/null)
|
||||||
|
stop_spinner
|
||||||
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
|
msg_warn "$(translate 'No self-test log available for') $SELECTED_DISK"
|
||||||
|
else
|
||||||
|
echo "$OUTPUT" | head -30
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_info "$(translate 'Reading SMART self-test log...')"
|
||||||
|
# Active test: only "X% of test remaining" appears when a test is actually running
|
||||||
|
ACTIVE=$(smartctl -c "$SELECTED_DISK" 2>/dev/null | grep -iE "[1-9][0-9]?% of test remaining|Self-test routine in progress")
|
||||||
|
# Log: grab only result rows (^# N ...) and the column header (^Num)
|
||||||
|
LOG_OUT=$(smartctl -l selftest "$SELECTED_DISK" 2>/dev/null)
|
||||||
|
LOG_HEADER=$(echo "$LOG_OUT" | grep -E "^Num")
|
||||||
|
LOG_ENTRIES=$(echo "$LOG_OUT" | grep -E "^# ?[0-9]")
|
||||||
|
stop_spinner
|
||||||
|
if [[ -n "$ACTIVE" ]]; then
|
||||||
|
msg_ok "$(translate 'Test in progress:')"
|
||||||
|
echo "$ACTIVE"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
msg_ok "$(translate 'No test currently running')"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
if [[ -n "$LOG_ENTRIES" ]]; then
|
||||||
|
msg_ok "$(translate 'Recent test results:')"
|
||||||
|
[[ -n "$LOG_HEADER" ]] && echo "$LOG_HEADER"
|
||||||
|
echo "$LOG_ENTRIES"
|
||||||
|
else
|
||||||
|
msg_warn "$(translate 'No self-test history found for') $SELECTED_DISK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── Auto-export JSON (except long — handled by background monitor)
|
||||||
|
if [[ "$ACTION" != "long" && "$ACTION" != "report" ]]; then
|
||||||
|
JSON_PATH=$(_smart_json_path "$SELECTED_DISK")
|
||||||
|
if _smart_is_nvme "$SELECTED_DISK"; then
|
||||||
|
nvme smart-log -o json "$SELECTED_DISK" > "$JSON_PATH" 2>/dev/null
|
||||||
|
else
|
||||||
|
smartctl --json=c "$SELECTED_DISK" > "$JSON_PATH" 2>/dev/null
|
||||||
|
fi
|
||||||
|
[[ -s "$JSON_PATH" ]] || rm -f "$JSON_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── "report" uses dialog --textbox, no Press Enter needed
|
||||||
|
if [[ "$ACTION" != "report" ]]; then
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate 'Press Enter to continue...')"
|
||||||
|
read -r
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
@@ -1,73 +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.0
|
|
||||||
# Last Updated: 28/01/2025
|
|
||||||
# Description : Allows unmounting a previously mounted disk
|
|
||||||
# ==========================================================
|
|
||||||
|
|
||||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
|
||||||
BASE_DIR="/usr/local/share/proxmenux"
|
|
||||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
|
||||||
|
|
||||||
if [[ -f "$UTILS_FILE" ]]; then
|
|
||||||
source "$UTILS_FILE"
|
|
||||||
fi
|
|
||||||
load_language
|
|
||||||
initialize_cache
|
|
||||||
|
|
||||||
|
|
||||||
MOUNTED_DISKS=($(mount | grep '^/dev/' | grep 'on /mnt/' | awk '{print $3}'))
|
|
||||||
|
|
||||||
if [[ ${#MOUNTED_DISKS[@]} -eq 0 ]]; then
|
|
||||||
whiptail --title "$(translate "No Disks")" --msgbox "$(translate "No mounted disks found under /mnt.")" 8 50
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
MENU_ITEMS=()
|
|
||||||
for MNT in "${MOUNTED_DISKS[@]}"; do
|
|
||||||
UUID=$(blkid | grep "$MNT" | awk '{print $2}' | tr -d '"')
|
|
||||||
DESC="$MNT $UUID"
|
|
||||||
MENU_ITEMS+=("$MNT" "$DESC")
|
|
||||||
done
|
|
||||||
|
|
||||||
SELECTED=$(whiptail --title "$(translate "Unmount Disk")" --menu "$(translate "Select the disk you want to unmount:")" 20 70 10 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
[[ -z "$SELECTED" ]] && exit 0
|
|
||||||
|
|
||||||
|
|
||||||
whiptail --title "$(translate "Confirm Unmount")" --yesno "$(translate "Are you sure you want to unmount") $SELECTED?" 10 60 || exit 0
|
|
||||||
|
|
||||||
|
|
||||||
umount "$SELECTED" 2>/dev/null
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
whiptail --title "$(translate "Error")" --msgbox "$(translate "Failed to unmount disk at") $SELECTED" 8 60
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
msg_ok "$(translate "Unmounted:") $SELECTED"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
whiptail --title "$(translate "Delete Mount Folder")" --yesno "$(translate "Do you want to delete the mount point folder") $SELECTED?" 10 60
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
rm -rf "$SELECTED"
|
|
||||||
msg_ok "$(translate "Deleted folder:") $SELECTED"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE=$(findmnt -no SOURCE "$SELECTED")
|
|
||||||
UUID=$(blkid -s UUID -o value "$DEVICE")
|
|
||||||
|
|
||||||
if [ -n "$UUID" ]; then
|
|
||||||
sed -i "/UUID=$UUID/d" /etc/fstab
|
|
||||||
msg_ok "$(translate "fstab entry removed for") $UUID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
whiptail --title "$(translate "Done")" --msgbox "$(translate "Disk unmounted and cleaned successfully.")" 8 60
|
|
||||||
|
|
||||||
628
scripts/utilities/export_vm_ova_ovf.sh
Executable file
628
scripts/utilities/export_vm_ova_ovf.sh
Executable file
@@ -0,0 +1,628 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==========================================================
|
||||||
|
# ProxMenux - Export VM to OVA or OVF
|
||||||
|
# ==========================================================
|
||||||
|
# Author : MacRimi
|
||||||
|
# Copyright : (c) 2024 MacRimi
|
||||||
|
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
|
# Version : 1.0
|
||||||
|
# Last Updated: 07/04/2026
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||||
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
|
|
||||||
|
if [[ -f "$UTILS_FILE" ]]; then
|
||||||
|
source "$UTILS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
load_language
|
||||||
|
initialize_cache
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Missing dependency")" \
|
||||||
|
--msgbox "$(translate "Required command not found:") $cmd" 8 60
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
human_bytes() {
|
||||||
|
local bytes="$1"
|
||||||
|
local units=("B" "KB" "MB" "GB" "TB" "PB")
|
||||||
|
local idx=0
|
||||||
|
local value="$bytes"
|
||||||
|
|
||||||
|
[[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }
|
||||||
|
|
||||||
|
while [[ "$value" -ge 1024 && "$idx" -lt 5 ]]; do
|
||||||
|
value=$((value / 1024))
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "${value}${units[$idx]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize_name() {
|
||||||
|
local raw="$1"
|
||||||
|
local out
|
||||||
|
out=$(echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-zA-Z0-9._-]/_/g' | sed 's/__*/_/g' | sed 's/^_\+//;s/_\+$//')
|
||||||
|
[[ -z "$out" ]] && out="vm"
|
||||||
|
echo "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
xml_escape() {
|
||||||
|
local s="$1"
|
||||||
|
s=${s//&/&}
|
||||||
|
s=${s//</<}
|
||||||
|
s=${s//>/>}
|
||||||
|
s=${s//\"/"}
|
||||||
|
s=${s//\'/'}
|
||||||
|
echo "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_destination_dir() {
|
||||||
|
local dir="$1"
|
||||||
|
if [[ ! -d "$dir" ]]; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
|
||||||
|
--msgbox "$(translate "Destination directory does not exist:")\n$dir" 8 74
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -w "$dir" ]]; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Permission error")" \
|
||||||
|
--msgbox "$(translate "Destination directory is not writable:")\n$dir" 8 70
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
select_vm() {
|
||||||
|
local options=()
|
||||||
|
local line vmid name status
|
||||||
|
|
||||||
|
while read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
vmid=$(echo "$line" | awk '{print $1}')
|
||||||
|
name=$(echo "$line" | awk '{print $2}')
|
||||||
|
status=$(echo "$line" | awk '{print $3}')
|
||||||
|
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
|
||||||
|
[[ -z "$name" ]] && name="vm-${vmid}"
|
||||||
|
options+=("$vmid" "$name [$status]")
|
||||||
|
done < <(qm list 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ ${#options[@]} -eq 0 ]]; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "No VMs found")" \
|
||||||
|
--msgbox "$(translate "No virtual machines were found on this host.")" 8 60
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VMID=$(dialog --backtitle "ProxMenux" --title "$(translate "Export VM to OVA or OVF")" \
|
||||||
|
--menu "$(translate "Select VM to export:")" 20 80 12 \
|
||||||
|
"${options[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
[[ -n "$VMID" ]] || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_vm_stopped() {
|
||||||
|
local status
|
||||||
|
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||||
|
|
||||||
|
if [[ "$status" == "stopped" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! dialog --backtitle "ProxMenux" --title "$(translate "VM is running")" --yesno \
|
||||||
|
"$(translate "For a consistent export, the VM should be stopped.")\n\n$(translate "Do you want ProxMenux to stop it now?")" 10 70; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
qm shutdown "$VMID" --timeout 120 >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||||
|
[[ "$status" == "stopped" ]] && return 0
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if dialog --backtitle "ProxMenux" --title "$(translate "Shutdown timeout")" --yesno \
|
||||||
|
"$(translate "Graceful shutdown timed out.")\n\n$(translate "Force stop VM now?")" 10 60; then
|
||||||
|
qm stop "$VMID" >/dev/null 2>&1 || true
|
||||||
|
sleep 2
|
||||||
|
status=$(qm status "$VMID" 2>/dev/null | awk '{print $2}')
|
||||||
|
[[ "$status" == "stopped" ]] && return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Cannot continue")" \
|
||||||
|
--msgbox "$(translate "VM is still running. Export cancelled.")" 8 60
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
select_export_mode() {
|
||||||
|
EXPORT_MODE=$(dialog --backtitle "ProxMenux" --title "$(translate "Export Format")" \
|
||||||
|
--menu "$(translate "Select export format:")" 14 70 4 \
|
||||||
|
"ova" "$(translate "OVA (single portable file)")" \
|
||||||
|
"ovf" "$(translate "OVF (descriptor + VMDK files)")" \
|
||||||
|
3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$EXPORT_MODE" ]] || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
select_destination_dir() {
|
||||||
|
local dump_dir="/var/lib/vz/dump"
|
||||||
|
local iso_dir="/var/lib/vz/template/iso"
|
||||||
|
local options=(
|
||||||
|
"1" "$dump_dir [$(translate "recommended")]"
|
||||||
|
"2" "$iso_dir [$(translate "recommended")]"
|
||||||
|
"M" "$(translate "Manual path entry")"
|
||||||
|
)
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local choice
|
||||||
|
choice=$(dialog --backtitle "ProxMenux" --title "$(translate "Destination Directory")" \
|
||||||
|
--menu "$(translate "Select where to export VM files (OVA/OVF + temporary workspace):")" \
|
||||||
|
16 84 8 "${options[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
[[ -n "$choice" ]] || return 1
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
M)
|
||||||
|
DEST_DIR=$(dialog --backtitle "ProxMenux" --title "$(translate "Manual destination path")" \
|
||||||
|
--inputbox "$(translate "Enter destination directory for exported file(s):")" \
|
||||||
|
10 90 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$DEST_DIR" ]] || continue
|
||||||
|
if [[ ! -d "$DEST_DIR" ]]; then
|
||||||
|
if dialog --backtitle "ProxMenux" --title "$(translate "Create directory")" \
|
||||||
|
--yesno "$(translate "The selected directory does not exist:")\n$DEST_DIR\n\n$(translate "Do you want to create it now?")" \
|
||||||
|
11 80; then
|
||||||
|
if ! mkdir -p "$DEST_DIR" 2>/dev/null; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Directory error")" \
|
||||||
|
--msgbox "$(translate "Could not create destination directory:")\n$DEST_DIR" 8 74
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
validate_destination_dir "$DEST_DIR" || continue
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
DEST_DIR="$dump_dir"
|
||||||
|
validate_destination_dir "$DEST_DIR" || continue
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
DEST_DIR="$iso_dir"
|
||||||
|
validate_destination_dir "$DEST_DIR" || continue
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
get_vm_metadata() {
|
||||||
|
VM_CONF=$(qm config "$VMID" 2>/dev/null) || return 1
|
||||||
|
|
||||||
|
VM_NAME=$(echo "$VM_CONF" | awk -F': ' '/^name:/{print $2; exit}')
|
||||||
|
[[ -z "$VM_NAME" ]] && VM_NAME="vm-${VMID}"
|
||||||
|
|
||||||
|
VM_MEMORY=$(echo "$VM_CONF" | awk -F': ' '/^memory:/{print $2; exit}')
|
||||||
|
[[ -z "$VM_MEMORY" ]] && VM_MEMORY=1024
|
||||||
|
|
||||||
|
VM_CORES=$(echo "$VM_CONF" | awk -F': ' '/^cores:/{print $2; exit}')
|
||||||
|
VM_SOCKETS=$(echo "$VM_CONF" | awk -F': ' '/^sockets:/{print $2; exit}')
|
||||||
|
[[ -z "$VM_CORES" ]] && VM_CORES=1
|
||||||
|
[[ -z "$VM_SOCKETS" ]] && VM_SOCKETS=1
|
||||||
|
VM_VCPUS=$((VM_CORES * VM_SOCKETS))
|
||||||
|
|
||||||
|
VM_OSTYPE=$(echo "$VM_CONF" | awk -F': ' '/^ostype:/{print $2; exit}')
|
||||||
|
case "$VM_OSTYPE" in
|
||||||
|
l26|l24) VM_OS_DESC="Linux" ;;
|
||||||
|
win11|win10|win8|win7|w2k8|w2k12|w2k16|w2k19|w2k22|wxp|w2k|w2k3)
|
||||||
|
VM_OS_DESC="Windows"
|
||||||
|
;;
|
||||||
|
*) VM_OS_DESC="Other" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NET_COUNT=$(echo "$VM_CONF" | grep -E '^net[0-9]+:' | wc -l)
|
||||||
|
}
|
||||||
|
|
||||||
|
get_virtual_size_bytes() {
|
||||||
|
local src="$1"
|
||||||
|
local bytes=""
|
||||||
|
|
||||||
|
bytes=$(qemu-img info "$src" 2>/dev/null | sed -n 's/.*virtual size:.*(\([0-9]\+\) bytes).*/\1/p' | head -1)
|
||||||
|
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "$bytes"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -b "$src" ]]; then
|
||||||
|
bytes=$(blockdev --getsize64 "$src" 2>/dev/null || true)
|
||||||
|
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "$bytes"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
bytes=$(stat -c%s "$src" 2>/dev/null || true)
|
||||||
|
if [[ -n "$bytes" && "$bytes" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "$bytes"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "0"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_vm_disks() {
|
||||||
|
DISK_COUNT=0
|
||||||
|
unset DISK_SLOTS DISK_SRCS DISK_VSIZES
|
||||||
|
declare -ga DISK_SLOTS DISK_SRCS DISK_VSIZES
|
||||||
|
|
||||||
|
local line slot value source src
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^(scsi|sata|virtio|ide)[0-9]+: ]]; then
|
||||||
|
slot="${line%%:*}"
|
||||||
|
value="${line#*: }"
|
||||||
|
|
||||||
|
[[ "$value" == *"media=cdrom"* ]] && continue
|
||||||
|
[[ "$value" == *"cloudinit"* ]] && continue
|
||||||
|
|
||||||
|
source="${value%%,*}"
|
||||||
|
[[ -z "$source" || "$source" == "none" ]] && continue
|
||||||
|
|
||||||
|
src=""
|
||||||
|
if [[ "$source" == /dev/* || "$source" == /* ]]; then
|
||||||
|
src="$source"
|
||||||
|
elif [[ "$source" == *:* ]]; then
|
||||||
|
src=$(pvesm path "$source" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$src" || ! -e "$src" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
DISK_SLOTS+=("$slot")
|
||||||
|
DISK_SRCS+=("$src")
|
||||||
|
DISK_VSIZES+=("$(get_virtual_size_bytes "$src")")
|
||||||
|
DISK_COUNT=$((DISK_COUNT + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$VM_CONF"
|
||||||
|
|
||||||
|
[[ "$DISK_COUNT" -gt 0 ]] || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_destination_space() {
|
||||||
|
local total=0
|
||||||
|
local i
|
||||||
|
for i in "${DISK_VSIZES[@]}"; do
|
||||||
|
[[ "$i" =~ ^[0-9]+$ ]] && total=$((total + i))
|
||||||
|
done
|
||||||
|
|
||||||
|
local factor=120
|
||||||
|
[[ "$EXPORT_MODE" == "ova" ]] && factor=220
|
||||||
|
REQUIRED_BYTES=$((total * factor / 100))
|
||||||
|
|
||||||
|
AVAILABLE_BYTES=$(df -PB1 "$DEST_DIR" | awk 'NR==2{print $4}')
|
||||||
|
[[ "$AVAILABLE_BYTES" =~ ^[0-9]+$ ]] || AVAILABLE_BYTES=0
|
||||||
|
|
||||||
|
if [[ "$AVAILABLE_BYTES" -lt "$REQUIRED_BYTES" ]]; then
|
||||||
|
if ! dialog --backtitle "ProxMenux" --title "$(translate "Low free space warning")" --yesno \
|
||||||
|
"$(translate "Estimated required free space:") $(human_bytes "$REQUIRED_BYTES") ($REQUIRED_BYTES bytes)\n$(translate "Current free space:") $(human_bytes "$AVAILABLE_BYTES") ($AVAILABLE_BYTES bytes)\n\n$(translate "Do you want to continue anyway?")" 13 90; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_ovf_descriptor() {
|
||||||
|
local ovf_path="$1"
|
||||||
|
local vm_name_xml os_desc_xml
|
||||||
|
vm_name_xml=$(xml_escape "$VM_NAME")
|
||||||
|
os_desc_xml=$(xml_escape "$VM_OS_DESC")
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
echo '<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">'
|
||||||
|
echo ' <References>'
|
||||||
|
} > "$ovf_path"
|
||||||
|
|
||||||
|
local idx file_id disk_id file_name file_size capacity
|
||||||
|
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||||
|
file_id="file$((idx + 1))"
|
||||||
|
file_name="${EXPORT_DISK_FILES[$idx]}"
|
||||||
|
file_size=$(stat -c%s "$WORK_DIR/$file_name")
|
||||||
|
echo " <File ovf:id=\"$file_id\" ovf:href=\"$file_name\" ovf:size=\"$file_size\"/>" >> "$ovf_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
echo ' </References>'
|
||||||
|
echo ' <DiskSection>'
|
||||||
|
echo ' <Info>Virtual disk information</Info>'
|
||||||
|
} >> "$ovf_path"
|
||||||
|
|
||||||
|
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||||
|
file_id="file$((idx + 1))"
|
||||||
|
disk_id="vmdisk$((idx + 1))"
|
||||||
|
capacity="${DISK_VSIZES[$idx]}"
|
||||||
|
[[ -z "$capacity" || "$capacity" -le 0 ]] && capacity=$(stat -c%s "$WORK_DIR/${EXPORT_DISK_FILES[$idx]}")
|
||||||
|
echo " <Disk ovf:diskId=\"$disk_id\" ovf:fileRef=\"$file_id\" ovf:capacity=\"$capacity\" ovf:capacityAllocationUnits=\"byte\" ovf:format=\"http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized\"/>" >> "$ovf_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
echo ' </DiskSection>'
|
||||||
|
echo " <VirtualSystem ovf:id=\"$(sanitize_name "$VM_NAME")\">"
|
||||||
|
echo ' <Info>A virtual machine</Info>'
|
||||||
|
echo " <Name>$vm_name_xml</Name>"
|
||||||
|
echo ' <OperatingSystemSection ovf:id="94">'
|
||||||
|
echo ' <Info>Guest operating system</Info>'
|
||||||
|
echo " <Description>$os_desc_xml</Description>"
|
||||||
|
echo ' </OperatingSystemSection>'
|
||||||
|
echo ' <VirtualHardwareSection>'
|
||||||
|
echo ' <Info>Virtual hardware requirements</Info>'
|
||||||
|
echo ' <System>'
|
||||||
|
echo ' <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>'
|
||||||
|
echo ' <vssd:InstanceID>0</vssd:InstanceID>'
|
||||||
|
echo ' <vssd:VirtualSystemIdentifier>vm</vssd:VirtualSystemIdentifier>'
|
||||||
|
echo ' <vssd:VirtualSystemType>vmx-14</vssd:VirtualSystemType>'
|
||||||
|
echo ' </System>'
|
||||||
|
echo ' <Item>'
|
||||||
|
echo ' <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>'
|
||||||
|
echo ' <rasd:Description>Number of Virtual CPUs</rasd:Description>'
|
||||||
|
echo ' <rasd:ElementName>Virtual CPU(s)</rasd:ElementName>'
|
||||||
|
echo ' <rasd:InstanceID>1</rasd:InstanceID>'
|
||||||
|
echo " <rasd:VirtualQuantity>$VM_VCPUS</rasd:VirtualQuantity>"
|
||||||
|
echo ' <rasd:ResourceType>3</rasd:ResourceType>'
|
||||||
|
echo ' </Item>'
|
||||||
|
echo ' <Item>'
|
||||||
|
echo ' <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>'
|
||||||
|
echo ' <rasd:Description>Memory Size</rasd:Description>'
|
||||||
|
echo ' <rasd:ElementName>Memory</rasd:ElementName>'
|
||||||
|
echo ' <rasd:InstanceID>2</rasd:InstanceID>'
|
||||||
|
echo " <rasd:VirtualQuantity>$VM_MEMORY</rasd:VirtualQuantity>"
|
||||||
|
echo ' <rasd:ResourceType>4</rasd:ResourceType>'
|
||||||
|
echo ' </Item>'
|
||||||
|
echo ' <Item>'
|
||||||
|
echo ' <rasd:Address>0</rasd:Address>'
|
||||||
|
echo ' <rasd:Description>SCSI Controller</rasd:Description>'
|
||||||
|
echo ' <rasd:ElementName>SCSI Controller 0</rasd:ElementName>'
|
||||||
|
echo ' <rasd:InstanceID>10</rasd:InstanceID>'
|
||||||
|
echo ' <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>'
|
||||||
|
echo ' <rasd:ResourceType>6</rasd:ResourceType>'
|
||||||
|
echo ' </Item>'
|
||||||
|
} >> "$ovf_path"
|
||||||
|
|
||||||
|
for idx in "${!EXPORT_DISK_FILES[@]}"; do
|
||||||
|
disk_id="vmdisk$((idx + 1))"
|
||||||
|
echo ' <Item>' >> "$ovf_path"
|
||||||
|
echo " <rasd:AddressOnParent>$idx</rasd:AddressOnParent>" >> "$ovf_path"
|
||||||
|
echo ' <rasd:Description>Hard disk</rasd:Description>' >> "$ovf_path"
|
||||||
|
echo " <rasd:ElementName>Hard disk $((idx + 1))</rasd:ElementName>" >> "$ovf_path"
|
||||||
|
echo " <rasd:HostResource>ovf:/disk/$disk_id</rasd:HostResource>" >> "$ovf_path"
|
||||||
|
echo " <rasd:InstanceID>$((200 + idx + 1))</rasd:InstanceID>" >> "$ovf_path"
|
||||||
|
echo ' <rasd:Parent>10</rasd:Parent>' >> "$ovf_path"
|
||||||
|
echo ' <rasd:ResourceType>17</rasd:ResourceType>' >> "$ovf_path"
|
||||||
|
echo ' </Item>' >> "$ovf_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$NET_COUNT" -gt 0 ]]; then
|
||||||
|
local n
|
||||||
|
for n in $(seq 1 "$NET_COUNT"); do
|
||||||
|
{
|
||||||
|
echo ' <Item>'
|
||||||
|
echo ' <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>'
|
||||||
|
echo ' <rasd:Connection>VM Network</rasd:Connection>'
|
||||||
|
echo " <rasd:ElementName>Ethernet adapter $n</rasd:ElementName>"
|
||||||
|
echo " <rasd:InstanceID>$((300 + n))</rasd:InstanceID>"
|
||||||
|
echo ' <rasd:ResourceType>10</rasd:ResourceType>'
|
||||||
|
echo ' </Item>'
|
||||||
|
} >> "$ovf_path"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo ' </VirtualHardwareSection>'
|
||||||
|
echo ' </VirtualSystem>'
|
||||||
|
echo '</Envelope>'
|
||||||
|
} >> "$ovf_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_manifest() {
|
||||||
|
local mf_path="$1"
|
||||||
|
shift
|
||||||
|
local files=("$@")
|
||||||
|
: > "$mf_path"
|
||||||
|
|
||||||
|
local f hash
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
hash=$(sha1sum "$WORK_DIR/$f" | awk '{print $1}')
|
||||||
|
echo "SHA1($f)= $hash" >> "$mf_path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
print_export_result() {
|
||||||
|
local mode="$1"
|
||||||
|
local path="$2"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
msg_title "$(translate "Export Summary")"
|
||||||
|
|
||||||
|
msg_ok "$(translate "VM:") ${VMID} — ${VM_NAME}"
|
||||||
|
msg_ok "$(translate "vCPUs:") ${VM_VCPUS} $(translate "Memory:") ${VM_MEMORY} MB $(translate "Disks exported:") ${DISK_COUNT}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$mode" == "ova" ]]; then
|
||||||
|
local ova_size ova_sha1
|
||||||
|
ova_size=$(stat -c%s "$path" 2>/dev/null || echo 0)
|
||||||
|
ova_sha1=$(sha1sum "$path" 2>/dev/null | awk '{print $1}')
|
||||||
|
msg_ok "$(translate "Format:") OVA — $(translate "single portable archive")"
|
||||||
|
msg_ok "$(translate "File:") $path"
|
||||||
|
msg_ok "$(translate "Size:") $(human_bytes "$ova_size") (${ova_size} $(translate "bytes"))"
|
||||||
|
msg_ok "SHA1: ${ova_sha1}"
|
||||||
|
else
|
||||||
|
local fsz total_size=0 f
|
||||||
|
msg_ok "$(translate "Format:") OVF — $(translate "descriptor + VMDK files")"
|
||||||
|
msg_ok "$(translate "Directory:") $path"
|
||||||
|
for f in "${EXPORT_DISK_FILES[@]}"; do
|
||||||
|
fsz=$(stat -c%s "$path/$f" 2>/dev/null || echo 0)
|
||||||
|
total_size=$((total_size + fsz))
|
||||||
|
msg_info2 " ${f} [$(human_bytes "$fsz")]"
|
||||||
|
done
|
||||||
|
msg_ok "$(translate "Total size:") $(human_bytes "$total_size")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
msg_ok "$(translate "Compatible with:") VMware ESXi 6.7+ (vmx-14) · VMware Workstation / Fusion · VirtualBox · Proxmox VE"
|
||||||
|
msg_info2 "$(translate "Not portable:") $(translate "PCI passthrough, TPM state, cloud-init configuration, Proxmox hooks")"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
run_export() {
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Export VM to OVA or OVF")"
|
||||||
|
|
||||||
|
msg_ok "$(translate "VM selected:") $VMID ($VM_NAME)"
|
||||||
|
msg_ok "$(translate "Export mode:") ${EXPORT_MODE^^}"
|
||||||
|
msg_ok "$(translate "Destination:") $DEST_DIR"
|
||||||
|
|
||||||
|
local ts vm_safe base_name
|
||||||
|
ts=$(date +%Y%m%d_%H%M%S)
|
||||||
|
vm_safe=$(sanitize_name "$VM_NAME")
|
||||||
|
base_name="${vm_safe}-${VMID}-${ts}"
|
||||||
|
|
||||||
|
WORK_DIR=$(mktemp -d "$DEST_DIR/.ovaovf-${base_name}-XXXXXX")
|
||||||
|
if [[ ! -d "$WORK_DIR" ]]; then
|
||||||
|
msg_error "$(translate "Could not create temporary working directory.")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_ok "$(translate "Working directory:") $WORK_DIR"
|
||||||
|
|
||||||
|
# Clean up temp dir on unexpected exit (Ctrl+C, unhandled error, etc.)
|
||||||
|
trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
declare -ga EXPORT_DISK_FILES
|
||||||
|
EXPORT_DISK_FILES=()
|
||||||
|
|
||||||
|
local i src dst disk_name
|
||||||
|
for i in "${!DISK_SRCS[@]}"; do
|
||||||
|
src="${DISK_SRCS[$i]}"
|
||||||
|
disk_name="${base_name}-disk$((i + 1)).vmdk"
|
||||||
|
dst="$WORK_DIR/$disk_name"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
msg_info "$(translate "Converting disk") $((i + 1))/$DISK_COUNT: ${DISK_SLOTS[$i]}"
|
||||||
|
msg_info2 "$(translate "Source:") $src"
|
||||||
|
|
||||||
|
if ! qemu-img convert -p -O vmdk -o subformat=streamOptimized "$src" "$dst"; then
|
||||||
|
msg_error "$(translate "Disk conversion failed for") ${DISK_SLOTS[$i]}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPORT_DISK_FILES+=("$disk_name")
|
||||||
|
msg_ok "$(translate "Converted:") $disk_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
local ovf_file mf_file
|
||||||
|
ovf_file="${base_name}.ovf"
|
||||||
|
mf_file="${base_name}.mf"
|
||||||
|
|
||||||
|
msg_info "$(translate "Generating OVF descriptor...")"
|
||||||
|
generate_ovf_descriptor "$WORK_DIR/$ovf_file"
|
||||||
|
|
||||||
|
msg_info "$(translate "Generating manifest...")"
|
||||||
|
generate_manifest "$WORK_DIR/$mf_file" "$ovf_file" "${EXPORT_DISK_FILES[@]}"
|
||||||
|
|
||||||
|
if [[ "$EXPORT_MODE" == "ovf" ]]; then
|
||||||
|
local final_dir="$DEST_DIR/${base_name}-ovf"
|
||||||
|
rm -rf "$final_dir"
|
||||||
|
trap - EXIT
|
||||||
|
mv "$WORK_DIR" "$final_dir"
|
||||||
|
|
||||||
|
print_export_result "ovf" "$final_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ova_path="$DEST_DIR/${base_name}.ova"
|
||||||
|
msg_info "$(translate "Packaging OVA file...")"
|
||||||
|
|
||||||
|
if ! tar -C "$WORK_DIR" -cf "$ova_path" "$ovf_file" "$mf_file" "${EXPORT_DISK_FILES[@]}"; then
|
||||||
|
msg_error "$(translate "Failed to create OVA archive.")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap - EXIT
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
|
||||||
|
print_export_result "ova" "$ova_path"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
require_cmd dialog || exit 1
|
||||||
|
require_cmd qm || exit 1
|
||||||
|
require_cmd pvesm || exit 1
|
||||||
|
require_cmd qemu-img || exit 1
|
||||||
|
require_cmd tar || exit 1
|
||||||
|
require_cmd sha1sum || exit 1
|
||||||
|
|
||||||
|
if ! command -v pveversion >/dev/null 2>&1; then
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
select_vm || exit 0
|
||||||
|
ensure_vm_stopped || exit 0
|
||||||
|
select_export_mode || exit 0
|
||||||
|
select_destination_dir || exit 0
|
||||||
|
|
||||||
|
get_vm_metadata || {
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "Could not read VM configuration.")" 8 60
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_vm_disks || {
|
||||||
|
dialog --backtitle "ProxMenux" --title "$(translate "No exportable disks")" \
|
||||||
|
--msgbox "$(translate "No exportable VM disks were found (CD-ROM/cloud-init are excluded).")" 9 80
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_destination_space || exit 0
|
||||||
|
|
||||||
|
if ! dialog --backtitle "ProxMenux" --title "$(translate "Confirm export")" --yesno \
|
||||||
|
"$(translate "VM:") $VMID ($VM_NAME)\n$(translate "Disks to export:") $DISK_COUNT\n$(translate "Format:") ${EXPORT_MODE^^}\n$(translate "Destination:") $DEST_DIR\n\n$(translate "Continue?")" 13 80; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if run_export; then
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate "Press Enter to return...")"
|
||||||
|
read -r
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
msg_error "$(translate "Export failed.")"
|
||||||
|
msg_info2 "$(translate "Temporary working directory (if present):") $WORK_DIR"
|
||||||
|
msg_success "$(translate "Press Enter to return...")"
|
||||||
|
read -r
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
612
scripts/utilities/import_vm_ova_ovf.sh
Executable file
612
scripts/utilities/import_vm_ova_ovf.sh
Executable file
@@ -0,0 +1,612 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==========================================================
|
||||||
|
# ProxMenux - Import VM from OVA or OVF
|
||||||
|
# ==========================================================
|
||||||
|
# Author : MacRimi
|
||||||
|
# Copyright : (c) 2024 MacRimi
|
||||||
|
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
|
||||||
|
# Version : 1.0
|
||||||
|
# Last Updated: 10/04/2026
|
||||||
|
# ==========================================================
|
||||||
|
# Description:
|
||||||
|
# Imports a virtual machine from an OVA or OVF package into Proxmox VE.
|
||||||
|
# Compatible with exports from VMware ESXi, VMware Workstation/Fusion,
|
||||||
|
# VirtualBox, and Proxmox itself (via export_vm_ova_ovf).
|
||||||
|
#
|
||||||
|
# What is imported:
|
||||||
|
# - Disk images (VMDK converted to the target storage format)
|
||||||
|
# - CPU and memory settings
|
||||||
|
# - Number of network interfaces
|
||||||
|
# - VM name and OS type hint
|
||||||
|
#
|
||||||
|
# What requires manual review after import:
|
||||||
|
# - Network bridge assignment (vmbr0 assigned by default)
|
||||||
|
# - NIC model (e1000 by default — change to VirtIO if guest supports it)
|
||||||
|
# - Firmware (BIOS/UEFI — must match what the original VM used)
|
||||||
|
# - VirtIO/qemu-guest-agent installation inside the guest (especially from ESXi)
|
||||||
|
# - PCI passthrough, TPM, cloud-init, snapshots — not portable in OVF/OVA
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
BASE_DIR="/usr/local/share/proxmenux"
|
||||||
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||||
|
|
||||||
|
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
|
||||||
|
load_language
|
||||||
|
initialize_cache
|
||||||
|
|
||||||
|
BACKTITLE="ProxMenux"
|
||||||
|
UI_MENU_H=20
|
||||||
|
UI_MENU_W=84
|
||||||
|
UI_MENU_LIST_H=10
|
||||||
|
|
||||||
|
# Globals populated during the flow
|
||||||
|
SOURCE_FILE=""
|
||||||
|
OVF_FILE=""
|
||||||
|
OVF_DIR=""
|
||||||
|
WORK_DIR=""
|
||||||
|
|
||||||
|
OVF_VM_NAME=""
|
||||||
|
OVF_VCPUS=1
|
||||||
|
OVF_MEMORY_MB=1024
|
||||||
|
OVF_DISK_FILES=()
|
||||||
|
OVF_DISK_CAPACITIES=()
|
||||||
|
OVF_NET_COUNT=0
|
||||||
|
OVF_OS_TYPE="other"
|
||||||
|
|
||||||
|
NEW_VMID=""
|
||||||
|
NEW_VM_NAME=""
|
||||||
|
STORAGE=""
|
||||||
|
BRIDGE="vmbr0"
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# HELPERS
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
human_bytes() {
|
||||||
|
local bytes="$1"
|
||||||
|
local units=("B" "KB" "MB" "GB" "TB")
|
||||||
|
local idx=0 value="$bytes"
|
||||||
|
[[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]] && { echo "N/A"; return; }
|
||||||
|
while [[ "$value" -ge 1024 && "$idx" -lt 4 ]]; do
|
||||||
|
value=$((value / 1024))
|
||||||
|
idx=$((idx + 1))
|
||||||
|
done
|
||||||
|
echo "${value}${units[$idx]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# SELECT SOURCE FILE
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
select_source_file() {
|
||||||
|
local dump_dir="/var/lib/vz/dump"
|
||||||
|
local iso_dir="/var/lib/vz/template/iso"
|
||||||
|
local options=(
|
||||||
|
"1" "$dump_dir"
|
||||||
|
"2" "$iso_dir"
|
||||||
|
"M" "$(translate "Manual path entry")"
|
||||||
|
)
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local choice
|
||||||
|
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Import VM from OVA or OVF")" \
|
||||||
|
--menu "$(translate "Where is the OVA/OVF file located?")" \
|
||||||
|
14 82 4 "${options[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$choice" ]] || return 1
|
||||||
|
|
||||||
|
local search_dir=""
|
||||||
|
case "$choice" in
|
||||||
|
1) search_dir="$dump_dir" ;;
|
||||||
|
2) search_dir="$iso_dir" ;;
|
||||||
|
M)
|
||||||
|
search_dir=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Custom Path")" \
|
||||||
|
--inputbox "\n$(translate "Enter directory containing OVA/OVF files:")" \
|
||||||
|
10 82 "/var/lib/vz/dump" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$search_dir" ]] || continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ ! -d "$search_dir" ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "Not found")" \
|
||||||
|
--msgbox "$(translate "Directory does not exist:")\n$search_dir" 8 74
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local file_opts=()
|
||||||
|
while IFS= read -r f; do
|
||||||
|
local fname size_h
|
||||||
|
fname=$(basename "$f")
|
||||||
|
size_h=$(du -sh "$f" 2>/dev/null | awk '{print $1}')
|
||||||
|
file_opts+=("$f" "$fname [$size_h]")
|
||||||
|
done < <(find "$search_dir" -maxdepth 2 \( -name "*.ova" -o -name "*.ovf" \) 2>/dev/null | sort)
|
||||||
|
|
||||||
|
if [[ ${#file_opts[@]} -eq 0 ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "No files found")" \
|
||||||
|
--msgbox "$(translate "No .ova or .ovf files found in:")\n\n$search_dir" 10 74
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local selected
|
||||||
|
selected=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Select OVA/OVF file")" \
|
||||||
|
--menu "$(translate "Select the file to import:")" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"${file_opts[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
[[ -n "$selected" ]] || continue
|
||||||
|
|
||||||
|
SOURCE_FILE="$selected"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# EXTRACT OVA / LOCATE OVF
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
prepare_ovf() {
|
||||||
|
local src="$SOURCE_FILE"
|
||||||
|
local ext="${src##*.}"
|
||||||
|
ext="${ext,,}"
|
||||||
|
|
||||||
|
if [[ "$ext" == "ova" ]]; then
|
||||||
|
WORK_DIR=$(mktemp -d "/tmp/.proxmenux-import-XXXXXX")
|
||||||
|
trap 'rm -rf "$WORK_DIR" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
msg_info "$(translate "Extracting OVA archive...")"
|
||||||
|
if ! tar xf "$src" -C "$WORK_DIR" 2>/dev/null; then
|
||||||
|
msg_error "$(translate "Failed to extract OVA file:") $src"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
msg_ok "$(translate "Archive extracted.")"
|
||||||
|
|
||||||
|
OVF_FILE=$(find "$WORK_DIR" -maxdepth 2 -name "*.ovf" | head -1)
|
||||||
|
if [[ -z "$OVF_FILE" ]]; then
|
||||||
|
msg_error "$(translate "No .ovf descriptor found inside OVA.")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
OVF_DIR=$(dirname "$OVF_FILE")
|
||||||
|
|
||||||
|
elif [[ "$ext" == "ovf" ]]; then
|
||||||
|
OVF_FILE="$src"
|
||||||
|
OVF_DIR=$(dirname "$src")
|
||||||
|
WORK_DIR=""
|
||||||
|
|
||||||
|
else
|
||||||
|
msg_error "$(translate "Unsupported format. Only .ova and .ovf files are supported.")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# PARSE OVF XML
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
parse_ovf() {
|
||||||
|
local ovf_file="$1"
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(awk '
|
||||||
|
BEGIN {
|
||||||
|
in_item=0; rt=""; qty=""
|
||||||
|
file_count=0; cap_count=0; net_count=0
|
||||||
|
name=""; vcpu="1"; mem="1024"; os=""
|
||||||
|
}
|
||||||
|
|
||||||
|
/<[Nn]ame>/ {
|
||||||
|
match($0, /<[Nn]ame>([^<]+)</, a)
|
||||||
|
if (a[1] != "" && name == "") name = a[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/[Ll]inux/ && /[Dd]escription|[Oo]perating/ { if (os == "") os="linux" }
|
||||||
|
/[Ww]indows/ && /[Dd]escription|[Oo]perating/ { if (os == "") os="windows" }
|
||||||
|
|
||||||
|
/ovf:href=|href=/ {
|
||||||
|
n = split($0, parts, /"/)
|
||||||
|
for (i=1; i<=n; i++) {
|
||||||
|
if (parts[i] ~ /\.(vmdk|qcow2|img|raw)$/) {
|
||||||
|
files[file_count++] = parts[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/[Cc]apacity=/ {
|
||||||
|
match($0, /[Cc]apacity="([0-9]+)"/, a)
|
||||||
|
if (a[1]+0 > 0) caps[cap_count++] = a[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/<Item>|<Item / { in_item=1; rt=""; qty="" }
|
||||||
|
/<\/Item>/ {
|
||||||
|
if (in_item) {
|
||||||
|
if (rt=="3" && qty ~ /^[0-9]+$/) vcpu=qty
|
||||||
|
if (rt=="4" && qty ~ /^[0-9]+$/) mem=qty
|
||||||
|
if (rt=="10") net_count++
|
||||||
|
}
|
||||||
|
in_item=0
|
||||||
|
}
|
||||||
|
/ResourceType>/ {
|
||||||
|
match($0, /ResourceType>([0-9]+)</, a); rt=a[1]
|
||||||
|
}
|
||||||
|
/VirtualQuantity>/ {
|
||||||
|
match($0, /VirtualQuantity>([0-9]+)</, a); qty=a[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
||||||
|
if (name == "") name = "imported-vm"
|
||||||
|
print "NAME=" name
|
||||||
|
print "VCPU=" vcpu
|
||||||
|
print "MEM=" mem
|
||||||
|
print "NET=" net_count
|
||||||
|
print "OS=" os
|
||||||
|
for (i=0; i<file_count; i++) print "FILE=" files[i]
|
||||||
|
for (i=0; i<cap_count; i++) print "CAP=" caps[i]
|
||||||
|
}
|
||||||
|
' "$ovf_file")
|
||||||
|
|
||||||
|
OVF_VM_NAME=$(echo "$result" | grep '^NAME=' | cut -d= -f2-)
|
||||||
|
OVF_VCPUS=$(echo "$result" | grep '^VCPU=' | cut -d= -f2-)
|
||||||
|
OVF_MEMORY_MB=$(echo "$result" | grep '^MEM=' | cut -d= -f2-)
|
||||||
|
OVF_NET_COUNT=$(echo "$result" | grep '^NET=' | cut -d= -f2-)
|
||||||
|
OVF_OS_TYPE=$(echo "$result" | grep '^OS=' | cut -d= -f2-)
|
||||||
|
|
||||||
|
OVF_DISK_FILES=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
OVF_DISK_FILES+=("${line#FILE=}")
|
||||||
|
done < <(echo "$result" | grep '^FILE=')
|
||||||
|
|
||||||
|
OVF_DISK_CAPACITIES=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
OVF_DISK_CAPACITIES+=("${line#CAP=}")
|
||||||
|
done < <(echo "$result" | grep '^CAP=')
|
||||||
|
|
||||||
|
[[ -z "$OVF_VM_NAME" ]] && OVF_VM_NAME="imported-vm"
|
||||||
|
[[ ! "$OVF_VCPUS" =~ ^[0-9]+$ ]] && OVF_VCPUS=1
|
||||||
|
[[ ! "$OVF_MEMORY_MB" =~ ^[0-9]+$ ]] && OVF_MEMORY_MB=1024
|
||||||
|
[[ ! "$OVF_NET_COUNT" =~ ^[0-9]+$ ]] && OVF_NET_COUNT=0
|
||||||
|
|
||||||
|
case "$OVF_OS_TYPE" in
|
||||||
|
linux) OVF_OS_TYPE="l26" ;;
|
||||||
|
windows) OVF_OS_TYPE="win10" ;;
|
||||||
|
*) OVF_OS_TYPE="other" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ ${#OVF_DISK_FILES[@]} -gt 0 ]] || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# SELECT IMPORT OPTIONS (dialogs — no terminal output)
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
select_import_options() {
|
||||||
|
# VMID
|
||||||
|
local suggested_vmid
|
||||||
|
suggested_vmid=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
NEW_VMID=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "VM ID")" \
|
||||||
|
--inputbox "\n$(translate "Enter the VMID for the new VM:") ($(translate "suggested:") $suggested_vmid)" \
|
||||||
|
10 72 "$suggested_vmid" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$NEW_VMID" ]] || return 1
|
||||||
|
|
||||||
|
if ! [[ "$NEW_VMID" =~ ^[0-9]+$ ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "Invalid VMID")" \
|
||||||
|
--msgbox "$(translate "VMID must be a number.")" 8 50
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if qm status "$NEW_VMID" &>/dev/null; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "VMID in use")" \
|
||||||
|
--msgbox "$(translate "VMID $NEW_VMID is already in use. Please choose another.")" 8 60
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
# VM Name
|
||||||
|
NEW_VM_NAME=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "VM Name")" \
|
||||||
|
--inputbox "\n$(translate "Enter name for the imported VM:")" \
|
||||||
|
10 72 "$OVF_VM_NAME" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$NEW_VM_NAME" ]] || return 1
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
local storage_list storage_opts=()
|
||||||
|
storage_list=$(pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}')
|
||||||
|
if [[ -z "$storage_list" ]]; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "No storage")" \
|
||||||
|
--msgbox "$(translate "No storage volumes available for VM images.")" 8 60
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
while IFS= read -r s; do
|
||||||
|
storage_opts+=("$s" "")
|
||||||
|
done <<< "$storage_list"
|
||||||
|
|
||||||
|
STORAGE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Select Storage")" \
|
||||||
|
--menu "$(translate "Select storage for imported disk(s):")" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"${storage_opts[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$STORAGE" ]] || return 1
|
||||||
|
|
||||||
|
# Network bridge
|
||||||
|
local bridge_opts=()
|
||||||
|
while IFS= read -r br; do
|
||||||
|
[[ -n "$br" ]] && bridge_opts+=("$br" "")
|
||||||
|
done < <(ip link show type bridge 2>/dev/null | awk -F': ' '/^[0-9]+:/{print $2}' | sed 's/@.*//')
|
||||||
|
|
||||||
|
if [[ ${#bridge_opts[@]} -gt 1 ]]; then
|
||||||
|
BRIDGE=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Network Bridge")" \
|
||||||
|
--menu "$(translate "Select bridge for network interface(s):")" \
|
||||||
|
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
|
||||||
|
"${bridge_opts[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
[[ -n "$BRIDGE" ]] || return 1
|
||||||
|
elif [[ ${#bridge_opts[@]} -eq 1 ]]; then
|
||||||
|
BRIDGE="${bridge_opts[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# CONFIRM BEFORE IMPORT (dialog)
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
confirm_import() {
|
||||||
|
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||||
|
local disk_info="" i
|
||||||
|
|
||||||
|
for i in "${!OVF_DISK_FILES[@]}"; do
|
||||||
|
local cap="${OVF_DISK_CAPACITIES[$i]:-0}"
|
||||||
|
disk_info+="\n disk$((i+1)): ${OVF_DISK_FILES[$i]} ($(human_bytes "$cap"))"
|
||||||
|
done
|
||||||
|
|
||||||
|
local msg
|
||||||
|
msg="$(translate "New VM:") $NEW_VMID ($NEW_VM_NAME)\n"
|
||||||
|
msg+="$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "OS type:") $OVF_OS_TYPE\n"
|
||||||
|
msg+="$(translate "NICs:") $OVF_NET_COUNT $(translate "Bridge:") $BRIDGE\n"
|
||||||
|
msg+="$(translate "Storage:") $STORAGE\n"
|
||||||
|
msg+="$(translate "Disks to import:") $disk_count${disk_info}\n\n"
|
||||||
|
msg+="$(translate "Continue?")"
|
||||||
|
|
||||||
|
dialog --backtitle "$BACKTITLE" \
|
||||||
|
--title "$(translate "Confirm Import")" \
|
||||||
|
--yesno "$msg" 18 84 3>&1 1>&2 2>&3
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# RUN IMPORT (terminal output only — no dialogs)
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
run_import() {
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Import VM from OVA or OVF")"
|
||||||
|
|
||||||
|
msg_ok "$(translate "VM:") $NEW_VMID ($NEW_VM_NAME)"
|
||||||
|
msg_ok "$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "OS:") $OVF_OS_TYPE"
|
||||||
|
msg_ok "$(translate "Storage:") $STORAGE $(translate "Bridge:") $BRIDGE $(translate "NICs:") $OVF_NET_COUNT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Create VM shell
|
||||||
|
msg_info "$(translate "Creating VM...")"
|
||||||
|
if ! qm create "$NEW_VMID" \
|
||||||
|
--name "$NEW_VM_NAME" \
|
||||||
|
--memory "$OVF_MEMORY_MB" \
|
||||||
|
--cores "$OVF_VCPUS" \
|
||||||
|
--ostype "$OVF_OS_TYPE" \
|
||||||
|
--scsihw lsi \
|
||||||
|
--net0 "e1000,bridge=$BRIDGE" \
|
||||||
|
&>/dev/null; then
|
||||||
|
msg_error "$(translate "Failed to create VM") $NEW_VMID"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
msg_ok "$(translate "VM shell created:") $NEW_VMID"
|
||||||
|
|
||||||
|
# Add extra NICs (net0 already created above)
|
||||||
|
local n
|
||||||
|
for n in $(seq 1 $((OVF_NET_COUNT - 1))); do
|
||||||
|
qm set "$NEW_VMID" "--net${n}" "e1000,bridge=$BRIDGE" &>/dev/null || true
|
||||||
|
done
|
||||||
|
[[ "$OVF_NET_COUNT" -gt 1 ]] && msg_ok "$(translate "Network interfaces added:") $OVF_NET_COUNT"
|
||||||
|
|
||||||
|
# 2. Import disks
|
||||||
|
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||||
|
local i disk_file src_path
|
||||||
|
local TEMP_STATUS_FILE TEMP_DISK_FILE
|
||||||
|
|
||||||
|
for i in "${!OVF_DISK_FILES[@]}"; do
|
||||||
|
disk_file="${OVF_DISK_FILES[$i]}"
|
||||||
|
src_path="$OVF_DIR/$disk_file"
|
||||||
|
|
||||||
|
if [[ ! -f "$src_path" ]]; then
|
||||||
|
msg_error "$(translate "Disk file not found:") $src_path"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
msg_info "$(translate "Importing disk") $((i + 1))/$disk_count: $disk_file"
|
||||||
|
msg_info2 "$(translate "Source:") $src_path"
|
||||||
|
|
||||||
|
TEMP_STATUS_FILE=$(mktemp)
|
||||||
|
TEMP_DISK_FILE=$(mktemp)
|
||||||
|
|
||||||
|
(
|
||||||
|
qm importdisk "$NEW_VMID" "$src_path" "$STORAGE" 2>&1
|
||||||
|
echo $? > "$TEMP_STATUS_FILE"
|
||||||
|
) | while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ transferred ]]; then
|
||||||
|
local pct
|
||||||
|
pct=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
|
||||||
|
[[ -n "$pct" ]] && echo -ne "\r${TAB}${BL}- $(translate "Importing:") $disk_file -${CL} ${pct}%"
|
||||||
|
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
|
||||||
|
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo -ne "\n"
|
||||||
|
|
||||||
|
local import_status
|
||||||
|
import_status=$(cat "$TEMP_STATUS_FILE" 2>/dev/null)
|
||||||
|
rm -f "$TEMP_STATUS_FILE"
|
||||||
|
[[ -z "$import_status" ]] && import_status=1
|
||||||
|
|
||||||
|
if [[ "$import_status" -ne 0 ]]; then
|
||||||
|
msg_error "$(translate "Import failed for:") $disk_file"
|
||||||
|
rm -f "$TEMP_DISK_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Locate the unused disk entry in VM config
|
||||||
|
local unused_id unused_disk
|
||||||
|
unused_id=$(qm config "$NEW_VMID" | grep -E '^unused[0-9]+:' | tail -1 | cut -d: -f1)
|
||||||
|
unused_disk=$(qm config "$NEW_VMID" | grep -E '^unused[0-9]+:' | tail -1 | cut -d: -f2- | xargs)
|
||||||
|
rm -f "$TEMP_DISK_FILE"
|
||||||
|
|
||||||
|
if [[ -z "$unused_disk" ]]; then
|
||||||
|
msg_error "$(translate "Could not locate imported disk in VM config.")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Attach to scsi slot i
|
||||||
|
if ! qm set "$NEW_VMID" "--scsi${i}" "$unused_disk" &>/dev/null; then
|
||||||
|
msg_error "$(translate "Failed to attach disk as scsi$i.")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove the unused marker
|
||||||
|
[[ -n "$unused_id" ]] && qm set "$NEW_VMID" --delete "$unused_id" &>/dev/null || true
|
||||||
|
|
||||||
|
msg_ok "$(translate "Disk attached as:") scsi${i} (${disk_file})"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. Set boot disk
|
||||||
|
echo ""
|
||||||
|
msg_info "$(translate "Configuring boot order...")"
|
||||||
|
if qm set "$NEW_VMID" --boot c --bootdisk "scsi0" &>/dev/null; then
|
||||||
|
msg_ok "$(translate "Boot disk:") scsi0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# PRINT FINAL RESULT
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
print_import_result() {
|
||||||
|
local disk_count="${#OVF_DISK_FILES[@]}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
msg_title "$(translate "Import Summary")"
|
||||||
|
|
||||||
|
msg_ok "$(translate "VM imported successfully")"
|
||||||
|
msg_ok "$(translate "VM ID:") $NEW_VMID $(translate "Name:") $NEW_VM_NAME"
|
||||||
|
msg_ok "$(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB $(translate "Disks:") $disk_count"
|
||||||
|
msg_ok "$(translate "Storage:") $STORAGE $(translate "Bridge:") $BRIDGE $(translate "NICs:") $OVF_NET_COUNT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
msg_ok "$(translate "To start the VM:") qm start $NEW_VMID"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
msg_title "$(translate "Manual steps recommended after import")"
|
||||||
|
msg_info2 "$(translate "Network :") $(translate "Verify bridge assignment and NIC model — change to VirtIO if guest drivers are available")"
|
||||||
|
msg_info2 "$(translate "Firmware :") $(translate "Check BIOS/UEFI in Hardware > BIOS — must match what the original VM used")"
|
||||||
|
msg_info2 "$(translate "Drivers :") $(translate "If imported from ESXi: install qemu-guest-agent inside the guest OS")"
|
||||||
|
msg_info2 "$(translate "Display :") $(translate "Set Display > Graphic card (VGA, SPICE or VirtIO) to match the guest")"
|
||||||
|
msg_info2 "$(translate "OS type :") $(translate "Verify Options > OS Type — currently set to:") $OVF_OS_TYPE"
|
||||||
|
echo ""
|
||||||
|
msg_info2 "$(translate "Not imported:") $(translate "PCI passthrough, TPM state, cloud-init, snapshots, Proxmox-specific hooks")"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# MAIN
|
||||||
|
# -------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if ! command -v pveversion >/dev/null 2>&1; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "Error")" \
|
||||||
|
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in dialog qm pvesm qemu-img tar; do
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
dialog --backtitle "$BACKTITLE" --title "$(translate "Missing dependency")" \
|
||||||
|
--msgbox "$(translate "Required command not found:") $cmd" 8 60
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 1: pick the OVA/OVF file (dialog)
|
||||||
|
select_source_file || exit 0
|
||||||
|
|
||||||
|
# Step 2: extract + parse (terminal output)
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate "Import VM from OVA or OVF")"
|
||||||
|
|
||||||
|
msg_ok "$(translate "Source:") $SOURCE_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
prepare_ovf || {
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate "Press Enter to return...")"
|
||||||
|
read -r
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info "$(translate "Parsing OVF descriptor...")"
|
||||||
|
if ! parse_ovf "$OVF_FILE"; then
|
||||||
|
msg_error "$(translate "Could not parse OVF file, or no disk image references found.")"
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate "Press Enter to return...")"
|
||||||
|
read -r
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
msg_ok "$(translate "OVF parsed:")"
|
||||||
|
msg_info2 " $(translate "Name:") $OVF_VM_NAME $(translate "vCPUs:") $OVF_VCPUS $(translate "Memory:") ${OVF_MEMORY_MB} MB"
|
||||||
|
msg_info2 " $(translate "Disks:") ${#OVF_DISK_FILES[@]} $(translate "NICs:") $OVF_NET_COUNT $(translate "OS hint:") $OVF_OS_TYPE"
|
||||||
|
|
||||||
|
# Clean screen before returning to dialogs
|
||||||
|
show_proxmenux_logo
|
||||||
|
|
||||||
|
# Step 3: configure the new VM (dialogs)
|
||||||
|
select_import_options || exit 0
|
||||||
|
|
||||||
|
# Step 4: confirm (dialog)
|
||||||
|
confirm_import || exit 0
|
||||||
|
|
||||||
|
# Step 5: do the import (terminal output only)
|
||||||
|
if run_import; then
|
||||||
|
print_import_result
|
||||||
|
msg_success "$(translate "Press Enter to return to menu...")"
|
||||||
|
read -r
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
msg_error "$(translate "Import failed. VM $NEW_VMID may be in partial state.")"
|
||||||
|
msg_info2 "$(translate "To remove partial VM:") qm destroy $NEW_VMID --destroy-unreferenced-disks 1"
|
||||||
|
echo ""
|
||||||
|
msg_success "$(translate "Press Enter to return...")"
|
||||||
|
read -r
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -80,21 +80,21 @@ function select_disk_type() {
|
|||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --backtitle "ProxMenux" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
choice=$(whiptail --backtitle "ProxMenux" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||||
"1" "$(translate "Add virtual disk")" \
|
"a" "$(translate "Add virtual disk")" \
|
||||||
"2" "$(translate "Add import disk")" \
|
"b" "$(translate "Add import disk")" \
|
||||||
"3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||||
"r" "$(translate "Reset current storage selection")" \
|
"r" "$(translate "Reset current storage selection")" \
|
||||||
"d" "$(translate "[ Finish and continue ]")" \
|
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || return 1
|
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || return 1
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1)
|
a)
|
||||||
select_virtual_disk
|
select_virtual_disk
|
||||||
;;
|
;;
|
||||||
2)
|
b)
|
||||||
select_import_disk
|
select_import_disk
|
||||||
;;
|
;;
|
||||||
3)
|
c)
|
||||||
select_controller_nvme
|
select_controller_nvme
|
||||||
;;
|
;;
|
||||||
r)
|
r)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function select_linux_iso() {
|
|||||||
--backtitle "ProxMenux" \
|
--backtitle "ProxMenux" \
|
||||||
--title "Opciones de instalación de Linux" \
|
--title "Opciones de instalación de Linux" \
|
||||||
--menu "\nSeleccione el tipo de instalación de Linux:\n\n$header" \
|
--menu "\nSeleccione el tipo de instalación de Linux:\n\n$header" \
|
||||||
18 72 10 \
|
20 70 10 \
|
||||||
1 "$(printf '%-35s│ %s' 'Instalar con metodo tradicional' 'Desde ISO oficial')" \
|
1 "$(printf '%-35s│ %s' 'Instalar con metodo tradicional' 'Desde ISO oficial')" \
|
||||||
2 "$(printf '%-35s│ %s' 'Instalar con script Cloud-Init' 'Helper Scripts')" \
|
2 "$(printf '%-35s│ %s' 'Instalar con script Cloud-Init' 'Helper Scripts')" \
|
||||||
3 "$(printf '%-35s│ %s' 'Instalar con ISO personal' 'Almacenamiento local')" \
|
3 "$(printf '%-35s│ %s' 'Instalar con ISO personal' 'Almacenamiento local')" \
|
||||||
@@ -140,7 +140,7 @@ function select_linux_iso_official() {
|
|||||||
|
|
||||||
CHOICE=$(dialog --backtitle "ProxMenux" \
|
CHOICE=$(dialog --backtitle "ProxMenux" \
|
||||||
--title "$(translate "Official Linux Distributions")" \
|
--title "$(translate "Official Linux Distributions")" \
|
||||||
--menu "$(translate "Select the Linux distribution to install:")\n\n$HEADER_TEXT" 20 80 12 \
|
--menu "$(translate "Select the Linux distribution to install:")\n\n$HEADER_TEXT" 20 70 12 \
|
||||||
"${MENU_OPTIONS[@]}" \
|
"${MENU_OPTIONS[@]}" \
|
||||||
3>&1 1>&2 2>&3)
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ local OTHER_OPTIONS=(
|
|||||||
local choice
|
local choice
|
||||||
choice=$(dialog --backtitle "ProxMenux" \
|
choice=$(dialog --backtitle "ProxMenux" \
|
||||||
--title "$(translate "Other Prebuilt Linux VMs")" \
|
--title "$(translate "Other Prebuilt Linux VMs")" \
|
||||||
--menu "\n$(translate "Select one of the ready-to-run Linux VMs:")" 18 70 10 \
|
--menu "\n$(translate "Select one of the ready-to-run Linux VMs:")" 20 70 10 \
|
||||||
"${OTHER_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
"${OTHER_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
if [[ $? -ne 0 || "$choice" == "4" ]]; then
|
if [[ $? -ne 0 || "$choice" == "4" ]]; then
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function select_windows_iso() {
|
|||||||
--backtitle "ProxMenux" \
|
--backtitle "ProxMenux" \
|
||||||
--title "Opciones de instalación de Windows" \
|
--title "Opciones de instalación de Windows" \
|
||||||
--menu "\nSeleccione el tipo de instalación de Windows:\n\n$header" \
|
--menu "\nSeleccione el tipo de instalación de Windows:\n\n$header" \
|
||||||
18 70 10 \
|
20 70 10 \
|
||||||
1 "$(printf '%-34s│ %s' 'Instalar con ISO UUP Dump' 'UUP Dump ISO creator')" \
|
1 "$(printf '%-34s│ %s' 'Instalar con ISO UUP Dump' 'UUP Dump ISO creator')" \
|
||||||
2 "$(printf '%-34s│ %s' 'Instalar con ISO personal' 'Almacenamiento local')" \
|
2 "$(printf '%-34s│ %s' 'Instalar con ISO personal' 'Almacenamiento local')" \
|
||||||
3 "Volver al menú principal" \
|
3 "Volver al menú principal" \
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
|||||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
||||||
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
||||||
fi
|
fi
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh"
|
||||||
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh"
|
||||||
|
fi
|
||||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
||||||
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
||||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
||||||
@@ -475,24 +480,24 @@ function select_disk_type() {
|
|||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||||
"1" "$(translate "Add virtual disk")" \
|
"a" "$(translate "Add virtual disk")" \
|
||||||
"2" "$(translate "Add import disk")" \
|
"b" "$(translate "Add import disk")" \
|
||||||
"3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||||
"r" "$(translate "Reset current storage selection")" \
|
"r" "$(translate "Reset current storage selection")" \
|
||||||
"d" "$(translate "[ Finish and continue ]")" \
|
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
||||||
msg_warn "$(translate "Storage plan selection cancelled.")"
|
msg_warn "$(translate "Storage plan selection cancelled.")"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1)
|
a)
|
||||||
select_virtual_disk
|
select_virtual_disk
|
||||||
;;
|
;;
|
||||||
2)
|
b)
|
||||||
select_import_disk
|
select_import_disk
|
||||||
;;
|
;;
|
||||||
3)
|
c)
|
||||||
select_controller_nvme
|
select_controller_nvme
|
||||||
;;
|
;;
|
||||||
r)
|
r)
|
||||||
@@ -575,50 +580,6 @@ function select_virtual_disk() {
|
|||||||
VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}")
|
VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}")
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_import_disk() {
|
|
||||||
msg_info "$(translate "Detecting available disks...")"
|
|
||||||
_refresh_host_storage_cache
|
|
||||||
|
|
||||||
local FREE_DISKS=()
|
|
||||||
local DISK INFO MODEL SIZE LABEL DESCRIPTION
|
|
||||||
while read -r DISK; do
|
|
||||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
|
||||||
_disk_is_host_system_used "$DISK" && continue
|
|
||||||
|
|
||||||
INFO=($(lsblk -dn -o MODEL,SIZE "$DISK"))
|
|
||||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
|
||||||
SIZE="${INFO[-1]}"
|
|
||||||
LABEL=""
|
|
||||||
if _disk_used_in_guest_configs "$DISK"; then
|
|
||||||
LABEL+=" [⚠ $(translate "In use by VM/LXC config")]"
|
|
||||||
fi
|
|
||||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
|
||||||
if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then
|
|
||||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON")
|
|
||||||
else
|
|
||||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
|
||||||
fi
|
|
||||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
|
||||||
|
|
||||||
stop_spinner
|
|
||||||
if [[ ${#FREE_DISKS[@]} -eq 0 ]]; then
|
|
||||||
whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local selected
|
|
||||||
selected=$(whiptail --title "Select Import Disks" --checklist \
|
|
||||||
"$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 78 10 \
|
|
||||||
"${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) || return 1
|
|
||||||
|
|
||||||
IMPORT_DISKS=()
|
|
||||||
local item
|
|
||||||
for item in $(echo "$selected" | tr -d '"'); do
|
|
||||||
IMPORT_DISKS+=("$item")
|
|
||||||
done
|
|
||||||
export IMPORT_DISKS
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_controller_nvme() {
|
function select_controller_nvme() {
|
||||||
local VM_STORAGE_IOMMU_REBOOT_POLICY="defer"
|
local VM_STORAGE_IOMMU_REBOOT_POLICY="defer"
|
||||||
|
|
||||||
@@ -747,7 +708,7 @@ function prompt_controller_conflict_policy() {
|
|||||||
shift
|
shift
|
||||||
local -a source_vms=("$@")
|
local -a source_vms=("$@")
|
||||||
local msg vmid vm_name st ob
|
local msg vmid vm_name st ob
|
||||||
msg="$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
msg="\n$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||||
for vmid in "${source_vms[@]}"; do
|
for vmid in "${source_vms[@]}"; do
|
||||||
vm_name=$(_vm_name_by_id "$vmid")
|
vm_name=$(_vm_name_by_id "$vmid")
|
||||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||||
@@ -757,7 +718,7 @@ function prompt_controller_conflict_policy() {
|
|||||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||||
|
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 20 80 10 \
|
||||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||||
"3" "$(translate "Skip this device")" \
|
"3" "$(translate "Skip this device")" \
|
||||||
@@ -1486,18 +1447,26 @@ if [[ "$GPU_WIZARD_APPLIED" == "yes" ]]; then
|
|||||||
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
||||||
fi
|
fi
|
||||||
local HOST_REBOOT_REQUIRED="no"
|
local HOST_REBOOT_REQUIRED="no"
|
||||||
|
local REBOOT_REASONS=""
|
||||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
HOST_REBOOT_REQUIRED="yes"
|
HOST_REBOOT_REQUIRED="yes"
|
||||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||||
|
REBOOT_REASONS+="$(translate "IOMMU has been enabled on this system.")\n"
|
||||||
fi
|
fi
|
||||||
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
||||||
HOST_REBOOT_REQUIRED="yes"
|
HOST_REBOOT_REQUIRED="yes"
|
||||||
|
REBOOT_REASONS+="$(translate "GPU passthrough changes require a host reboot.")\n"
|
||||||
fi
|
fi
|
||||||
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
||||||
if whiptail --title "$(translate "Reboot Recommended")" --yesno \
|
echo ""
|
||||||
"$(translate "A host reboot is required to apply passthrough changes before starting the VM.")\n\n$(translate "Do you want to reboot now?")" 11 78; then
|
if whiptail --title "$(translate "Reboot Required")" --yesno \
|
||||||
|
"\n${REBOOT_REASONS}\n$(translate "A host reboot is required before starting the VM. Reboot now?")" 13 78; then
|
||||||
msg_warn "$(translate "Rebooting the system...")"
|
msg_warn "$(translate "Rebooting the system...")"
|
||||||
reboot
|
reboot
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||||
|
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo -e
|
echo -e
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ function prompt_controller_conflict_policy() {
|
|||||||
shift
|
shift
|
||||||
local -a source_vms=("$@")
|
local -a source_vms=("$@")
|
||||||
local msg vmid vm_name st ob
|
local msg vmid vm_name st ob
|
||||||
msg="$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
msg="\n$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||||
for vmid in "${source_vms[@]}"; do
|
for vmid in "${source_vms[@]}"; do
|
||||||
vm_name=$(_vm_name_by_id "$vmid")
|
vm_name=$(_vm_name_by_id "$vmid")
|
||||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||||
@@ -128,7 +128,7 @@ function prompt_controller_conflict_policy() {
|
|||||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||||
|
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 20 80 10 \
|
||||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||||
"3" "$(translate "Skip this device")" \
|
"3" "$(translate "Skip this device")" \
|
||||||
@@ -554,6 +554,7 @@ fi
|
|||||||
if qm set "$VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >/dev/null 2>&1; then
|
if qm set "$VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >/dev/null 2>&1; then
|
||||||
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})"
|
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})"
|
||||||
DISK_INFO+="<p>Controller/NVMe: ${pci}</p>"
|
DISK_INFO+="<p>Controller/NVMe: ${pci}</p>"
|
||||||
|
BOOT_ORDER="${BOOT_ORDER:+$BOOT_ORDER;}hostpci${hostpci_idx}"
|
||||||
hostpci_idx=$((hostpci_idx + 1))
|
hostpci_idx=$((hostpci_idx + 1))
|
||||||
else
|
else
|
||||||
msg_error "$(translate "Failed to assign Controller/NVMe") (${pci})"
|
msg_error "$(translate "Failed to assign Controller/NVMe") (${pci})"
|
||||||
@@ -769,18 +770,26 @@ if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then
|
|||||||
echo -e
|
echo -e
|
||||||
fi
|
fi
|
||||||
local HOST_REBOOT_REQUIRED="no"
|
local HOST_REBOOT_REQUIRED="no"
|
||||||
|
local REBOOT_REASONS=""
|
||||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
HOST_REBOOT_REQUIRED="yes"
|
HOST_REBOOT_REQUIRED="yes"
|
||||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||||
|
REBOOT_REASONS+="$(translate "IOMMU has been enabled on this system.")\n"
|
||||||
fi
|
fi
|
||||||
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
||||||
HOST_REBOOT_REQUIRED="yes"
|
HOST_REBOOT_REQUIRED="yes"
|
||||||
|
REBOOT_REASONS+="$(translate "GPU passthrough changes require a host reboot.")\n"
|
||||||
fi
|
fi
|
||||||
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
||||||
if whiptail --title "$(translate "Reboot Recommended")" --yesno \
|
echo ""
|
||||||
"$(translate "A host reboot is required to apply passthrough changes before starting the VM.")\n\n$(translate "Do you want to reboot now?")" 11 78; then
|
if whiptail --title "$(translate "Reboot Required")" --yesno \
|
||||||
|
"\n${REBOOT_REASONS}\n$(translate "A host reboot is required before starting the VM. Reboot now?")" 13 78; then
|
||||||
msg_warn "$(translate "Rebooting the system...")"
|
msg_warn "$(translate "Rebooting the system...")"
|
||||||
reboot
|
reboot
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||||
|
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
msg_success "$(translate "Press Enter to return to the main menu...")"
|
msg_success "$(translate "Press Enter to return to the main menu...")"
|
||||||
@@ -807,10 +816,6 @@ elif [[ "$OS_TYPE" == "3" ]]; then
|
|||||||
echo -e
|
echo -e
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
|
||||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_success "$(translate "Press Enter to return to the main menu...")"
|
msg_success "$(translate "Press Enter to return to the main menu...")"
|
||||||
read -r
|
read -r
|
||||||
bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh"
|
bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh"
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then
|
|||||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then
|
||||||
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh"
|
||||||
fi
|
fi
|
||||||
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh"
|
||||||
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh" ]]; then
|
||||||
|
source "$LOCAL_SCRIPTS_DEFAULT/vm/disk_selector.sh"
|
||||||
|
fi
|
||||||
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
||||||
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
||||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
||||||
@@ -490,24 +495,24 @@ function select_disk_type() {
|
|||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||||
"1" "$(translate "Add virtual disk")" \
|
"a" "$(translate "Add virtual disk")" \
|
||||||
"2" "$(translate "Add import disk")" \
|
"b" "$(translate "Add import disk")" \
|
||||||
"3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||||
"r" "$(translate "Reset current storage selection")" \
|
"r" "$(translate "Reset current storage selection")" \
|
||||||
"d" "$(translate "[ Finish and continue ]")" \
|
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
||||||
msg_warn "$(translate "Storage plan selection cancelled.")"
|
msg_warn "$(translate "Storage plan selection cancelled.")"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1)
|
a)
|
||||||
select_virtual_disk
|
select_virtual_disk
|
||||||
;;
|
;;
|
||||||
2)
|
b)
|
||||||
select_import_disk
|
select_import_disk
|
||||||
;;
|
;;
|
||||||
3)
|
c)
|
||||||
select_controller_nvme
|
select_controller_nvme
|
||||||
;;
|
;;
|
||||||
r)
|
r)
|
||||||
@@ -590,49 +595,6 @@ function select_virtual_disk() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_import_disk() {
|
|
||||||
msg_info "$(translate "Detecting available disks...")"
|
|
||||||
_refresh_host_storage_cache
|
|
||||||
|
|
||||||
local FREE_DISKS=()
|
|
||||||
local DISK INFO MODEL SIZE LABEL DESCRIPTION
|
|
||||||
while read -r DISK; do
|
|
||||||
[[ "$DISK" =~ /dev/zd ]] && continue
|
|
||||||
_disk_is_host_system_used "$DISK" && continue
|
|
||||||
|
|
||||||
INFO=($(lsblk -dn -o MODEL,SIZE "$DISK"))
|
|
||||||
MODEL="${INFO[@]::${#INFO[@]}-1}"
|
|
||||||
SIZE="${INFO[-1]}"
|
|
||||||
LABEL=""
|
|
||||||
if _disk_used_in_guest_configs "$DISK"; then
|
|
||||||
LABEL+=" [⚠ $(translate "In use by VM/LXC config")]"
|
|
||||||
fi
|
|
||||||
DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL")
|
|
||||||
if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then
|
|
||||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON")
|
|
||||||
else
|
|
||||||
FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF")
|
|
||||||
fi
|
|
||||||
done < <(lsblk -dn -e 7,11 -o PATH)
|
|
||||||
stop_spinner
|
|
||||||
if [[ ${#FREE_DISKS[@]} -eq 0 ]]; then
|
|
||||||
whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local selected
|
|
||||||
selected=$(whiptail --title "Select Import Disks" --checklist \
|
|
||||||
"$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 78 10 \
|
|
||||||
"${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) || return 1
|
|
||||||
|
|
||||||
IMPORT_DISKS=()
|
|
||||||
local item
|
|
||||||
for item in $(echo "$selected" | tr -d '"'); do
|
|
||||||
IMPORT_DISKS+=("$item")
|
|
||||||
done
|
|
||||||
export IMPORT_DISKS
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_controller_nvme() {
|
function select_controller_nvme() {
|
||||||
local VM_STORAGE_IOMMU_REBOOT_POLICY="defer"
|
local VM_STORAGE_IOMMU_REBOOT_POLICY="defer"
|
||||||
|
|
||||||
@@ -761,7 +723,7 @@ function prompt_controller_conflict_policy() {
|
|||||||
shift
|
shift
|
||||||
local -a source_vms=("$@")
|
local -a source_vms=("$@")
|
||||||
local msg vmid vm_name st ob
|
local msg vmid vm_name st ob
|
||||||
msg="$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
msg="\n$(translate "Selected controller/NVMe is already assigned to other VM(s):")\n\n"
|
||||||
for vmid in "${source_vms[@]}"; do
|
for vmid in "${source_vms[@]}"; do
|
||||||
vm_name=$(_vm_name_by_id "$vmid")
|
vm_name=$(_vm_name_by_id "$vmid")
|
||||||
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
st="stopped"; _vm_status_is_running "$vmid" && st="running"
|
||||||
@@ -771,7 +733,7 @@ function prompt_controller_conflict_policy() {
|
|||||||
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
msg+="\n$(translate "Choose action for this controller/NVMe:")"
|
||||||
|
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
|
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 20 80 10 \
|
||||||
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
|
||||||
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
"2" "$(translate "Move to target VM (remove from source VM config)")" \
|
||||||
"3" "$(translate "Skip this device")" \
|
"3" "$(translate "Skip this device")" \
|
||||||
@@ -1398,6 +1360,7 @@ function create_vm() {
|
|||||||
msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}"
|
msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}"
|
||||||
DISK_INFO="${DISK_INFO}<p>Controller/NVMe: ${PCI_DEV}</p>"
|
DISK_INFO="${DISK_INFO}<p>Controller/NVMe: ${PCI_DEV}</p>"
|
||||||
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n"
|
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n"
|
||||||
|
BOOT_ORDER_LIST+=("hostpci${HOSTPCI_INDEX}")
|
||||||
HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1))
|
HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1))
|
||||||
else
|
else
|
||||||
msg_error "Failed to configure controller/NVMe: ${PCI_DEV}"
|
msg_error "Failed to configure controller/NVMe: ${PCI_DEV}"
|
||||||
@@ -1511,18 +1474,26 @@ else
|
|||||||
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
||||||
fi
|
fi
|
||||||
local HOST_REBOOT_REQUIRED="no"
|
local HOST_REBOOT_REQUIRED="no"
|
||||||
|
local REBOOT_REASONS=""
|
||||||
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
if [[ "${VM_STORAGE_IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
|
||||||
HOST_REBOOT_REQUIRED="yes"
|
HOST_REBOOT_REQUIRED="yes"
|
||||||
msg_warn "$(translate "IOMMU was enabled during this wizard. Reboot the host to apply it.")"
|
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||||
|
REBOOT_REASONS+="$(translate "IOMMU has been enabled on this system.")\n"
|
||||||
fi
|
fi
|
||||||
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
if [[ "$GPU_WIZARD_REBOOT_REQUIRED" == "yes" ]]; then
|
||||||
HOST_REBOOT_REQUIRED="yes"
|
HOST_REBOOT_REQUIRED="yes"
|
||||||
|
REBOOT_REASONS+="$(translate "GPU passthrough changes require a host reboot.")\n"
|
||||||
fi
|
fi
|
||||||
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
|
||||||
if whiptail --title "$(translate "Reboot Recommended")" --yesno \
|
echo ""
|
||||||
"$(translate "A host reboot is required to apply passthrough changes before starting the VM.")\n\n$(translate "Do you want to reboot now?")" 11 78; then
|
if whiptail --title "$(translate "Reboot Required")" --yesno \
|
||||||
|
"\n${REBOOT_REASONS}\n$(translate "A host reboot is required before starting the VM. Reboot now?")" 13 78; then
|
||||||
msg_warn "$(translate "Rebooting the system...")"
|
msg_warn "$(translate "Rebooting the system...")"
|
||||||
reboot
|
reboot
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||||
|
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo -e
|
echo -e
|
||||||
|
|||||||
Reference in New Issue
Block a user