2026-04-04 00:54:57 +02:00
#!/bin/bash
# ==========================================================
# ProxMenux - GPU Passthrough to Virtual Machine (VM)
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# Version : 1.0
# Last Updated: 03/04/2026
# ==========================================================
# Description:
# Automates full GPU passthrough (VFIO) from Proxmox host to a VM.
# Supports Intel iGPU, AMD and NVIDIA GPUs.
#
# Features:
# - IOMMU detection and activation offer
# - Multi-GPU selection menu
# - IOMMU group analysis (all group devices passed together)
# - Single-GPU warning (host loses physical video output)
# - Switch mode: detects GPU used in LXC or another VM
# - AMD ROM dump via sysfs
# - Idempotent host config (modules, vfio.conf, blacklist)
# - VM config: hostpci entries, NVIDIA KVM hiding
# ==========================================================
2026-04-05 11:24:08 +02:00
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 "
2026-04-04 00:54:57 +02:00
BASE_DIR = "/usr/local/share/proxmenux"
2026-04-12 20:32:34 +02:00
TOOLS_JSON = " $BASE_DIR /installed_tools.json"
2026-04-05 11:24:08 +02:00
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
2026-04-04 00:54:57 +02:00
LOG_FILE = "/tmp/add_gpu_vm.log"
screen_capture = "/tmp/proxmenux_add_gpu_vm_screen_ $$ .txt"
if [[ -f " $UTILS_FILE " ]] ; then
source " $UTILS_FILE "
fi
2026-04-05 11:24:08 +02:00
if [[ -f " $LOCAL_SCRIPTS_LOCAL /global/pci_passthrough_helpers.sh" ]] ; then
source " $LOCAL_SCRIPTS_LOCAL /global/pci_passthrough_helpers.sh"
elif [[ -f " $LOCAL_SCRIPTS_DEFAULT /global/pci_passthrough_helpers.sh" ]] ; then
source " $LOCAL_SCRIPTS_DEFAULT /global/pci_passthrough_helpers.sh"
fi
2026-04-04 00:54:57 +02:00
load_language
initialize_cache
# ==========================================================
# Global state
# ==========================================================
declare -a ALL_GPU_PCIS =() # "0000:01:00.0"
declare -a ALL_GPU_TYPES =() # intel / amd / nvidia
declare -a ALL_GPU_NAMES =() # human-readable name
declare -a ALL_GPU_DRIVERS =() # current kernel driver
SELECTED_GPU = "" # intel / amd / nvidia
SELECTED_GPU_PCI = "" # 0000:01:00.0
SELECTED_GPU_NAME = ""
declare -a IOMMU_DEVICES =() # all PCI addrs in IOMMU group (endpoint devices)
declare -a IOMMU_VFIO_IDS =() # vendor:device for vfio-pci ids=
2026-04-05 11:24:08 +02:00
declare -a EXTRA_AUDIO_DEVICES =() # sibling audio function(s), typically *.1
2026-04-21 21:06:22 +02:00
declare -a EXTRA_AUDIO_INFO =() # parallel to EXTRA_AUDIO_DEVICES — "BDF|current_driver" pairs for the summary dialog
2026-04-04 00:54:57 +02:00
IOMMU_GROUP = ""
2026-04-06 19:13:31 +02:00
IOMMU_PENDING_REBOOT = false
2026-04-04 00:54:57 +02:00
SELECTED_VMID = ""
VM_NAME = ""
GPU_COUNT = 0
SINGLE_GPU_SYSTEM = false
SWITCH_FROM_LXC = false
SWITCH_LXC_LIST = ""
SWITCH_FROM_VM = false
SWITCH_VM_SRC = ""
2026-04-06 21:50:46 +02:00
SWITCH_VM_ACTION = "" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
2026-04-04 00:54:57 +02:00
TARGET_VM_ALREADY_HAS_GPU = false
2026-04-05 11:24:08 +02:00
VM_SWITCH_ALREADY_VFIO = false
PREFLIGHT_HOST_REBOOT_REQUIRED = true
2026-04-04 00:54:57 +02:00
AMD_ROM_FILE = ""
HOST_CONFIG_CHANGED = false # set to true whenever host VFIO config is actually written
2026-04-05 11:24:08 +02:00
PRESELECT_VMID = ""
WIZARD_CALL = false
GPU_WIZARD_RESULT_FILE = ""
2026-04-06 13:39:07 +02:00
declare -a LXC_AFFECTED_CTIDS =()
declare -a LXC_AFFECTED_NAMES =()
declare -a LXC_AFFECTED_RUNNING =() # 1 or 0
declare -a LXC_AFFECTED_ONBOOT =() # 1 or 0
LXC_SWITCH_ACTION = "" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
2026-04-04 00:54:57 +02:00
# ==========================================================
# Helpers
# ==========================================================
_get_pci_driver() {
local pci_full = " $1 "
local driver_link = "/sys/bus/pci/devices/ ${ pci_full } /driver"
if [[ -L " $driver_link " ]] ; then
basename " $( readlink " $driver_link " ) "
else
echo "none"
fi
}
_add_line_if_missing() {
local line = " $1 "
local file = " $2 "
touch " $file "
if ! grep -qF " $line " " $file " ; then
echo " $line " >> " $file "
HOST_CONFIG_CHANGED = true
fi
}
2026-04-05 11:24:08 +02:00
_append_unique() {
local needle = " $1 "
shift
local item
for item in " $@ " ; do
[[ " $item " == " $needle " ]] && return 1
done
return 0
}
_vm_is_running() {
local vmid = " $1 "
qm status " $vmid " 2>/dev/null | grep -q "status: running"
}
_vm_onboot_enabled() {
local vmid = " $1 "
qm config " $vmid " 2>/dev/null | grep -qE "^onboot:\s*1"
}
2026-04-06 13:39:07 +02:00
_ct_is_running() {
local ctid = " $1 "
pct status " $ctid " 2>/dev/null | grep -q "status: running"
}
_ct_onboot_enabled() {
local ctid = " $1 "
pct config " $ctid " 2>/dev/null | grep -qE "^onboot:\s*1"
}
_lxc_conf_uses_selected_gpu() {
local conf = " $1 "
case " $SELECTED_GPU " in
nvidia)
grep -qE "dev[0-9]+:.*(/dev/nvidia|/dev/nvidia-caps)" " $conf " 2>/dev/null
;;
amd)
grep -qE "dev[0-9]+:.*(/dev/dri|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" " $conf " 2>/dev/null
;;
intel)
grep -qE "dev[0-9]+:.*(/dev/dri)|lxc\.mount\.entry:.*dev/dri" " $conf " 2>/dev/null
;;
*)
grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" " $conf " 2>/dev/null
;;
esac
}
_lxc_switch_action_label() {
case " $LXC_SWITCH_ACTION " in
keep_gpu_disable_onboot) echo " $( translate 'Keep GPU in LXC config + disable Start on boot' ) " ;;
remove_gpu_keep_onboot) echo " $( translate 'Remove GPU from LXC config + keep Start on boot unchanged' ) " ;;
*) echo " $( translate 'No specific LXC action selected' ) " ;;
esac
}
2026-04-06 21:50:46 +02:00
_vm_switch_action_label() {
case " $SWITCH_VM_ACTION " in
keep_gpu_disable_onboot) echo " $( translate 'Keep GPU in source VM config + disable Start on boot if enabled' ) " ;;
remove_gpu_keep_onboot) echo " $( translate 'Remove GPU from source VM config + keep Start on boot unchanged' ) " ;;
*) echo " $( translate 'No specific VM action selected' ) " ;;
esac
}
2026-04-12 20:32:34 +02:00
_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
}
2026-04-05 11:24:08 +02:00
_set_wizard_result() {
local result = " $1 "
[[ -z " ${ GPU_WIZARD_RESULT_FILE :- } " ]] && return 0
printf '%s\n' " $result " >" $GPU_WIZARD_RESULT_FILE " 2>/dev/null || true
}
2026-04-12 20:32:34 +02:00
# ==========================================================
# 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'
}
2026-04-21 21:06:22 +02:00
# Msgbox: dialog in standalone mode, whiptail in wizard mode.
# I/O pinned to /dev/tty so the dialog renders reliably regardless of
# how the caller redirected stdin/stdout, and immune to the SIGTTOU
# trap that fires when this script is resumed as a background job.
2026-04-12 20:32:34 +02:00
_pmx_msgbox() {
local title = " $1 " msg = " $2 " h = " ${ 3 :- 10 } " w = " ${ 4 :- 72 } "
if [[ " $WIZARD_CALL " == "true" ]] ; then
whiptail --backtitle "ProxMenux" --title " $title " \
2026-04-21 21:06:22 +02:00
--msgbox " $( _strip_colors " $msg " ) " " $h " " $w " < /dev/tty > /dev/tty
2026-04-12 20:32:34 +02:00
else
dialog --backtitle "ProxMenux" --colors \
2026-04-21 21:06:22 +02:00
--title " $title " --msgbox " $msg " " $h " " $w " < /dev/tty > /dev/tty
2026-04-12 20:32:34 +02:00
fi
}
2026-04-21 21:06:22 +02:00
# Yesno: dialog in standalone mode, whiptail in wizard mode.
# Returns 0 for yes, 1 for no (same as dialog/whiptail).
# I/O pinned to /dev/tty — see the note on _pmx_msgbox.
2026-04-12 20:32:34 +02:00
_pmx_yesno() {
local title = " $1 " msg = " $2 " h = " ${ 3 :- 10 } " w = " ${ 4 :- 72 } "
if [[ " $WIZARD_CALL " == "true" ]] ; then
whiptail --backtitle "ProxMenux" --title " $title " \
2026-04-21 21:06:22 +02:00
--yesno " $( _strip_colors " $msg " ) " " $h " " $w " < /dev/tty > /dev/tty
2026-04-12 20:32:34 +02:00
else
dialog --backtitle "ProxMenux" --colors \
2026-04-21 21:06:22 +02:00
--title " $title " --yesno " $msg " " $h " " $w " < /dev/tty > /dev/tty
2026-04-12 20:32:34 +02:00
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 $?
}
2026-04-21 21:06:22 +02:00
# Checklist: dialog in standalone mode, whiptail in wizard mode.
# Usage: _pmx_checklist title msg h w list_h tag1 desc1 state1 tag2 desc2 state2 ...
# state is "on" or "off". Returns the space-separated list of selected
# tags on stdout (one line). Returns non-zero if the user cancels.
_pmx_checklist() {
local title = " $1 " msg = " $2 " h = " $3 " w = " $4 " lh = " $5 "
shift 5
if [[ " $WIZARD_CALL " == "true" ]] ; then
whiptail --backtitle "ProxMenux" \
--title " $title " \
--checklist " $( _strip_colors " $msg " ) " " $h " " $w " " $lh " \
" $@ " 3>& 1 1>& 2 2>& 3
else
dialog --backtitle "ProxMenux" --colors \
--title " $title " \
--checklist " $msg " " $h " " $w " " $lh " \
" $@ " 2>& 1 >/dev/tty
fi
return $?
}
2026-04-05 11:24:08 +02:00
_file_has_exact_line() {
local line = " $1 "
local file = " $2 "
[[ -f " $file " ]] || return 1
grep -qFx " $line " " $file "
}
evaluate_host_reboot_requirement() {
2026-04-06 19:13:31 +02:00
if [[ " $IOMMU_PENDING_REBOOT " == "true" ]] ; then
PREFLIGHT_HOST_REBOOT_REQUIRED = true
return 0
fi
2026-04-05 11:24:08 +02:00
# Fast path for VM-to-VM reassignment where GPU is already bound to vfio
if [[ " $VM_SWITCH_ALREADY_VFIO " == "true" ]] ; then
PREFLIGHT_HOST_REBOOT_REQUIRED = false
return 0
fi
local needs_change = false
local current_driver
current_driver = $( _get_pci_driver " $SELECTED_GPU_PCI " )
[[ " $current_driver " != "vfio-pci" ]] && needs_change = true
# /etc/modules expected lines
local modules_file = "/etc/modules"
local modules =( "vfio" "vfio_iommu_type1" "vfio_pci" )
local kernel_major kernel_minor
kernel_major = $( uname -r | cut -d. -f1)
kernel_minor = $( uname -r | cut -d. -f2)
if (( kernel_major < 6 || ( kernel_major == 6 && kernel_minor < 2 ) )) ; then
modules +=( "vfio_virqfd" )
fi
local mod
for mod in " ${ modules [@] } " ; do
_file_has_exact_line " $mod " " $modules_file " || needs_change = true
done
# vfio-pci ids
local vfio_conf = "/etc/modprobe.d/vfio.conf"
local ids_line ids_part
ids_line = $( grep "^options vfio-pci ids=" " $vfio_conf " 2>/dev/null | head -1)
if [[ -z " $ids_line " ]] ; then
needs_change = true
else
[[ " $ids_line " == *"disable_vga=1" * ]] || needs_change = true
ids_part = $( echo " $ids_line " | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//' )
local existing_ids =()
IFS = ',' read -ra existing_ids <<< " $ids_part "
local required found existing
for required in " ${ IOMMU_VFIO_IDS [@] } " ; do
found = false
for existing in " ${ existing_ids [@] } " ; do
[[ " $existing " == " $required " ]] && found = true && break
done
$found || needs_change = true
done
fi
# modprobe options files
_file_has_exact_line "options vfio_iommu_type1 allow_unsafe_interrupts=1" \
/etc/modprobe.d/iommu_unsafe_interrupts.conf || needs_change = true
_file_has_exact_line "options kvm ignore_msrs=1" \
/etc/modprobe.d/kvm.conf || needs_change = true
# AMD softdep
if [[ " $SELECTED_GPU " == "amd" ]] ; then
_file_has_exact_line "softdep radeon pre: vfio-pci" " $vfio_conf " || needs_change = true
_file_has_exact_line "softdep amdgpu pre: vfio-pci" " $vfio_conf " || needs_change = true
_file_has_exact_line "softdep snd_hda_intel pre: vfio-pci" " $vfio_conf " || needs_change = true
fi
# host driver blacklist
local blacklist_file = "/etc/modprobe.d/blacklist.conf"
case " $SELECTED_GPU " in
nvidia)
_file_has_exact_line "blacklist nouveau" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist nvidia" " $blacklist_file " || needs_change = true
2026-04-06 18:40:57 +02:00
_file_has_exact_line "blacklist nvidia_drm" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist nvidia_modeset" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist nvidia_uvm" " $blacklist_file " || needs_change = true
2026-04-05 11:24:08 +02:00
_file_has_exact_line "blacklist nvidiafb" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist lbm-nouveau" " $blacklist_file " || needs_change = true
_file_has_exact_line "options nouveau modeset=0" " $blacklist_file " || needs_change = true
2026-04-06 18:40:57 +02:00
[[ -f /etc/modules-load.d/nvidia-vfio.conf ]] && needs_change = true
grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null && needs_change = true
local svc
for svc in nvidia-persistenced.service nvidia-persistenced nvidia-powerd.service nvidia-fabricmanager.service; do
if systemctl is-active --quiet " $svc " 2>/dev/null || systemctl is-enabled --quiet " $svc " 2>/dev/null; then
needs_change = true
fi
done
2026-04-05 11:24:08 +02:00
;;
amd)
_file_has_exact_line "blacklist radeon" " $blacklist_file " || needs_change = true
_file_has_exact_line "blacklist amdgpu" " $blacklist_file " || needs_change = true
;;
intel)
_file_has_exact_line "blacklist i915" " $blacklist_file " || needs_change = true
;;
esac
if [[ " $needs_change " == "true" ]] ; then
PREFLIGHT_HOST_REBOOT_REQUIRED = true
else
PREFLIGHT_HOST_REBOOT_REQUIRED = false
fi
}
parse_cli_args() {
while [[ $# -gt 0 ]] ; do
case " $1 " in
--vmid)
if [[ -n " ${ 2 :- } " ]] ; then
PRESELECT_VMID = " $2 "
shift 2
else
shift
fi
;;
--wizard)
WIZARD_CALL = true
shift
;;
--result-file)
if [[ -n " ${ 2 :- } " ]] ; then
GPU_WIZARD_RESULT_FILE = " $2 "
shift 2
else
shift
fi
;;
*)
shift
;;
esac
done
}
2026-04-04 00:54:57 +02:00
_get_vm_run_title() {
if [[ " $SWITCH_FROM_LXC " == "true" && " $SWITCH_FROM_VM " == "true" ]] ; then
2026-04-05 11:24:08 +02:00
echo " $( translate 'GPU Passthrough to VM (reassign from LXC and another VM)' ) "
2026-04-04 00:54:57 +02:00
elif [[ " $SWITCH_FROM_LXC " == "true" ]] ; then
2026-04-05 11:24:08 +02:00
echo " $( translate 'GPU Passthrough to VM (from LXC)' ) "
2026-04-04 00:54:57 +02:00
elif [[ " $SWITCH_FROM_VM " == "true" ]] ; then
2026-04-05 11:24:08 +02:00
echo " $( translate 'GPU Passthrough to VM (reassign from another VM)' ) "
2026-04-04 00:54:57 +02:00
else
echo " $( translate 'GPU Passthrough to VM' ) "
fi
}
_is_pci_slot_assigned_to_vm() {
2026-04-05 11:24:08 +02:00
if declare -F _pci_slot_assigned_to_vm >/dev/null 2>& 1; then
_pci_slot_assigned_to_vm " $1 " " $2 "
return $?
fi
2026-04-04 00:54:57 +02:00
local pci_full = " $1 "
local vmid = " $2 "
local slot_base
slot_base = " ${ pci_full #0000: } "
slot_base = " ${ slot_base %.* } " # 01:00
qm config " $vmid " 2>/dev/null \
| grep -qE "^hostpci[0-9]+:.*(0000:)? ${ slot_base } (\\.[0-7])?([,[:space:]]| $)"
}
# Match a specific PCI function when possible.
# For function .0, also accept slot-only entries (e.g. 01:00) as equivalent.
_is_pci_function_assigned_to_vm() {
2026-04-05 11:24:08 +02:00
if declare -F _pci_function_assigned_to_vm >/dev/null 2>& 1; then
_pci_function_assigned_to_vm " $1 " " $2 "
return $?
fi
2026-04-04 00:54:57 +02:00
local pci_full = " $1 "
local vmid = " $2 "
local bdf slot func pattern
bdf = " ${ pci_full #0000: } " # 01:00.0
slot = " ${ bdf %.* } " # 01:00
func = " ${ bdf ##*. } " # 0
if [[ " $func " == "0" ]] ; then
pattern = "^hostpci[0-9]+:.*(0000:)?( ${ bdf } | ${ slot } )([,:[:space:]]| $)"
else
pattern = "^hostpci[0-9]+:.*(0000:)? ${ bdf } ([,[:space:]]| $)"
fi
qm config " $vmid " 2>/dev/null | grep -qE " $pattern "
}
ensure_selected_gpu_not_already_in_target_vm() {
while _is_pci_slot_assigned_to_vm " $SELECTED_GPU_PCI " " $SELECTED_VMID " ; do
local current_driver
current_driver = $( _get_pci_driver " $SELECTED_GPU_PCI " )
# GPU is already assigned to this VM, but host is not in VFIO mode.
# Continue so the script can re-activate VM passthrough on the host.
if [[ " $current_driver " != "vfio-pci" ]] ; then
TARGET_VM_ALREADY_HAS_GPU = true
local popup_title
popup_title = $( _get_vm_run_title)
2026-04-12 20:32:34 +02:00
_pmx_msgbox " ${ 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.' ) " \
2026-04-04 00:54:57 +02:00
13 78
return 0
fi
# Single GPU system: nothing else to choose
if [[ $GPU_COUNT -le 1 ]] ; then
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'GPU Already Added' ) " \
"\n $( translate 'The selected GPU is already assigned to this VM.' ) \n\n $( translate 'No changes are required.' ) " \
2026-04-04 00:54:57 +02:00
9 66
exit 0
fi
# Build menu with GPUs that are NOT already assigned to this VM
local menu_items =()
local i available
available = 0
for i in " ${ !ALL_GPU_PCIS[@] } " ; do
local pci label
pci = " ${ ALL_GPU_PCIS [ $i ] } "
_is_pci_slot_assigned_to_vm " $pci " " $SELECTED_VMID " && continue
2026-04-06 21:12:13 +02:00
label = " ${ ALL_GPU_NAMES [ $i ] } [ ${ ALL_GPU_DRIVERS [ $i ] } ] — ${ pci } "
2026-04-04 00:54:57 +02:00
menu_items +=( " $i " " $label " )
available = $(( available + 1 ))
done
if [[ $available -eq 0 ]] ; then
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( 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.' ) " \
2026-04-04 00:54:57 +02:00
10 70
exit 0
fi
2026-04-06 22:34:37 +02:00
local choice
2026-04-12 20:32:34 +02:00
choice = $( _pmx_menu \
" $( translate 'GPU Already Assigned to This VM' ) " \
"\n $( translate 'The selected GPU is already present in this VM. Select another GPU to continue:' ) " \
2026-04-06 22:21:38 +02:00
18 82 10 \
2026-04-12 20:32:34 +02:00
" ${ menu_items [@] } " ) || exit 0
2026-04-04 00:54:57 +02:00
SELECTED_GPU = " ${ ALL_GPU_TYPES [ $choice ] } "
SELECTED_GPU_PCI = " ${ ALL_GPU_PCIS [ $choice ] } "
SELECTED_GPU_NAME = " ${ ALL_GPU_NAMES [ $choice ] } "
done
}
# ==========================================================
# Phase 1 — Step 1: Detect host GPUs
# ==========================================================
detect_host_gpus() {
while IFS = read -r line; do
2026-04-06 21:12:13 +02:00
local pci_short pci_full name type driver pci_info
2026-04-04 00:54:57 +02:00
pci_short = $( echo " $line " | awk '{print $1}' )
pci_full = "0000: ${ pci_short } "
2026-04-06 21:12:13 +02:00
# Prefer full vendor/model descriptor for clearer menus.
pci_info = $( lspci -nn -s " ${ pci_short } " 2>/dev/null | sed 's/^[^ ]* //' )
name = " ${ pci_info #*: } "
[[ " $name " == " $pci_info " ]] && name = " $pci_info "
name = $( echo " $name " | sed -E 's/ \(rev [^)]+\)$//' | cut -c1-72)
[[ -z " $name " ]] && name = " $( translate 'Unknown GPU' ) "
2026-04-04 00:54:57 +02:00
if echo " $line " | grep -qi "Intel" ; then
type = "intel"
elif echo " $line " | grep -qiE "AMD|Advanced Micro|Radeon" ; then
type = "amd"
elif echo " $line " | grep -qi "NVIDIA" ; then
type = "nvidia"
else
type = "other"
fi
driver = $( _get_pci_driver " $pci_full " )
ALL_GPU_PCIS +=( " $pci_full " )
ALL_GPU_TYPES +=( " $type " )
ALL_GPU_NAMES +=( " $name " )
ALL_GPU_DRIVERS +=( " $driver " )
done < <( lspci -nn | grep -iE "VGA compatible controller|3D controller|Display controller" \
| grep -iv "Ethernet\|Network\|Audio" )
GPU_COUNT = ${# ALL_GPU_PCIS [@] }
if [[ $GPU_COUNT -eq 0 ]] ; then
2026-04-05 11:24:08 +02:00
_set_wizard_result "no_gpu"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'No GPU Detected' ) " \
"\n $( translate 'No compatible GPU was detected on this host.' ) " 8 60
2026-04-04 00:54:57 +02:00
exit 0
fi
[[ $GPU_COUNT -eq 1 ]] && SINGLE_GPU_SYSTEM = true
}
# ==========================================================
# Phase 1 — Step 2: Check IOMMU, offer to enable it
# ==========================================================
check_iommu_enabled() {
2026-04-12 20:32:34 +02:00
# 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
2026-04-05 11:24:08 +02:00
if declare -F _pci_is_iommu_active >/dev/null 2>& 1 && _pci_is_iommu_active; then
2026-04-12 20:32:34 +02:00
_gpu_register_vfio_iommu_tool
2026-04-05 11:24:08 +02:00
return 0
fi
2026-04-06 19:13:31 +02:00
local configured_next_boot = false
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
configured_next_boot = true
fi
if [[ " $configured_next_boot " == "true" ]] ; then
IOMMU_PENDING_REBOOT = true
HOST_CONFIG_CHANGED = true
2026-04-12 20:32:34 +02:00
_gpu_register_vfio_iommu_tool
VM_STORAGE_IOMMU_PENDING_REBOOT = 1
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.' ) " \
2026-04-06 19:13:31 +02:00
11 78
2026-04-04 00:54:57 +02:00
return 0
fi
local msg
msg = "\n $( translate 'IOMMU is not active on this system.' ) \n\n"
msg += " $( translate 'GPU passthrough to VMs requires IOMMU to be enabled in the kernel.' ) \n\n"
msg += " $( translate 'Do you want to enable IOMMU now?' ) \n\n"
msg += " $( translate 'Note: A system reboot will be required after enabling IOMMU.' ) \n"
2026-04-06 19:13:31 +02:00
msg += " $( translate 'Configuration will continue now and be effective after reboot.' ) "
2026-04-04 00:54:57 +02:00
2026-04-12 20:32:34 +02:00
_pmx_yesno " $( translate 'IOMMU Required' ) " " $msg " 15 72
2026-04-04 00:54:57 +02:00
local response = $?
2026-04-05 11:24:08 +02:00
[[ " $WIZARD_CALL " != "true" ]] && clear
2026-04-04 00:54:57 +02:00
if [[ $response -eq 0 ]] ; then
2026-04-05 11:24:08 +02:00
[[ " $WIZARD_CALL " != "true" ]] && show_proxmenux_logo
2026-04-04 00:54:57 +02:00
msg_title " $( translate 'Enabling IOMMU' ) "
2026-04-06 19:13:31 +02:00
if ! _enable_iommu_cmdline; then
echo
msg_error " $( translate 'Failed to configure IOMMU automatically.' ) "
echo
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
exit 0
fi
IOMMU_PENDING_REBOOT = true
HOST_CONFIG_CHANGED = true
2026-04-12 20:32:34 +02:00
_gpu_register_vfio_iommu_tool
VM_STORAGE_IOMMU_PENDING_REBOOT = 1
export VM_STORAGE_IOMMU_PENDING_REBOOT
2026-04-04 00:54:57 +02:00
echo
2026-04-06 19:13:31 +02:00
msg_success " $( translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.' ) "
2026-04-04 00:54:57 +02:00
echo
2026-04-06 19:13:31 +02:00
if [[ " $WIZARD_CALL " != "true" ]] ; then
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
fi
return 0
2026-04-04 00:54:57 +02:00
fi
exit 0
}
_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"
msg_info " $( translate 'Intel CPU detected' ) "
elif [[ " $cpu_vendor " == "AuthenticAMD" ]] ; then
iommu_param = "amd_iommu=on"
msg_info " $( translate 'AMD CPU detected' ) "
else
msg_error " $( translate 'Unknown CPU vendor. Cannot determine IOMMU parameter.' ) "
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
# systemd-boot / ZFS
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 >/dev/null 2>& 1 || true
msg_ok " $( translate 'IOMMU parameters added to /etc/kernel/cmdline' ) "
else
msg_ok " $( translate 'IOMMU already configured in /etc/kernel/cmdline' ) "
fi
elif [[ -f " $grub_file " ]] ; then
# GRUB
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 >/dev/null 2>& 1 || true
msg_ok " $( translate 'IOMMU parameters added to GRUB' ) "
else
msg_ok " $( translate 'IOMMU already configured in GRUB' ) "
fi
else
msg_error " $( translate 'Neither /etc/kernel/cmdline nor /etc/default/grub found.' ) "
return 1
fi
}
# ==========================================================
# Phase 1 — Step 3: GPU selection
# ==========================================================
select_gpu() {
# Single GPU: auto-select, no menu needed
if [[ $GPU_COUNT -eq 1 ]] ; then
SELECTED_GPU = " ${ ALL_GPU_TYPES [0] } "
SELECTED_GPU_PCI = " ${ ALL_GPU_PCIS [0] } "
SELECTED_GPU_NAME = " ${ ALL_GPU_NAMES [0] } "
return 0
fi
# Multiple GPUs: show menu
local menu_items =()
local i
for i in " ${ !ALL_GPU_PCIS[@] } " ; do
2026-04-06 21:12:13 +02:00
local label = " ${ ALL_GPU_NAMES [ $i ] } [ ${ ALL_GPU_DRIVERS [ $i ] } ] — ${ ALL_GPU_PCIS [ $i ] } "
2026-04-04 00:54:57 +02:00
menu_items +=( " $i " " $label " )
done
2026-04-06 22:34:37 +02:00
local choice
2026-04-12 20:32:34 +02:00
choice = $( _pmx_menu \
" $( translate 'Select GPU for VM Passthrough' ) " \
"\n $( translate 'Select the GPU to pass through to the VM:' ) " \
2026-04-06 22:21:38 +02:00
18 82 10 \
2026-04-12 20:32:34 +02:00
" ${ menu_items [@] } " ) || exit 0
2026-04-04 00:54:57 +02:00
SELECTED_GPU = " ${ ALL_GPU_TYPES [ $choice ] } "
SELECTED_GPU_PCI = " ${ ALL_GPU_PCIS [ $choice ] } "
SELECTED_GPU_NAME = " ${ ALL_GPU_NAMES [ $choice ] } "
}
2026-04-19 12:26:52 +02:00
# ==========================================================
# SR-IOV guard — refuse to assign a Virtual Function or a Physical
# Function with active VFs. Matches the policy in switch_gpu_mode.sh:
# writing this GPU's vendor:device to /etc/modprobe.d/vfio.conf would
# let vfio-pci claim the PF at next boot and destroy the whole VF
# tree. ProxMenux does not yet manage SR-IOV lifecycle, so we stop
# before touching vfio.conf / blacklist.conf.
# ==========================================================
check_sriov_and_block_if_needed() {
declare -F _pci_sriov_role >/dev/null 2>& 1 || return 0
[[ -n " $SELECTED_GPU_PCI " ]] || return 0
local role first_word detail = ""
role = $( _pci_sriov_role " $SELECTED_GPU_PCI " )
first_word = " ${ role %% * } "
case " $first_word " in
vf)
local parent = " ${ role #vf } "
detail = " $( translate 'The selected device' ) \Zb ${ SELECTED_GPU_PCI } \Zn $( translate 'is an SR-IOV Virtual Function (VF). Its parent Physical Function is' ) \Zb ${ parent } \Zn."
;;
pf-active)
local n = " ${ role #pf-active } "
detail = " $( translate 'The selected device' ) \Zb ${ SELECTED_GPU_PCI } \Zn $( translate 'is a Physical Function with' ) \Zb ${ n } \Zn $( translate 'active Virtual Functions. Changing its driver binding would destroy every VF.' ) "
;;
*)
return 0
;;
esac
local msg
msg = "\n\Zb\Z6 $( translate 'SR-IOV Configuration Detected' ) \Zn\n\n"
msg += " ${ detail } \n\n"
msg += " $( translate 'To assign VFs to VMs or LXCs, edit the configuration manually via the Proxmox web interface. The Physical Function will remain bound to the native driver.' ) "
_pmx_msgbox " $( translate 'SR-IOV Configuration Detected' ) " " $msg " 16 82
[[ " $WIZARD_CALL " == "true" ]] && _set_wizard_result "cancelled"
exit 0
}
2026-04-04 00:54:57 +02:00
# ==========================================================
# Phase 1 — Step 4: Single-GPU warning
# ==========================================================
warn_single_gpu() {
[[ " $SINGLE_GPU_SYSTEM " != "true" ]] && return 0
local msg
msg = "\n\Zb\Z1⚠ $( translate 'WARNING: This is a single GPU system' ) \Zn\n\n"
msg += " $( translate 'When this GPU is passed through to a VM, the Proxmox host will lose all video output on the physical monitor.' ) \n\n"
msg += " $( translate 'After the reboot, you will only be able to access the Proxmox host via:' ) \n"
msg += " • SSH\n"
msg += " • Proxmox Web UI (https)\n"
msg += " • Serial console\n\n"
msg += " $( translate 'The VM guest will have exclusive access to the GPU.' ) \n\n"
2026-04-06 21:50:46 +02:00
msg += "\Z1 $( translate 'Important: some GPUs may still fail in passthrough and can affect host stability or overall performance depending on hardware/firmware quality.' ) \Zn\n\n"
2026-04-04 00:54:57 +02:00
msg += " $( translate 'Make sure you have SSH or Web UI access before rebooting.' ) \n\n"
msg += " $( translate 'Do you want to continue?' ) "
2026-04-12 20:32:34 +02:00
_pmx_yesno " $( translate 'Single GPU Warning' ) " " $msg " 22 76
2026-04-04 00:54:57 +02:00
[[ $? -ne 0 ]] && exit 0
}
# ==========================================================
# Phase 1 — Step 4b: Hardware passthrough compatibility check
# ==========================================================
# Returns: apu | dedicated | unknown
_detect_amd_gpu_subtype() {
local name_lower
name_lower = $( echo " $SELECTED_GPU_NAME " | tr '[:upper:]' '[:lower:]' )
# Known AMD APU / integrated GPU codenames (mobile and desktop)
local apu_codenames =(
"lucienne" "renoir" "cezanne" "van gogh" "barcelo"
"rembrandt" "phoenix" "hawk point" "strix" "mendocino"
"pollock" "raphael" "dragon range" "raven" "picasso"
)
for codename in " ${ apu_codenames [@] } " ; do
echo " $name_lower " | grep -qi " $codename " && echo "apu" && return
done
# Markers of discrete / dedicated cards
if echo " $name_lower " | grep -qiE "radeon rx|radeon pro|radeon vii|radeon r[0-9]|firepro|instinct|navi|polaris|vega|fiji|ellesmere|baffin" ; then
echo "dedicated"
return
fi
echo "unknown"
}
# Returns: flr | bus | pm | none | unknown
_check_pci_reset_method() {
local pci_full = " $1 "
local reset_file = "/sys/bus/pci/devices/ ${ pci_full } /reset_method"
if [[ ! -f " $reset_file " ]] ; then
[[ -f "/sys/bus/pci/devices/ ${ pci_full } /reset" ]] && echo "unknown" || echo "none"
return
fi
local method
method = $( cat " $reset_file " 2>/dev/null)
echo " $method " | grep -q "flr" && echo "flr" && return
echo " $method " | grep -q "pm" && echo "pm" && return
echo " $method " | grep -q "bus" && echo "bus" && return
echo "unknown"
}
# Returns: igpu | dedicated | unknown
_detect_intel_gpu_subtype() {
local name_lower pci_full
name_lower = $( echo " $SELECTED_GPU_NAME " | tr '[:upper:]' '[:lower:]' )
pci_full = " $SELECTED_GPU_PCI "
# Typical integrated Intel GPU PCI function
[[ " $pci_full " == "0000:00:02.0" ]] && echo "igpu" && return
# Common integrated markers
if echo " $name_lower " | grep -qiE "uhd|hd graphics|iris|xe graphics|integrated" ; then
echo "igpu"
return
fi
# Common dedicated markers (Intel Arc family)
if echo " $name_lower " | grep -qiE "arc|a3[0-9]{2}|a5[0-9]{2}|a7[0-9]{2}|a7[5-9]0|b5[0-9]{2}|b7[0-9]{2}" ; then
echo "dedicated"
return
fi
echo "unknown"
}
check_intel_vm_compatibility() {
local pci_full = " $SELECTED_GPU_PCI "
2026-04-06 13:39:07 +02:00
local gpu_subtype reset_method power_state vendor device viddid
2026-04-04 00:54:57 +02:00
gpu_subtype = $( _detect_intel_gpu_subtype)
reset_method = $( _check_pci_reset_method " $pci_full " )
power_state = $( cat "/sys/bus/pci/devices/ ${ pci_full } /power_state" 2>/dev/null | tr -d '[:space:]' )
2026-04-06 13:39:07 +02:00
vendor = $( cat "/sys/bus/pci/devices/ ${ pci_full } /vendor" 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]' )
device = $( cat "/sys/bus/pci/devices/ ${ pci_full } /device" 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]' )
viddid = " ${ vendor } : ${ device } "
# ── BLOCKER: Known unsupported Intel Apollo Lake iGPU IDs ────────────
if [[ " $viddid " == "8086:5a84" || " $viddid " == "8086:5a85" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'GPU Not Compatible with VM Passthrough' ) \Zn\n\n"
msg += " $( translate 'The selected Intel GPU belongs to Apollo Lake generation and is blocked by policy for VM passthrough due to host instability risk.' ) \n\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n"
msg += " \ZbID: ${ viddid } \Zn\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.' ) "
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Blocked GPU ID' ) " " $msg " 20 84
2026-04-06 13:39:07 +02:00
exit 0
fi
2026-04-04 00:54:57 +02:00
# ── BLOCKER: Intel GPU in D3cold ──────────────────────────────────────
if [[ " $power_state " == "D3cold" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'GPU Not Available for Reliable VM Passthrough' ) \Zn\n\n"
msg += " $( translate 'The selected Intel GPU is currently in power state D3cold' ) :\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n\n"
msg += " $( translate 'Detected power state' ) : \Zb ${ power_state } \Zn\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"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'High-Risk GPU Power State' ) " " $msg " 20 80
2026-04-04 00:54:57 +02:00
exit 0
fi
# ── BLOCKER: no usable reset method ──────────────────────────────────
if [[ " $reset_method " == "none" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'Incompatible Reset Capability for Intel GPU' ) \Zn\n\n"
msg += " $( translate 'The selected Intel GPU does not expose a PCI reset interface' ) :\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n\n"
msg += " $( translate 'Detected reset method' ) : \Zb ${ reset_method } \Zn\n\n"
msg += " $( translate 'Without a usable reset path, passthrough reliability is poor and VM' ) \n"
msg += " $( translate 'startup/restart errors are likely.' ) \n\n"
msg += "\Zb $( translate 'Configuration has been stopped due to high reset risk.' ) \Zn"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Reset Capability Blocked' ) " " $msg " 20 80
2026-04-04 00:54:57 +02:00
exit 0
fi
# ── BLOCKER: Intel dGPU without FLR ──────────────────────────────────
if [[ " $gpu_subtype " == "dedicated" && " $reset_method " != "flr" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'Incompatible Reset Capability for Intel dGPU' ) \Zn\n\n"
msg += " $( translate 'An Intel dedicated GPU has been detected without FLR support' ) :\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n\n"
msg += " $( translate 'Detected reset method' ) : \Zb ${ reset_method } \Zn\n\n"
msg += " $( translate 'For dedicated GPUs, FLR is required by this policy to reduce VM' ) \n"
msg += " $( translate 'start/restart failures and reset instability.' ) \n\n"
msg += "\Zb $( translate 'Configuration has been stopped due to high reset risk.' ) \Zn"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Reset Capability Blocked' ) " " $msg " 20 80
2026-04-04 00:54:57 +02:00
exit 0
fi
# ── WARNING: Intel subtype unknown and reset is not FLR ──────────────
if [[ " $gpu_subtype " == "unknown" && " $reset_method " != "flr" ]] ; then
local msg
msg = "\n\Z4\Zb $( translate 'Warning: Limited PCI Reset Support' ) \Zn\n\n"
msg += " $( translate 'The selected Intel GPU has non-FLR reset support and unknown subtype' ) :\n"
msg += " $( translate 'Detected subtype' ) : \Zb ${ gpu_subtype } \Zn\n"
msg += " $( translate 'Detected reset method' ) : \Zb ${ reset_method } \Zn\n\n"
msg += " $( translate 'Passthrough may work, but startup/restart reliability is not guaranteed.' ) \n\n"
msg += " $( translate 'Do you want to continue anyway?' ) "
2026-04-12 20:32:34 +02:00
_pmx_yesno " $( translate 'Reset Capability Warning' ) " " $msg " 18 78
2026-04-04 00:54:57 +02:00
[[ $? -ne 0 ]] && exit 0
fi
}
check_gpu_vm_compatibility() {
[[ " $SELECTED_GPU " != "amd" && " $SELECTED_GPU " != "intel" ]] && return 0
if [[ " $SELECTED_GPU " == "intel" ]] ; then
check_intel_vm_compatibility
return 0
fi
local pci_full = " $SELECTED_GPU_PCI "
local gpu_subtype reset_method power_state
gpu_subtype = $( _detect_amd_gpu_subtype)
reset_method = $( _check_pci_reset_method " $pci_full " )
power_state = $( cat "/sys/bus/pci/devices/ ${ pci_full } /power_state" 2>/dev/null | tr -d '[:space:]' )
# ── BLOCKER: AMD device currently in D3cold ──────────────────────────
# D3cold on AMD passthrough candidates is a high-risk state for VM use.
# In practice this often leads to failed power-up/reset when QEMU starts.
if [[ " $power_state " == "D3cold" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'GPU Not Available for Reliable VM Passthrough' ) \Zn\n\n"
msg += " $( translate 'The selected AMD GPU is currently in power state D3cold' ) :\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n\n"
msg += " $( translate 'Detected power state' ) : \Zb ${ power_state } \Zn\n\n"
msg += " $( translate 'This state indicates a high risk of passthrough failure due to' ) :\n"
msg += " • $( translate 'Inaccessible device during VM startup' ) \n"
msg += " • $( translate 'Failed transitions from D3cold to D0' ) \n"
msg += " • $( translate 'Potential QEMU startup/assertion failures' ) \n\n"
msg += "\Zb $( translate 'Configuration has been stopped to prevent an unusable VM state.' ) \Zn"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'High-Risk GPU Power State' ) " " $msg " 22 80
2026-04-04 00:54:57 +02:00
exit 0
fi
# ── BLOCKER: AMD APU without FLR reset ───────────────────────────────
# Validated in testing: Lucienne/Renoir/Cezanne + bus-only reset →
# "write error: Inappropriate ioctl for device" on PCI reset
# "Unable to change power state from D3cold to D0"
# QEMU pci_irq_handler assertion failure → VM does not start
if [[ " $gpu_subtype " == "apu" && " $reset_method " != "flr" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'GPU Not Compatible with VM Passthrough' ) \Zn\n\n"
msg += " $( translate 'An AMD integrated GPU (APU) has been detected' ) :\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n\n"
msg += " $( translate 'Although VFIO can bind to this device, full passthrough to a VM is' ) \n"
msg += " $( translate 'not reliable on this hardware due to the following limitations' ) :\n\n"
msg += " • $( translate 'PCI reset method' ) : \Zb ${ reset_method } \Zn"
msg += " — $( translate 'Function Level Reset (FLR) not available' ) \n"
msg += " • $( translate 'SoC-integrated GPU: tight coupling with other SoC components' ) \n"
msg += " • $( translate 'Power state D3cold/D0 transitions may be inaccessible' ) \n"
[[ " $power_state " == "D3cold" ]] && \
2026-04-06 21:50:46 +02:00
msg += " • \Z1 $( translate 'Current power state: D3cold (device currently inaccessible)' ) \Zn\n"
2026-04-04 00:54:57 +02:00
msg += "\n $( translate 'Attempting passthrough with this GPU typically results in' ) :\n"
msg += " — write error: Inappropriate ioctl for device\n"
msg += " — Unable to change power state from D3cold to D0\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"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Incompatible GPU for VM Passthrough' ) " " $msg " 26 80
2026-04-04 00:54:57 +02:00
exit 0
fi
# ── BLOCKER: AMD dedicated GPU without FLR reset ─────────────────────
# User policy: for dGPU + no FLR, do not continue automatically.
if [[ " $gpu_subtype " == "dedicated" && " $reset_method " != "flr" ]] ; then
local msg
msg = "\n\Zb\Z1 $( translate 'Incompatible Reset Capability for AMD dGPU' ) \Zn\n\n"
msg += " $( translate 'An AMD dedicated GPU has been detected without FLR support' ) :\n"
msg += " ${ SELECTED_GPU_NAME } \n"
msg += " ${ SELECTED_GPU_PCI } \n\n"
msg += " $( translate 'Detected reset method' ) : \Zb ${ reset_method } \Zn\n\n"
msg += " $( translate 'Without Function Level Reset (FLR), passthrough is not considered reliable' ) \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"
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Reset Capability Blocked' ) " " $msg " 20 80
2026-04-04 00:54:57 +02:00
exit 0
fi
# ── WARNING: Unknown AMD subtype without FLR ─────────────────────────
# Keep optional path for unknown classifications only.
if [[ " $gpu_subtype " == "unknown" && " $reset_method " != "flr" ]] ; then
local msg
msg = "\n\Z4\Zb $( translate 'Warning: Limited PCI Reset Support' ) \Zn\n\n"
msg += " $( translate 'The selected AMD GPU does not report FLR reset support' ) :\n"
msg += " $( translate 'Detected subtype' ) : \Zb ${ gpu_subtype } \Zn\n"
msg += " $( translate 'Detected reset method' ) : \Zb ${ reset_method } \Zn\n\n"
msg += " $( translate 'Passthrough may fail depending on hardware/firmware implementation.' ) \n\n"
msg += " $( translate 'Do you want to continue anyway?' ) "
2026-04-12 20:32:34 +02:00
_pmx_yesno " $( translate 'Reset Capability Warning' ) " " $msg " 18 78
2026-04-04 00:54:57 +02:00
[[ $? -ne 0 ]] && exit 0
fi
}
# ==========================================================
# Phase 1 — Step 5: IOMMU group analysis
# ==========================================================
analyze_iommu_group() {
local pci_full = " $SELECTED_GPU_PCI "
local group_link = "/sys/bus/pci/devices/ ${ pci_full } /iommu_group"
if [[ ! -L " $group_link " ]] ; then
2026-04-06 19:13:31 +02:00
if [[ " $IOMMU_PENDING_REBOOT " == "true" ]] ; then
IOMMU_GROUP = "pending-reboot"
IOMMU_DEVICES =( " $pci_full " )
IOMMU_VFIO_IDS =()
local vid did
vid = $( cat "/sys/bus/pci/devices/ ${ pci_full } /vendor" 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 } " )
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( 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 } " \
2026-04-06 19:13:31 +02:00
14 82
return 0
fi
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( 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.' ) " \
2026-04-04 00:54:57 +02:00
10 72
exit 1
fi
IOMMU_GROUP = $( basename " $( readlink " $group_link " ) " )
IOMMU_DEVICES =()
IOMMU_VFIO_IDS =()
local group_dir = "/sys/kernel/iommu_groups/ ${ IOMMU_GROUP } /devices"
local display_lines = ""
local extra_devices = 0
for dev_path in " ${ group_dir } /" *; do
[[ -e " $dev_path " ]] || continue
local dev
dev = $( basename " $dev_path " )
# Skip PCI bridges and host bridges (class 0x0604 / 0x0600)
local dev_class
dev_class = $( cat "/sys/bus/pci/devices/ ${ dev } /class" 2>/dev/null)
2026-05-09 18:59:59 +02:00
if [[ " $dev_class " == 0x0604* || " $dev_class " == 0x0600* ]] ; then
2026-04-04 00:54:57 +02:00
continue
fi
IOMMU_DEVICES +=( " $dev " )
# Collect vendor:device ID
local vid did
vid = $( cat "/sys/bus/pci/devices/ ${ dev } /vendor" 2>/dev/null | sed 's/0x//' )
did = $( cat "/sys/bus/pci/devices/ ${ dev } /device" 2>/dev/null | sed 's/0x//' )
[[ -n " $vid " && -n " $did " ]] && IOMMU_VFIO_IDS +=( " ${ vid } : ${ did } " )
# Build display line
local dev_name dev_driver
dev_name = $( lspci -nn -s " ${ dev #0000: } " 2>/dev/null | sed 's/^[^ ]* //' | cut -c1-52)
dev_driver = $( _get_pci_driver " $dev " )
display_lines += " • ${ dev } ${ dev_name } [ ${ dev_driver } ]\n"
[[ " $dev " != " $pci_full " ]] && extra_devices = $(( extra_devices + 1 ))
done
}
2026-04-21 21:06:22 +02:00
# Returns 0 if the BDF at $1 is a real PCI audio device (class 04xx).
_pci_is_audio_device() {
local bdf = " $1 "
[[ -n " $bdf " ]] || return 1
local dev_path = "/sys/bus/pci/devices/ ${ bdf } "
[[ -d " $dev_path " ]] || return 1
2026-04-05 11:24:08 +02:00
local class_hex
class_hex = $( cat " ${ dev_path } /class" 2>/dev/null | sed 's/^0x//' )
2026-04-21 21:06:22 +02:00
[[ " ${ class_hex : 0 : 2 } " == "04" ]]
}
2026-04-05 11:24:08 +02:00
2026-04-21 21:06:22 +02:00
# Registers an audio BDF for passthrough alongside the GPU.
# Idempotent: skips if the BDF was already recorded by analyze_iommu_group
# (IOMMU_DEVICES) or by a previous call here (EXTRA_AUDIO_DEVICES).
# Updates EXTRA_AUDIO_DEVICES, EXTRA_AUDIO_INFO, and IOMMU_VFIO_IDS.
_register_gpu_audio_device() {
local bdf = " $1 "
[[ -n " $bdf " ]] || return 1
local dev_path = "/sys/bus/pci/devices/ ${ bdf } "
[[ -d " $dev_path " ]] || return 1
local dev
2026-04-05 11:24:08 +02:00
for dev in " ${ IOMMU_DEVICES [@] } " ; do
2026-04-21 21:06:22 +02:00
[[ " $dev " == " $bdf " ]] && return 0
done
for dev in " ${ EXTRA_AUDIO_DEVICES [@] } " ; do
[[ " $dev " == " $bdf " ]] && return 0
2026-04-05 11:24:08 +02:00
done
2026-04-21 21:06:22 +02:00
EXTRA_AUDIO_DEVICES +=( " $bdf " )
local drv
drv = $( _get_pci_driver " $bdf " )
EXTRA_AUDIO_INFO +=( " ${ bdf } | ${ drv } " )
2026-04-05 11:24:08 +02:00
local vid did new_id
vid = $( cat " ${ dev_path } /vendor" 2>/dev/null | sed 's/0x//' )
did = $( cat " ${ dev_path } /device" 2>/dev/null | sed 's/0x//' )
if [[ -n " $vid " && -n " $did " ]] ; then
new_id = " ${ vid } : ${ did } "
if _append_unique " $new_id " " ${ IOMMU_VFIO_IDS [@] } " ; then
IOMMU_VFIO_IDS +=( " $new_id " )
fi
fi
2026-04-21 21:06:22 +02:00
return 0
}
# Scans the host for all class-04 PCI audio devices and lets the user
# pick which ones to pass to the VM. Only invoked when the selected GPU
# has no .1 sibling audio function — the dGPU fast path continues to
# auto-include that sibling without prompting.
#
# Devices already in the GPU's IOMMU group are excluded from the list
# (analyze_iommu_group has already queued them). The checklist defaults
# to all-OFF so nothing gets passed through silently.
_prompt_user_for_audio_devices() {
# Collect eligible audio BDFs from sysfs.
local -a candidates =()
local dev_path bdf
for dev_path in /sys/bus/pci/devices/*; do
[[ -d " $dev_path " ]] || continue
bdf = $( basename " $dev_path " )
_pci_is_audio_device " $bdf " || continue
# Skip ones already queued by the IOMMU group sweep.
local skip = false dev
for dev in " ${ IOMMU_DEVICES [@] } " ; do
[[ " $dev " == " $bdf " ]] && { skip = true; break; }
done
$skip && continue
candidates +=( " $bdf " )
done
[[ ${# candidates [@] } -eq 0 ]] && return 0
# Build checklist items: tag=BDF, description="<name> (driver: X)".
local -a items =()
local name drv label
for bdf in " ${ candidates [@] } " ; do
name = $( lspci -nn -s " ${ bdf #0000: } " 2>/dev/null \
| sed 's/^[^ ]* //' \
| sed 's/ \[0401\]//; s/ \[0403\]//; s/ \[0400\]//' \
| cut -c1-52)
[[ -z " $name " ]] && name = "PCI audio"
drv = $( _get_pci_driver " $bdf " )
label = " ${ name } (driver: ${ drv } )"
items +=( " $bdf " " $label " "off" )
done
local prompt selection dialog_h list_h
prompt = " $( translate 'The selected GPU has no dedicated .1 audio sibling function.' ) \n"
prompt += " $( translate 'If you want HDMI/analog audio inside the VM, select the audio controller(s) to pass through along with the GPU.' ) \n\n"
prompt += " $( translate 'Default is none (video-only passthrough). Use SPACE to toggle selections.' ) "
# Give the list area a floor of 4 rows so a single candidate doesn't
# render cramped under the description. Overall dialog height scales
# with that floor + room for the 4-line prompt, blank line, borders
# and button row.
list_h = ${# candidates [@] }
(( list_h < 4 )) && list_h = 4
dialog_h = $(( list_h + 14 ))
selection = $( _pmx_checklist \
" $( translate 'Add Audio Passthrough' ) " \
" $prompt " \
" $dialog_h " 82 " $list_h " \
" ${ items [@] } " ) || return 0
# dialog wraps selected tags in quotes, whiptail does not — _strip them.
selection = $( echo " $selection " | tr -d '"' )
[[ -z " $selection " ]] && return 0
local picked
for picked in $selection ; do
_register_gpu_audio_device " $picked "
done
}
detect_optional_gpu_audio() {
EXTRA_AUDIO_DEVICES =()
EXTRA_AUDIO_INFO =()
# Fast path: dGPUs (NVIDIA / AMD discrete) and some APUs expose audio
# as function .1 of the same slot. When present, auto-include it —
# this is the unambiguous, always-safe case because such audio only
# outputs through the GPU's own ports and was never used by the host.
local sibling_audio = " ${ SELECTED_GPU_PCI %.* } .1"
if _pci_is_audio_device " $sibling_audio " ; then
_register_gpu_audio_device " $sibling_audio "
return 0
fi
# Slow path: no sibling audio (typical for Intel iGPUs whose HDMI
# audio lives on the PCH, or setups with an external sound card).
# Ask the user explicitly via checklist — the decision of whether to
# pass chipset audio alongside an iGPU is intentional, not automatic.
_prompt_user_for_audio_devices
2026-04-05 11:24:08 +02:00
}
2026-04-04 00:54:57 +02:00
# ==========================================================
# Phase 1 — Step 6: VM selection
# ==========================================================
select_vm() {
2026-04-05 11:24:08 +02:00
if [[ -n " $PRESELECT_VMID " ]] ; then
if qm config " $PRESELECT_VMID " >/dev/null 2>& 1; then
SELECTED_VMID = " $PRESELECT_VMID "
VM_NAME = $( qm config " $SELECTED_VMID " 2>/dev/null | grep "^name:" | awk '{print $2}' )
return 0
fi
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Invalid VMID' ) " \
"\n $( translate 'The preselected VMID does not exist on this host:' ) ${ PRESELECT_VMID } " 9 72
2026-04-05 11:24:08 +02:00
exit 1
fi
2026-04-04 00:54:57 +02:00
local menu_items =()
while IFS = read -r line; do
[[ " $line " = ~ ^[[ :space:]] *VMID ]] && continue
local vmid name status
vmid = $( echo " $line " | awk '{print $1}' )
name = $( echo " $line " | awk '{print $2}' )
status = $( echo " $line " | awk '{print $3}' )
[[ -z " $vmid " || ! " $vmid " = ~ ^[ 0-9] +$ ]] && continue
menu_items +=( " $vmid " " ${ name :- VM - ${ vmid }} ( ${ status } )" )
done < <( qm list 2>/dev/null)
if [[ ${# menu_items [@] } -eq 0 ]] ; then
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( 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.' ) " \
2026-04-04 00:54:57 +02:00
10 68
exit 0
fi
2026-04-12 20:32:34 +02:00
SELECTED_VMID = $( _pmx_menu \
" $( translate 'Select Virtual Machine' ) " \
"\n $( translate 'Select the VM to add the GPU to:' ) " \
2026-04-04 00:54:57 +02:00
20 72 12 \
2026-04-12 20:32:34 +02:00
" ${ menu_items [@] } " ) || exit 0
2026-04-04 00:54:57 +02:00
VM_NAME = $( qm config " $SELECTED_VMID " 2>/dev/null | grep "^name:" | awk '{print $2}' )
}
# ==========================================================
# Phase 1 — Step 7: Machine type check (must be q35)
# ==========================================================
check_vm_machine_type() {
local machine_line
machine_line = $( qm config " $SELECTED_VMID " 2>/dev/null | grep "^machine:" | awk '{print $2}' )
if echo " $machine_line " | grep -q "q35" ; then
return 0
fi
local msg
msg = "\n $( translate 'The selected VM' ) \" ${ VM_NAME } \" ( ${ SELECTED_VMID } ) "
msg += " $( translate 'is not configured as machine type q35.' ) \n\n"
msg += " $( translate 'PCIe GPU passthrough requires:' ) \n"
msg += " • $( translate 'Machine type: q35' ) \n"
msg += " • $( translate 'BIOS: OVMF (UEFI)' ) \n\n"
msg += " $( translate 'Changing the machine type on an existing installed VM is not safe: it changes the chipset and PCI slot layout, which typically prevents the guest OS from booting.' ) \n\n"
msg += " $( translate 'To use GPU passthrough, please create a new VM configured with:' ) \n"
msg += " • $( translate 'Machine: q35' ) \n"
msg += " • $( translate 'BIOS: OVMF (UEFI)' ) \n"
msg += " • $( translate 'Storage controller: VirtIO SCSI' ) "
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'Incompatible Machine Type' ) " " $msg " 20 78
2026-04-04 00:54:57 +02:00
exit 0
}
# ==========================================================
# Phase 1 — Step 8: Switch mode detection
# ==========================================================
check_switch_mode() {
local pci_slot = " ${ SELECTED_GPU_PCI #0000: } " # 01:00.0
pci_slot = " ${ pci_slot %.* } " # 01:00
# ── LXC conflict check ────────────────────────────────
2026-04-06 13:39:07 +02:00
LXC_AFFECTED_CTIDS =()
LXC_AFFECTED_NAMES =()
LXC_AFFECTED_RUNNING =()
LXC_AFFECTED_ONBOOT =()
LXC_SWITCH_ACTION = ""
2026-04-04 00:54:57 +02:00
local lxc_affected =()
2026-04-06 13:39:07 +02:00
local running_count = 0
local onboot_count = 0
2026-04-04 00:54:57 +02:00
for conf in /etc/pve/lxc/*.conf; do
[[ -f " $conf " ]] || continue
2026-04-06 13:39:07 +02:00
_lxc_conf_uses_selected_gpu " $conf " || continue
local ctid ct_name running_flag onboot_flag
ctid = $( basename " $conf " .conf)
ct_name = $( pct config " $ctid " 2>/dev/null | awk '/^hostname:/ {print $2}' )
[[ -z " $ct_name " ]] && ct_name = "CT- ${ ctid } "
running_flag = 0
onboot_flag = 0
_ct_is_running " $ctid " && running_flag = 1
_ct_onboot_enabled " $ctid " && onboot_flag = 1
LXC_AFFECTED_CTIDS +=( " $ctid " )
LXC_AFFECTED_NAMES +=( " $ct_name " )
LXC_AFFECTED_RUNNING +=( " $running_flag " )
LXC_AFFECTED_ONBOOT +=( " $onboot_flag " )
lxc_affected +=( "CT ${ ctid } ( ${ ct_name } )" )
[[ " $running_flag " == "1" ]] && running_count = $(( running_count + 1 ))
[[ " $onboot_flag " == "1" ]] && onboot_count = $(( onboot_count + 1 ))
2026-04-04 00:54:57 +02:00
done
if [[ ${# lxc_affected [@] } -gt 0 ]] ; then
SWITCH_FROM_LXC = true
SWITCH_LXC_LIST = $( IFS = ', ' ; echo " ${ lxc_affected [*] } " )
2026-04-06 13:39:07 +02:00
local msg action_choice
msg = "\n $( translate 'The selected GPU is currently used by the following LXC container(s):' ) \n\n"
local i
for i in " ${ !LXC_AFFECTED_CTIDS[@] } " ; do
local status_txt onboot_txt
status_txt = " $( translate 'stopped' ) "
onboot_txt = "onboot=0"
[[ " ${ LXC_AFFECTED_RUNNING [ $i ] } " == "1" ]] && status_txt = " $( translate 'running' ) "
[[ " ${ LXC_AFFECTED_ONBOOT [ $i ] } " == "1" ]] && onboot_txt = "onboot=1"
msg += " • CT ${ LXC_AFFECTED_CTIDS [ $i ] } ( ${ LXC_AFFECTED_NAMES [ $i ] } ) [ ${ status_txt } , ${ onboot_txt } ]\n"
2026-04-04 00:54:57 +02:00
done
msg += "\n $( translate 'VM passthrough requires exclusive VFIO binding of the GPU.' ) \n"
2026-04-06 13:39:07 +02:00
msg += " $( translate 'Choose how to handle affected LXC containers before switching to VM mode.' ) \n\n"
[[ " $running_count " -gt 0 ]] && \
2026-04-06 21:50:46 +02:00
msg += "\Z1 $( translate 'Running containers detected' ) : ${ running_count } \Zn\n"
2026-04-06 13:39:07 +02:00
[[ " $onboot_count " -gt 0 ]] && \
msg += "\Z1\Zb $( translate 'Start on boot enabled (onboot=1)' ) : ${ onboot_count } \Zn\n"
2026-04-06 21:50:46 +02:00
msg += "\n\Z1 $( translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.' ) \Zn"
2026-04-04 00:54:57 +02:00
2026-04-12 20:32:34 +02:00
action_choice = $( _pmx_menu --default-item "2" \
" $( translate 'GPU Used in LXC Containers' ) " \
" $msg " 25 96 8 \
2026-04-06 13:39:07 +02:00
"1" " $( translate 'Keep GPU in LXC config (disable Start on boot)' ) " \
2026-04-12 20:32:34 +02:00
"2" " $( translate 'Remove GPU from LXC config (keep Start on boot)' ) " ) || exit 0
2026-04-06 13:39:07 +02:00
case " $action_choice " in
1) LXC_SWITCH_ACTION = "keep_gpu_disable_onboot" ;;
2) LXC_SWITCH_ACTION = "remove_gpu_keep_onboot" ;;
*) exit 0 ;;
esac
else
SWITCH_FROM_LXC = false
2026-04-04 00:54:57 +02:00
fi
# ── VM conflict check (different VM than selected) ────
local vm_src_id = "" vm_src_name = ""
for conf in /etc/pve/qemu-server/*.conf; do
[[ -f " $conf " ]] || continue
local vmid
vmid = $( basename " $conf " .conf)
[[ " $vmid " == " $SELECTED_VMID " ]] && continue # same target VM, no conflict
if grep -qE "hostpci[0-9]+:.* ${ pci_slot } " " $conf " ; then
vm_src_id = " $vmid "
vm_src_name = $( grep "^name:" " $conf " 2>/dev/null | awk '{print $2}' )
break
fi
done
if [[ -n " $vm_src_id " ]] ; then
2026-04-05 11:24:08 +02:00
local src_running = false
_vm_is_running " $vm_src_id " && src_running = true
if [[ " $src_running " == "true" ]] ; then
local msg
msg = "\n $( translate 'The selected GPU is already assigned to another VM that is currently running:' ) \n\n"
msg += " VM ${ vm_src_id } ( ${ vm_src_name :- VM - ${ vm_src_id }} )\n\n"
msg += " $( translate 'The same GPU cannot be used by two VMs at the same time.' ) \n\n"
msg += " $( translate 'Next step: stop that VM first, then run' ) \n"
msg += " Hardware Graphics → Add GPU to VM\n"
msg += " $( translate 'to move the GPU safely.' ) "
2026-04-12 20:32:34 +02:00
_pmx_msgbox " $( translate 'GPU Busy in Running VM' ) " " $msg " 16 78
2026-04-05 11:24:08 +02:00
exit 0
fi
2026-04-04 00:54:57 +02:00
SWITCH_FROM_VM = true
SWITCH_VM_SRC = " $vm_src_id "
2026-04-06 21:50:46 +02:00
SWITCH_VM_ACTION = "remove_gpu_keep_onboot"
2026-04-05 11:24:08 +02:00
local selected_driver
selected_driver = $( _get_pci_driver " $SELECTED_GPU_PCI " )
if [[ " $selected_driver " == "vfio-pci" && " $SWITCH_FROM_LXC " != "true" ]] ; then
VM_SWITCH_ALREADY_VFIO = true
fi
local src_onboot target_onboot
src_onboot = "0"
target_onboot = "0"
_vm_onboot_enabled " $vm_src_id " && src_onboot = "1"
_vm_onboot_enabled " $SELECTED_VMID " && target_onboot = "1"
2026-04-04 00:54:57 +02:00
local msg
msg = "\n $( translate 'The selected GPU is already configured for passthrough to:' ) \n\n"
msg += " VM ${ vm_src_id } ( ${ vm_src_name :- VM - ${ vm_src_id }} )\n\n"
2026-04-05 11:24:08 +02:00
msg += " $( translate 'That VM is currently stopped, so the GPU can be reassigned now.' ) \n"
2026-04-06 21:50:46 +02:00
msg += "\Z1\Zb $( translate 'Important: both VMs cannot be running at the same time with the same GPU.' ) \Zn\n\n"
msg += " $( translate 'Target VM' ) : VM ${ SELECTED_VMID } ( ${ VM_NAME :- VM - ${ SELECTED_VMID }} )\n"
msg += " $( translate 'Source VM' ) : VM ${ vm_src_id } ( ${ vm_src_name :- VM - ${ vm_src_id }} )\n\n"
2026-04-05 11:24:08 +02:00
if [[ " $src_onboot " == "1" && " $target_onboot " == "1" ]] ; then
2026-04-06 21:50:46 +02:00
msg += "\Z1\Zb $( translate 'Warning: both VMs have autostart enabled (onboot=1).' ) \Zn\n"
msg += "\Z1\Zb $( translate 'Disable autostart on one VM to avoid startup conflicts.' ) \Zn\n\n"
2026-04-05 11:24:08 +02:00
fi
if [[ " $VM_SWITCH_ALREADY_VFIO " == "true" ]] ; then
msg += " $( translate 'Host GPU is already bound to vfio-pci. Host reconfiguration/reboot should not be required for this VM-to-VM reassignment.' ) \n\n"
fi
2026-04-06 21:50:46 +02:00
msg += " $( translate 'Choose conflict policy for the source VM:' ) "
2026-04-04 00:54:57 +02:00
2026-04-06 22:34:37 +02:00
local vm_action_choice
2026-04-12 20:32:34 +02:00
vm_action_choice = $( _pmx_menu --default-item "1" \
" $( translate 'GPU Already Assigned to Another VM' ) " \
" $msg " 24 84 8 \
2026-04-06 22:21:38 +02:00
"1" " $( translate 'Keep GPU in source VM config (disable Start on boot if enabled)' ) " \
2026-04-12 20:32:34 +02:00
"2" " $( translate 'Remove GPU from source VM config (keep Start on boot)' ) " ) || exit 0
2026-04-06 21:50:46 +02:00
case " $vm_action_choice " in
1) SWITCH_VM_ACTION = "keep_gpu_disable_onboot" ;;
2) SWITCH_VM_ACTION = "remove_gpu_keep_onboot" ;;
*) exit 0 ;;
esac
2026-04-04 00:54:57 +02:00
fi
}
# ==========================================================
# Phase 1 — Step 9: Confirmation summary
# ==========================================================
confirm_summary() {
local msg
msg = "\n $( translate 'The following changes will be applied' ) :\n"
msg += "\n ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
msg += " $( translate 'GPU' ) : ${ SELECTED_GPU_NAME } \n"
msg += " $( translate 'PCI Address' ) : ${ SELECTED_GPU_PCI } \n"
2026-04-06 19:13:31 +02:00
if [[ " $IOMMU_PENDING_REBOOT " == "true" ]] ; then
msg += " $( translate 'IOMMU Group' ) : $( translate 'pending (reboot required to enumerate full group)' ) \n"
else
msg += " $( translate 'IOMMU Group' ) : ${ IOMMU_GROUP } ( ${# IOMMU_DEVICES [@] } $( translate 'devices' ) )\n"
fi
2026-04-04 00:54:57 +02:00
msg += " $( translate 'Target VM' ) : ${ VM_NAME :- VM - ${ SELECTED_VMID }} ( ${ SELECTED_VMID } )\n"
msg += " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
msg += " \Zb $( translate 'Host' ) :\Zn\n"
2026-04-05 11:24:08 +02:00
if [[ " $PREFLIGHT_HOST_REBOOT_REQUIRED " != "true" ]] ; then
msg += " • $( translate 'Host VFIO configuration already up to date' ) \n"
msg += " • $( translate 'No host VFIO reconfiguration expected' ) \n"
msg += " • $( translate 'No host reboot expected' ) \n\n"
else
msg += " • $( translate 'VFIO modules in /etc/modules' ) \n"
msg += " • $( translate 'vfio-pci IDs in /etc/modprobe.d/vfio.conf' ) \n"
[[ " $SELECTED_GPU " == "amd" ]] && \
msg += " • $( translate 'AMD softdep configured' ) \n"
[[ " $SELECTED_GPU " == "amd" ]] && \
msg += " • $( translate 'GPU ROM dump to /usr/share/kvm/' ) \n"
msg += " • $( translate 'GPU driver blacklisted' ) \n"
msg += " • $( translate 'initramfs updated' ) \n"
msg += " • \Zb $( translate 'System reboot required' ) \Zn\n\n"
fi
2026-04-04 00:54:57 +02:00
msg += " \Zb $( translate 'VM' ) ${ SELECTED_VMID } :\Zn\n"
[[ " $TARGET_VM_ALREADY_HAS_GPU " == "true" ]] && \
msg += " • $( translate 'Existing hostpci entries detected — they will be reused' ) \n"
msg += " • $( translate 'Virtual display normalized to vga: std (compatibility)' ) \n"
2026-04-06 19:13:31 +02:00
if [[ " $IOMMU_PENDING_REBOOT " == "true" ]] ; then
msg += " • $( translate 'hostpci entries for selected GPU functions (full IOMMU group will be enforced after reboot)' ) \n"
else
msg += " • $( translate 'hostpci entries for all IOMMU group devices' ) \n"
fi
2026-04-21 21:06:22 +02:00
if [[ ${# EXTRA_AUDIO_DEVICES [@] } -gt 0 ]] ; then
msg += " • $( translate 'Additional audio function(s) to be added' ) :\n"
local _audio_info _audio_bdf _audio_drv
for _audio_info in " ${ EXTRA_AUDIO_INFO [@] } " ; do
_audio_bdf = " ${ _audio_info %%|* } "
_audio_drv = " ${ _audio_info #*| } "
if [[ -n " $_audio_drv " && " $_audio_drv " != "none" && " $_audio_drv " != "vfio-pci" ]] ; then
msg += " • ${ _audio_bdf } \Zb( ${ _audio_drv } )\Zn\n"
else
msg += " • ${ _audio_bdf } \n"
fi
done
fi
2026-04-04 00:54:57 +02:00
[[ " $SELECTED_GPU " == "nvidia" ]] && \
msg += " • $( translate 'NVIDIA KVM hiding (cpu hidden=1)' ) \n"
2026-04-06 13:39:07 +02:00
if [[ " $SWITCH_FROM_LXC " == "true" ]] ; then
2026-04-06 21:50:46 +02:00
msg += "\n \Z1• $( translate 'Affected LXC containers' ) : ${ SWITCH_LXC_LIST } \Zn\n"
msg += " \Z1• $( translate 'Selected LXC action' ) : $( _lxc_switch_action_label) \Zn\n"
2026-04-06 13:39:07 +02:00
if [[ " $LXC_SWITCH_ACTION " == "remove_gpu_keep_onboot" ]] ; then
2026-04-06 21:50:46 +02:00
msg += " \Z1• $( translate 'To use the GPU again in LXC, run Add GPU to LXC from GPUs and Coral-TPU Menu' ) \Zn\n"
2026-04-06 13:39:07 +02:00
fi
fi
2026-04-06 21:50:46 +02:00
if [[ " $SWITCH_FROM_VM " == "true" ]] ; then
if [[ " $SWITCH_VM_ACTION " == "keep_gpu_disable_onboot" ]] ; then
msg += "\n \Z1• $( translate 'GPU will remain configured in source VM' ) : ${ SWITCH_VM_SRC } \Zn\n"
msg += " \Z1• $( translate 'Start on boot will be disabled on source VM only if currently enabled' ) \Zn\n"
msg += " \Z1• $( translate 'GPU guard hook will block concurrent start when another VM is already using this GPU' ) \Zn\n"
else
msg += "\n \Z1• $( translate 'GPU will be removed from source VM config' ) : ${ SWITCH_VM_SRC } \Zn\n"
fi
msg += " \Z1• $( translate 'Selected VM action' ) : $( _vm_switch_action_label) \Zn\n"
fi
2026-04-04 00:54:57 +02:00
msg += "\n $( translate 'Do you want to proceed?' ) "
local run_title
run_title = $( _get_vm_run_title)
2026-04-12 20:32:34 +02:00
_pmx_yesno " ${ run_title } " " $msg " 28 78
2026-04-06 22:34:37 +02:00
[[ $? -ne 0 ]] && exit 0
2026-04-04 00:54:57 +02:00
}
# ==========================================================
# Phase 2 — Processing
# ==========================================================
# ── VFIO modules in /etc/modules ─────────────────────────
add_vfio_modules() {
msg_info " $( translate 'Configuring VFIO modules...' ) "
local modules =( "vfio" "vfio_iommu_type1" "vfio_pci" )
local kernel_major kernel_minor
kernel_major = $( uname -r | cut -d. -f1)
kernel_minor = $( uname -r | cut -d. -f2)
if (( kernel_major < 6 || ( kernel_major == 6 && kernel_minor < 2 ) )) ; then
modules +=( "vfio_virqfd" )
fi
for mod in " ${ modules [@] } " ; do
_add_line_if_missing " $mod " /etc/modules
done
msg_ok " $( translate 'VFIO modules configured in /etc/modules' ) " | tee -a " $screen_capture "
}
# ── vfio-pci IDs — merge with existing ones ─────────────
configure_vfio_pci_ids() {
2026-05-28 20:49:59 +02:00
msg_info " $( translate 'Configuring vfio-pci binding...' ) "
2026-04-04 00:54:57 +02:00
local vfio_conf = "/etc/modprobe.d/vfio.conf"
touch " $vfio_conf "
2026-05-28 20:49:59 +02:00
# ────────────────────────────────────────────────────────────────
# NVIDIA: per-BDF binding (multi-GPU safe). The `options vfio-pci
# ids=VENDOR:DEVICE` approach captures EVERY GPU with the same
# vendor:device ID — fatal when two NVIDIA GPUs share a model.
# Instead, we list the exact BDF(s) of the target GPU in the
# initramfs hook, and add `softdep nvidia pre: vfio-pci` so vfio
# has a chance to claim the BDF before nvidia loads.
# ────────────────────────────────────────────────────────────────
if [[ " $SELECTED_GPU " == "nvidia" ]] ; then
# Clean up any previous ids= line that captured this NVIDIA
# (older versions of this script wrote it; remove to avoid
# collateral grabs on sibling GPUs of the same model).
if grep -qE '^options vfio-pci ids=' " $vfio_conf " 2>/dev/null; then
local existing_line ids_part
existing_line = $( grep '^options vfio-pci ids=' " $vfio_conf " | head -1)
ids_part = $( echo " $existing_line " | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//' )
local kept =()
IFS = ',' read -ra existing_ids <<< " $ids_part "
for eid in " ${ existing_ids [@] } " ; do
local drop = false
for nvid in " ${ IOMMU_VFIO_IDS [@] } " ; do
[[ " $eid " == " $nvid " ]] && drop = true && break
done
$drop || kept +=( " $eid " )
done
sed -i '/^options vfio-pci ids=/d' " $vfio_conf "
if [[ ${# kept [@] } -gt 0 ]] ; then
local kept_str
kept_str = $( IFS = ',' ; echo " ${ kept [*] } " )
echo "options vfio-pci ids= ${ kept_str } disable_vga=1" >> " $vfio_conf "
fi
HOST_CONFIG_CHANGED = true
fi
# Ensure vfio loads before nvidia so the per-BDF override wins.
_add_line_if_missing "softdep nvidia pre: vfio-pci" " $vfio_conf "
_add_line_if_missing "softdep nvidia_drm pre: vfio-pci" " $vfio_conf "
_add_line_if_missing "softdep nvidia_modeset pre: vfio-pci" " $vfio_conf "
_add_line_if_missing "softdep nvidia_uvm pre: vfio-pci" " $vfio_conf "
# Per-BDF binder hook. IOMMU_DEVICES has the BDFs for the GPU
# we're passing (and any same-group functions like the audio
# function). Add all of them so the whole IOMMU group goes to
# vfio-pci as Proxmox expects.
local -a bdfs_to_bind =()
for bdf in " ${ IOMMU_DEVICES [@] } " ; do
bdfs_to_bind +=( " $bdf " )
done
_proxmenux_vfio_bind_add_bdfs " ${ bdfs_to_bind [@] } "
msg_ok " $( translate 'NVIDIA per-BDF VFIO binding configured' ) ( ${ bdfs_to_bind [*] } )" | tee -a " $screen_capture "
return 0
fi
# ────────────────────────────────────────────────────────────────
# AMD / Intel: keep the legacy options vfio-pci ids= approach.
# These vendors rarely run multi-GPU same-model on the same host,
# and their drivers don't have the kill-switch problem nvidia has.
# ────────────────────────────────────────────────────────────────
2026-04-04 00:54:57 +02:00
# Collect existing IDs (if any)
local existing_ids =()
local existing_line
existing_line = $( grep "^options vfio-pci ids=" " $vfio_conf " 2>/dev/null | head -1)
if [[ -n " $existing_line " ]] ; then
local ids_part
ids_part = $( echo " $existing_line " | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//' )
IFS = ',' read -ra existing_ids <<< " $ids_part "
fi
# Merge: add new IDs not already present
local all_ids =( " ${ existing_ids [@] } " )
for new_id in " ${ IOMMU_VFIO_IDS [@] } " ; do
local found = false
for existing in " ${ existing_ids [@] } " ; do
[[ " $existing " == " $new_id " ]] && found = true && break
done
$found || all_ids +=( " $new_id " )
done
local ids_str
ids_str = $( IFS = ',' ; echo " ${ all_ids [*] } " )
local existing_full_line
existing_full_line = $( grep "^options vfio-pci ids=" " $vfio_conf " 2>/dev/null | head -1)
local new_full_line = "options vfio-pci ids= ${ ids_str } disable_vga=1"
if [[ " $existing_full_line " != " $new_full_line " ]] ; then
sed -i '/^options vfio-pci ids=/d' " $vfio_conf "
echo " $new_full_line " >> " $vfio_conf "
HOST_CONFIG_CHANGED = true
fi
msg_ok " $( translate 'vfio-pci IDs configured' ) ( ${ ids_str } )" | tee -a " $screen_capture "
}
# ── IOMMU interrupt remapping ─────────────────────────────
configure_iommu_options() {
_add_line_if_missing "options vfio_iommu_type1 allow_unsafe_interrupts=1" \
/etc/modprobe.d/iommu_unsafe_interrupts.conf
_add_line_if_missing "options kvm ignore_msrs=1" \
/etc/modprobe.d/kvm.conf
msg_ok " $( translate 'IOMMU interrupt remapping configured' ) " | tee -a " $screen_capture "
}
# ── AMD softdep ──────────────────────────────────────────
add_softdep_amd() {
msg_info " $( translate 'Configuring AMD softdep...' ) "
local vfio_conf = "/etc/modprobe.d/vfio.conf"
_add_line_if_missing "softdep radeon pre: vfio-pci" " $vfio_conf "
_add_line_if_missing "softdep amdgpu pre: vfio-pci" " $vfio_conf "
_add_line_if_missing "softdep snd_hda_intel pre: vfio-pci" " $vfio_conf "
msg_ok " $( translate 'AMD softdep configured in /etc/modprobe.d/vfio.conf' ) " | tee -a " $screen_capture "
}
# ── Blacklist GPU drivers (idempotent) ───────────────────
blacklist_gpu_drivers() {
msg_info " $( translate 'Blacklisting GPU host drivers...' ) "
local blacklist_file = "/etc/modprobe.d/blacklist.conf"
touch " $blacklist_file "
case " $SELECTED_GPU " in
nvidia)
2026-05-28 20:49:59 +02:00
# Only blacklist the open-source `nouveau` driver — never the
# proprietary `nvidia` module. Blacklisting nvidia globally
# would kill any OTHER NVIDIA GPU that should stay on the host
# (multi-GPU NVIDIA scenarios). The VFIO binding for the GPUs
# passed through is handled by `proxmenux-vfio-bind` via per-BDF
# driver_override + softdep nvidia pre: vfio-pci.
2026-04-04 00:54:57 +02:00
_add_line_if_missing "blacklist nouveau" " $blacklist_file "
_add_line_if_missing "blacklist lbm-nouveau" " $blacklist_file "
_add_line_if_missing "options nouveau modeset=0" " $blacklist_file "
;;
amd)
_add_line_if_missing "blacklist radeon" " $blacklist_file "
_add_line_if_missing "blacklist amdgpu" " $blacklist_file "
;;
intel)
_add_line_if_missing "blacklist i915" " $blacklist_file "
;;
esac
msg_ok " $( translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf' ) " | tee -a " $screen_capture "
}
2026-04-06 18:40:57 +02:00
sanitize_nvidia_host_stack_for_vfio() {
2026-05-28 20:49:59 +02:00
# In the new per-BDF model we only stop systemd services that could
# actively probe / lock GPUs at boot (persistenced) — but we DO NOT:
# - blacklist the nvidia kernel module
# - remove nvidia entries from /etc/modules
# - rename /etc/modules-load.d/nvidia-vfio.conf
# - rename /etc/udev/rules.d/70-nvidia.rules
# - create /etc/modprobe.d/nvidia-blacklist.conf with install /bin/false
# All of those were global and broke multi-GPU NVIDIA scenarios where
# one GPU goes to a VM (vfio-pci) and another stays on the host
# (nvidia driver). VFIO binding is now per-BDF via driver_override in
# an initramfs hook — the nvidia module stays usable for any GPU not
# explicitly targeted.
2026-04-06 18:40:57 +02:00
msg_info " $( translate 'Sanitizing NVIDIA host services for VFIO mode...' ) "
local changed = false
local state_dir = "/var/lib/proxmenux"
local state_file = " ${ state_dir } /nvidia-host-services.state"
local svc
local -a services =(
"nvidia-persistenced.service"
"nvidia-powerd.service"
"nvidia-fabricmanager.service"
)
mkdir -p " $state_dir " >/dev/null 2>& 1 || true
: > " $state_file "
for svc in " ${ services [@] } " ; do
local was_enabled = 0 was_active = 0
if systemctl is-enabled --quiet " $svc " 2>/dev/null; then
was_enabled = 1
fi
if systemctl is-active --quiet " $svc " 2>/dev/null; then
was_active = 1
fi
if (( was_enabled == 1 || was_active == 1 )) ; then
echo " ${ svc } enabled= ${ was_enabled } active= ${ was_active } " >>" $state_file "
fi
if systemctl is-active --quiet " $svc " 2>/dev/null; then
systemctl stop " $svc " >>" $LOG_FILE " 2>& 1 || true
changed = true
fi
if systemctl is-enabled --quiet " $svc " 2>/dev/null; then
systemctl disable " $svc " >>" $LOG_FILE " 2>& 1 || true
changed = true
fi
done
[[ -s " $state_file " ]] || rm -f " $state_file "
if $changed ; then
HOST_CONFIG_CHANGED = true
2026-05-28 20:49:59 +02:00
msg_ok " $( translate 'NVIDIA host services disabled for VFIO mode' ) " | tee -a " $screen_capture "
2026-04-06 18:40:57 +02:00
else
2026-05-28 20:49:59 +02:00
msg_ok " $( translate 'NVIDIA host services already aligned for VFIO mode' ) " | tee -a " $screen_capture "
2026-04-06 18:40:57 +02:00
fi
}
2026-05-28 20:49:59 +02:00
# Per-BDF VFIO binder + legacy NVIDIA blacklist migration are defined in
# scripts/global/pci_passthrough_helpers.sh (sourced at the top of this file).
# Functions exposed there:
# _proxmenux_vfio_bind_add_bdfs <bdf...>
# _proxmenux_vfio_bind_remove_bdfs <bdf...>
# _proxmenux_nvidia_migrate_legacy_blacklist
2026-04-04 00:54:57 +02:00
# ── AMD ROM dump: sysfs first, VFCT ACPI table as fallback ───────────────
_dump_rom_via_vfct() {
local rom_dest = " $1 "
local vfct_file = "/sys/firmware/acpi/tables/VFCT"
[[ -f " $vfct_file " ]] || return 1
# VFCT table layout:
# Offset 0-35 : standard ACPI header (36 bytes)
# Offset 36-47 : VFCT-specific fields (12 bytes)
# Offset 48 : first GPU_BIOS_IMAGE object
# +0 VendorID (2 bytes)
# +2 DeviceID (2 bytes)
# +4 SubsystemVendorID (2 bytes)
# +6 SubsystemDeviceID (2 bytes)
# +8 PCIBus/Device/Function/Reserved (4 bytes)
# +12 ImageLength (4 bytes, little-endian)
# +16 VBIOS image data
local img_length
img_length = $( od -An -tu4 -j60 -N4 " $vfct_file " 2>/dev/null | tr -d '[:space:]' )
if [[ -z " $img_length " || " $img_length " -le 0 ]] ; then
return 1
fi
dd if = " $vfct_file " bs = 1 skip = 64 count = " $img_length " of = " $rom_dest " 2>/dev/null
[[ -s " $rom_dest " ]]
}
dump_amd_rom() {
local pci_full = " $SELECTED_GPU_PCI "
local rom_path = "/sys/bus/pci/devices/ ${ pci_full } /rom"
local kvm_dir = "/usr/share/kvm"
mkdir -p " $kvm_dir "
local vid did
vid = $( cat "/sys/bus/pci/devices/ ${ pci_full } /vendor" 2>/dev/null | sed 's/0x//' )
did = $( cat "/sys/bus/pci/devices/ ${ pci_full } /device" 2>/dev/null | sed 's/0x//' )
local rom_filename = "vbios_ ${ vid } _ ${ did } .bin"
local rom_dest = " ${ kvm_dir } / ${ rom_filename } "
# ── Method 1: sysfs /rom ──────────────────────────────
if [[ -f " $rom_path " ]] ; then
msg_info " $( translate 'Dumping AMD GPU ROM BIOS via sysfs...' ) "
echo 1 > " $rom_path " 2>/dev/null
if cat " $rom_path " > " $rom_dest " 2>>" $LOG_FILE " && [[ -s " $rom_dest " ]] ; then
echo 0 > " $rom_path " 2>/dev/null
AMD_ROM_FILE = " $rom_filename "
msg_ok " $( translate 'GPU ROM dumped to' ) ${ rom_dest } " | tee -a " $screen_capture "
return 0
fi
echo 0 > " $rom_path " 2>/dev/null
rm -f " $rom_dest "
msg_warn " $( translate 'sysfs ROM dump failed — trying ACPI VFCT table...' ) "
else
msg_info " $( translate 'No sysfs ROM entry — trying ACPI VFCT table...' ) "
fi
# ── Method 2: ACPI VFCT table ────────────────────────
if _dump_rom_via_vfct " $rom_dest " ; then
AMD_ROM_FILE = " $rom_filename "
msg_ok " $( translate 'GPU ROM extracted from ACPI VFCT table to' ) ${ rom_dest } " | tee -a " $screen_capture "
return 0
fi
rm -f " $rom_dest "
msg_warn " $( translate 'ROM dump not available — configuring without romfile.' ) "
msg_warn " $( translate 'Passthrough may still work without a ROM file.' ) "
}
2026-04-06 13:39:07 +02:00
_remove_selected_gpu_from_lxc_conf() {
local conf = " $1 "
case " $SELECTED_GPU " in
nvidia)
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' " $conf "
;;
amd)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' " $conf "
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' " $conf "
2026-04-04 00:54:57 +02:00
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' " $conf "
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' " $conf "
2026-04-06 13:39:07 +02:00
;;
intel)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' " $conf "
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' " $conf "
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' " $conf "
;;
*)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' " $conf "
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' " $conf "
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' " $conf "
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' " $conf "
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' " $conf "
;;
esac
}
# ── Apply selected action for affected LXC (switch mode) ─
cleanup_lxc_configs() {
[[ " $SWITCH_FROM_LXC " != "true" ]] && return 0
[[ ${# LXC_AFFECTED_CTIDS [@] } -eq 0 ]] && return 0
2026-04-21 21:06:22 +02:00
msg_info2 " $( translate 'Applying selected LXC switch action' ) "
2026-04-06 13:39:07 +02:00
local i
for i in " ${ !LXC_AFFECTED_CTIDS[@] } " ; do
local ctid conf
ctid = " ${ LXC_AFFECTED_CTIDS [ $i ] } "
conf = "/etc/pve/lxc/ ${ ctid } .conf"
if [[ " ${ LXC_AFFECTED_RUNNING [ $i ] } " == "1" ]] ; then
msg_info " $( translate 'Stopping LXC' ) ${ ctid } ..."
2026-04-21 21:06:22 +02:00
# _pmx_stop_lxc: graceful shutdown with forceStop+timeout, then
# fallback to pct stop. Avoids the indefinite hang that raw
# `pct stop` produces when the container is locked or has
# unresponsive processes (Plex, databases, etc.).
if _pmx_stop_lxc " $ctid " " $LOG_FILE " ; then
2026-04-06 13:39:07 +02:00
msg_ok " $( translate 'LXC stopped' ) ${ ctid } " | tee -a " $screen_capture "
else
msg_warn " $( translate 'Could not stop LXC' ) ${ ctid } " | tee -a " $screen_capture "
fi
else
msg_ok " $( translate 'LXC already stopped' ) ${ ctid } " | tee -a " $screen_capture "
fi
if [[ " $LXC_SWITCH_ACTION " == "keep_gpu_disable_onboot" ]] ; then
if [[ " ${ LXC_AFFECTED_ONBOOT [ $i ] } " == "1" ]] ; then
if pct set " $ctid " -onboot 0 >>" $LOG_FILE " 2>& 1; then
msg_warn " $( translate 'Start on boot disabled for LXC' ) ${ ctid } " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to disable Start on boot for LXC' ) ${ ctid } " | tee -a " $screen_capture "
fi
fi
fi
if [[ " $LXC_SWITCH_ACTION " == "remove_gpu_keep_onboot" && -f " $conf " ]] ; then
_remove_selected_gpu_from_lxc_conf " $conf "
msg_ok " $( translate 'GPU access removed from LXC' ) ${ ctid } " | tee -a " $screen_capture "
2026-04-04 00:54:57 +02:00
fi
done
2026-04-06 13:39:07 +02:00
if [[ " $LXC_SWITCH_ACTION " == "remove_gpu_keep_onboot" ]] ; then
msg_warn " $( translate 'If needed again, re-add GPU to LXC from GPUs and Coral-TPU Menu → Add GPU to LXC.' ) " | tee -a " $screen_capture "
fi
2026-04-04 00:54:57 +02:00
}
# ── Remove GPU from another VM config (switch mode) ──────
cleanup_vm_config() {
[[ " $SWITCH_FROM_VM " != "true" ]] && return 0
[[ -z " $SWITCH_VM_SRC " ]] && return 0
local pci_slot = " ${ SELECTED_GPU_PCI #0000: } "
pci_slot = " ${ pci_slot %.* } " # 01:00
2026-04-12 20:32:34 +02:00
if [[ " $SWITCH_VM_ACTION " == "keep_gpu_disable_onboot" ]] ; then
2026-04-06 21:50:46 +02:00
msg_info " $( translate 'Keeping GPU in source VM config' ) ${ SWITCH_VM_SRC } ..."
if _vm_onboot_enabled " $SWITCH_VM_SRC " ; then
if qm set " $SWITCH_VM_SRC " -onboot 0 >>" $LOG_FILE " 2>& 1; then
msg_warn " $( translate 'Start on boot disabled for VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to disable Start on boot for VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
fi
else
msg_ok " $( translate 'Start on boot already disabled for VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
fi
msg_ok " $( translate 'GPU kept in source VM config' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
return 0
fi
2026-04-04 00:54:57 +02:00
local src_conf = "/etc/pve/qemu-server/ ${ SWITCH_VM_SRC } .conf"
if [[ -f " $src_conf " ]] ; then
msg_info " $( translate 'Removing GPU from VM' ) ${ SWITCH_VM_SRC } ..."
2026-04-21 21:06:22 +02:00
# Precise regex: slot must be followed by ".<function>" and a
# delimiter. Kept in sync with switch_gpu_mode.sh. A looser
# ".*${pci_slot}" would match the slot as a substring and wipe
# unrelated hostpci entries (e.g. slot "00:02" matching inside
# a dGPU BDF 0000:02:00.0).
sed -E -i "/^hostpci[0-9]+:[[:space:]]*(0000:)? ${ pci_slot } \.[0-7]([,[:space:]]| $)/d" " $src_conf "
2026-04-04 00:54:57 +02:00
msg_ok " $( translate 'GPU removed from VM' ) ${ SWITCH_VM_SRC } " | tee -a " $screen_capture "
2026-04-21 21:06:22 +02:00
# Cascade cleanup: detect audio companions orphaned in the
# source VM after the GPU slot is removed. Typical case: the
# source VM had an Intel iGPU at 00:02.0 paired with chipset
# audio at 00:1f.3 via the Part 1 checklist — the sed above
# only strips 00:02.* entries, leaving the chipset audio
# hostpci pointing at a device the source VM no longer uses.
#
# Unlike switch_gpu_mode (detach flow), we deliberately do NOT
# touch /etc/modprobe.d/vfio.conf here. The GPU is being moved
# to the current target VM, which may select the same audio
# companion in its own Part 1 checklist. Any vendor:device
# orphaned in vfio.conf after this move is inert — the user
# can clean it up later via switch_gpu_mode if they want.
if declare -F _vm_list_orphan_audio_hostpci >/dev/null 2>& 1; then
local _orphan_audio
_orphan_audio = $( _vm_list_orphan_audio_hostpci " $SWITCH_VM_SRC " " $pci_slot " )
if [[ -n " $_orphan_audio " ]] ; then
local -a _orph_items =()
local _oline _o_idx _o_bdf _o_name
while IFS = read -r _oline; do
[[ -z " $_oline " ]] && continue
_o_idx = " ${ _oline %%|* } "
_oline = " ${ _oline #*| } "
_o_bdf = " ${ _oline %%|* } "
_o_name = " ${ _oline #*| } "
_orph_items +=( " $_o_idx " " ${ _o_bdf } ${ _o_name } " "on" )
done <<< " $_orphan_audio "
local _prompt
_prompt = "\n $( translate 'The GPU has been moved out of VM' ) \Zb ${ SWITCH_VM_SRC } \Zn.\n\n"
_prompt += " $( translate 'The source VM also has these audio devices, likely added together with the GPU. Remove them too?' ) \n\n"
_prompt += " $( translate '(Checked entries will be removed. Uncheck to keep in VM.)' ) "
local _selected
_selected = $( _pmx_checklist \
" $( translate 'Associated Audio Devices' ) " \
" $_prompt " \
20 84 " $(( ${# _orph_items [@] } / 3 )) " \
" ${ _orph_items [@] } " ) || _selected = ""
_selected = $( echo " $_selected " | tr -d '"' )
local _sel _removed = ""
for _sel in $_selected ; do
if declare -F _vm_remove_hostpci_index >/dev/null 2>& 1; then
_vm_remove_hostpci_index " $SWITCH_VM_SRC " " $_sel " " $LOG_FILE " \
&& _removed += " hostpci ${ _sel } "
else
qm set " $SWITCH_VM_SRC " --delete "hostpci ${ _sel } " >>" $LOG_FILE " 2>& 1 \
&& _removed += " hostpci ${ _sel } "
fi
done
if [[ -n " $_removed " ]] ; then
show_proxmenux_logo
msg_title " ${ run_title } "
msg_ok " $( translate 'Associated audio removed from VM' ) : ${ SWITCH_VM_SRC } — ${ _removed } " \
| tee -a " $screen_capture "
fi
fi
fi
2026-04-04 00:54:57 +02:00
fi
}
# ── VM display normalization for passthrough stability ────
ensure_vm_display_std() {
msg_info " $( translate 'Checking VM virtual display model...' ) "
local current_vga current_base
current_vga = $( qm config " $SELECTED_VMID " 2>/dev/null | awk '/^vga:/ {print $2}' )
current_base = " ${ current_vga %%,* } "
if [[ -z " $current_base " ]] ; then
if qm set " $SELECTED_VMID " --vga std >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'Virtual display set to' ) : vga: std" | tee -a " $screen_capture "
else
msg_warn " $( translate 'Could not set VM virtual display to vga: std' ) " | tee -a " $screen_capture "
fi
return 0
fi
if [[ " $current_base " == "std" ]] ; then
msg_ok " $( translate 'Virtual display already set to' ) : vga: std" | tee -a " $screen_capture "
return 0
fi
if qm set " $SELECTED_VMID " --vga std >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'Virtual display changed from' ) ${ current_base } $( translate 'to' ) std" | tee -a " $screen_capture "
else
msg_warn " $( translate 'Could not change VM virtual display to vga: std' ) " | tee -a " $screen_capture "
fi
}
# ── Configure VM: add hostpci entries ─────────────────────
configure_vm() {
msg_info " $( translate 'Configuring VM' ) ${ SELECTED_VMID } ..."
# Find next free hostpciN index
local idx = 0
2026-04-05 11:24:08 +02:00
if declare -F _pci_next_hostpci_index >/dev/null 2>& 1; then
idx = $( _pci_next_hostpci_index " $SELECTED_VMID " 2>/dev/null || echo 0)
else
while qm config " $SELECTED_VMID " 2>/dev/null | grep -q "^hostpci ${ idx } :" ; do
idx = $(( idx + 1 ))
done
fi
2026-04-04 00:54:57 +02:00
# Primary GPU: pcie=1, x-vga=1 only for NVIDIA/AMD (not Intel iGPU), romfile if AMD
local gpu_opts = "pcie=1"
[[ " $SELECTED_GPU " == "nvidia" || " $SELECTED_GPU " == "amd" ]] && gpu_opts += ",x-vga=1"
[[ -n " $AMD_ROM_FILE " ]] && gpu_opts += ",romfile= ${ AMD_ROM_FILE } "
if _is_pci_function_assigned_to_vm " $SELECTED_GPU_PCI " " $SELECTED_VMID " ; then
msg_ok " $( translate 'GPU already present in target VM — existing hostpci entry reused' ) " | tee -a " $screen_capture "
else
2026-04-06 22:51:30 +02:00
if qm set " $SELECTED_VMID " --hostpci${ idx } " ${ SELECTED_GPU_PCI } , ${ gpu_opts } " >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'GPU added' ) : hostpci ${ idx } : ${ SELECTED_GPU_PCI } , ${ gpu_opts } " | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to add GPU to target VM' ) : ${ SELECTED_GPU_PCI } " | tee -a " $screen_capture "
return 1
fi
2026-04-04 00:54:57 +02:00
idx = $(( idx + 1 ))
fi
# Remaining IOMMU group devices (audio, USB controllers, etc.)
for dev in " ${ IOMMU_DEVICES [@] } " ; do
[[ " $dev " == " $SELECTED_GPU_PCI " ]] && continue
if _is_pci_function_assigned_to_vm " $dev " " $SELECTED_VMID " ; then
msg_ok " $( translate 'Device already present in target VM — existing hostpci entry reused' ) : ${ dev } " | tee -a " $screen_capture "
continue
fi
2026-04-06 22:51:30 +02:00
if qm set " $SELECTED_VMID " --hostpci${ idx } " ${ dev } ,pcie=1" >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'Device added' ) : hostpci ${ idx } : ${ dev } ,pcie=1" | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to add IOMMU group device' ) : ${ dev } " | tee -a " $screen_capture "
return 1
fi
2026-04-04 00:54:57 +02:00
idx = $(( idx + 1 ))
done
2026-04-05 11:24:08 +02:00
# Optional sibling GPU audio function (typically *.1) when split from IOMMU group
for dev in " ${ EXTRA_AUDIO_DEVICES [@] } " ; do
if _is_pci_function_assigned_to_vm " $dev " " $SELECTED_VMID " ; then
msg_ok " $( translate 'GPU audio already present in target VM — existing hostpci entry reused' ) : ${ dev } " | tee -a " $screen_capture "
continue
fi
2026-04-06 22:51:30 +02:00
if qm set " $SELECTED_VMID " --hostpci${ idx } " ${ dev } ,pcie=1" >>" $LOG_FILE " 2>& 1; then
msg_ok " $( translate 'GPU audio added' ) : hostpci ${ idx } : ${ dev } ,pcie=1" | tee -a " $screen_capture "
else
msg_error " $( translate 'Failed to add GPU audio function' ) : ${ dev } " | tee -a " $screen_capture "
return 1
fi
2026-04-05 11:24:08 +02:00
idx = $(( idx + 1 ))
done
2026-04-04 00:54:57 +02:00
# NVIDIA: hide KVM hypervisor from guest
[[ " $SELECTED_GPU " == "nvidia" ]] && _configure_nvidia_kvm_hide
2026-04-06 22:51:30 +02:00
return 0
2026-04-04 00:54:57 +02:00
}
_configure_nvidia_kvm_hide() {
msg_info " $( translate 'Configuring NVIDIA KVM hiding...' ) "
# CPU: host,hidden=1
local current_cpu
current_cpu = $( qm config " $SELECTED_VMID " 2>/dev/null | grep "^cpu:" | awk '{print $2}' )
if ! echo " $current_cpu " | grep -q "hidden=1" ; then
qm set " $SELECTED_VMID " --cpu "host,hidden=1,flags=+pcid" >>" $LOG_FILE " 2>& 1
msg_ok " $( translate 'CPU set to host,hidden=1,flags=+pcid' ) " | tee -a " $screen_capture "
else
msg_ok " $( translate 'NVIDIA CPU hiding already configured' ) " | tee -a " $screen_capture "
fi
# args: kvm=off + vendor_id spoof
local current_args
current_args = $( qm config " $SELECTED_VMID " 2>/dev/null | grep "^args:" | sed 's/^args: //' )
if ! echo " $current_args " | grep -q "kvm=off" ; then
local kvm_args = "-cpu 'host,+kvm_pv_unhalt,+kvm_pv_eoi,hv_vendor_id=NV43FIX,kvm=off'"
local new_args
if [[ -n " $current_args " ]] ; then
new_args = " ${ current_args } ${ kvm_args } "
else
new_args = " $kvm_args "
fi
qm set " $SELECTED_VMID " --args " $new_args " >>" $LOG_FILE " 2>& 1
msg_ok " $( translate 'NVIDIA KVM args configured (kvm=off, vendor_id spoof)' ) " | tee -a " $screen_capture "
else
msg_ok " $( translate 'NVIDIA KVM hiding already configured' ) " | tee -a " $screen_capture "
fi
}
# ── Update initramfs ─────────────────────────────────────
update_initramfs_host() {
msg_info " $( translate 'Updating initramfs (this may take a minute)...' ) "
update-initramfs -u -k all >>" $LOG_FILE " 2>& 1
msg_ok " $( translate 'initramfs updated' ) " | tee -a " $screen_capture "
}
# ==========================================================
# Main
# ==========================================================
main() {
2026-04-05 11:24:08 +02:00
parse_cli_args " $@ "
2026-04-04 00:54:57 +02:00
: >" $LOG_FILE "
: >" $screen_capture "
2026-04-05 11:24:08 +02:00
[[ " $WIZARD_CALL " == "true" ]] && _set_wizard_result "cancelled"
2026-04-04 00:54:57 +02:00
# ── Phase 1: all dialogs (no terminal output) ─────────
detect_host_gpus
check_iommu_enabled
select_gpu
2026-04-19 12:26:52 +02:00
check_sriov_and_block_if_needed
2026-04-04 00:54:57 +02:00
warn_single_gpu
select_vm
ensure_selected_gpu_not_already_in_target_vm
check_gpu_vm_compatibility
analyze_iommu_group
2026-04-05 11:24:08 +02:00
detect_optional_gpu_audio
2026-04-04 00:54:57 +02:00
check_vm_machine_type
check_switch_mode
2026-04-05 11:24:08 +02:00
evaluate_host_reboot_requirement
2026-04-04 00:54:57 +02:00
confirm_summary
# ── Phase 2: processing ───────────────────────────────
local run_title
run_title = $( _get_vm_run_title)
2026-04-05 11:24:08 +02:00
if [[ " $WIZARD_CALL " == "true" ]] ; then
echo
else
show_proxmenux_logo
msg_title " ${ run_title } "
fi
2026-04-04 00:54:57 +02:00
2026-05-28 20:49:59 +02:00
# Auto-migrate any leftover state from the previous (broken) global
# NVIDIA blacklist model BEFORE applying new config. Idempotent: no-op
# on clean hosts. Always runs in the NVIDIA flow so a host that was
# configured with an old ProxMenux release self-heals on the next run.
[[ " $SELECTED_GPU " == "nvidia" ]] && _proxmenux_nvidia_migrate_legacy_blacklist
2026-04-05 11:24:08 +02:00
if [[ " $VM_SWITCH_ALREADY_VFIO " == "true" ]] ; then
msg_ok " $( translate 'Host already in VFIO mode — skipping host reconfiguration for VM reassignment' ) " | tee -a " $screen_capture "
else
add_vfio_modules
configure_vfio_pci_ids
configure_iommu_options
[[ " $SELECTED_GPU " == "amd" ]] && add_softdep_amd
blacklist_gpu_drivers
[[ " $SELECTED_GPU " == "amd" ]] && dump_amd_rom
fi
2026-04-06 18:40:57 +02:00
[[ " $SELECTED_GPU " == "nvidia" ]] && sanitize_nvidia_host_stack_for_vfio
2026-04-04 00:54:57 +02:00
cleanup_lxc_configs
cleanup_vm_config
ensure_vm_display_std
2026-04-06 22:51:30 +02:00
if ! configure_vm; then
msg_error " $( translate 'VM passthrough configuration failed. Review the log and VM config.' ) "
[[ " $WIZARD_CALL " == "true" ]] && _set_wizard_result "failed"
rm -f " $screen_capture "
exit 1
fi
2026-04-04 00:54:57 +02:00
[[ " $HOST_CONFIG_CHANGED " == "true" ]] && update_initramfs_host
# ── Phase 3: summary ─────────────────────────────────
2026-04-05 11:24:08 +02:00
if [[ " $WIZARD_CALL " == "true" ]] ; then
2026-04-04 00:54:57 +02:00
echo
else
2026-04-05 11:24:08 +02:00
show_proxmenux_logo
msg_title " ${ run_title } "
cat " $screen_capture "
echo
fi
if [[ " $WIZARD_CALL " == "true" ]] ; then
2026-04-06 18:40:57 +02:00
if [[ " $HOST_CONFIG_CHANGED " == "true" ]] ; then
_set_wizard_result "applied_reboot_required"
else
_set_wizard_result "applied"
fi
2026-04-05 11:24:08 +02:00
rm -f " $screen_capture "
return 0
fi
echo -e " ${ TAB }${ BL } 📄 Log: ${ LOG_FILE }${ CL } "
if [[ " $HOST_CONFIG_CHANGED " == "true" ]] ; then
echo -e " ${ TAB }${ DGN } - $( translate 'Host VFIO configuration changed — reboot required before starting the VM.' ) ${ CL } "
else
echo -e " ${ TAB }${ DGN } - $( translate 'Host VFIO config was already up to date — no reboot needed.' ) ${ CL } "
2026-04-04 00:54:57 +02:00
fi
case " $SELECTED_GPU " in
nvidia)
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB }${ DGN } - $( translate 'Install NVIDIA drivers from nvidia.com inside the guest.' ) ${ CL } "
echo -e " ${ TAB }${ DGN } - $( translate 'If Code 43 error appears, KVM hiding is already configured.' ) ${ CL } "
2026-04-04 00:54:57 +02:00
;;
amd)
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB }${ DGN } - $( translate 'Install AMD GPU drivers inside the guest.' ) ${ CL } "
echo -e " ${ TAB }${ DGN } - $( translate 'If passthrough fails on Windows: install RadeonResetBugFix.' ) ${ CL } "
2026-04-04 00:54:57 +02:00
[[ -n " $AMD_ROM_FILE " ]] && \
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB }${ DGN } - $( translate 'ROM file used' ) : /usr/share/kvm/ ${ AMD_ROM_FILE }${ CL } "
2026-04-04 00:54:57 +02:00
;;
intel)
2026-04-05 11:24:08 +02:00
echo -e " ${ TAB }${ DGN } - $( translate 'Install Intel Graphics Driver inside the guest.' ) ${ CL } "
echo -e " ${ TAB }${ DGN } - $( translate 'Enable Remote Desktop (RDP) before disabling the virtual display.' ) ${ CL } "
2026-04-04 00:54:57 +02:00
;;
esac
2026-04-05 11:24:08 +02:00
echo
msg_info2 " $( translate 'If you want to use a physical monitor on the passthrough GPU:' ) "
echo " • $( translate 'First install the GPU drivers inside the guest and verify remote access (RDP/SSH).' ) "
echo " • $( translate 'Then change the VM display to none (vga: none) when the guest is stable.' ) "
2026-04-04 00:54:57 +02:00
echo
msg_success " $( translate 'GPU passthrough configured for VM' ) ${ SELECTED_VMID } ( ${ VM_NAME } )."
echo
rm -f " $screen_capture "
2026-04-21 21:06:22 +02:00
# Final reboot prompt. Whiptail is invoked directly (not through
# the _pmx_yesno helper) because the ProxMenux menu chain
# (menu → main_menu → hw_grafics_menu → add_gpu_vm) has been
# verified to work reliably with a bare whiptail here, while the
# dialog-based helper path hits process-group / TTY edge cases in
# that exact chain.
#
# The extra `Press Enter to continue ... read -r` between whiptail
# and `reboot` is deliberate — it gives the user a visible pause
# after the dialog closes so an accidental Enter on the yes button
# cannot trigger an immediate reboot.
2026-04-04 00:54:57 +02:00
if [[ " $HOST_CONFIG_CHANGED " == "true" ]] ; then
whiptail --title " $( translate 'Reboot Required' ) " \
--yesno " $( translate 'A reboot is required for VFIO binding to take effect. Do you want to restart now?' ) " 10 68
if [[ $? -eq 0 ]] ; then
2026-04-21 21:06:22 +02:00
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
2026-04-04 00:54:57 +02:00
msg_warn " $( translate 'Rebooting the system...' ) "
reboot
else
msg_info2 " $( translate 'You can reboot later manually.' ) "
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
fi
else
msg_success " $( translate 'Press Enter to continue...' ) "
read -r
fi
}
2026-04-05 11:24:08 +02:00
main " $@ "