mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-20 19:12:14 +00:00
2002 lines
81 KiB
Bash
2002 lines
81 KiB
Bash
#!/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
|
|
# ==========================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
|
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
|
|
BASE_DIR="/usr/local/share/proxmenux"
|
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
|
|
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
|
|
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
|
|
elif [[ ! -f "$UTILS_FILE" ]]; then
|
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
|
fi
|
|
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
|
|
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
|
|
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh"
|
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh"
|
|
fi
|
|
|
|
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=
|
|
declare -a EXTRA_AUDIO_DEVICES=() # sibling audio function(s), typically *.1
|
|
IOMMU_GROUP=""
|
|
IOMMU_PENDING_REBOOT=false
|
|
|
|
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=""
|
|
SWITCH_VM_ACTION="" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
|
|
TARGET_VM_ALREADY_HAS_GPU=false
|
|
VM_SWITCH_ALREADY_VFIO=false
|
|
PREFLIGHT_HOST_REBOOT_REQUIRED=true
|
|
|
|
AMD_ROM_FILE=""
|
|
|
|
HOST_CONFIG_CHANGED=false # set to true whenever host VFIO config is actually written
|
|
|
|
PRESELECT_VMID=""
|
|
WIZARD_CALL=false
|
|
GPU_WIZARD_RESULT_FILE=""
|
|
|
|
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
|
|
|
|
|
|
# ==========================================================
|
|
# 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
|
|
}
|
|
|
|
_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"
|
|
}
|
|
|
|
_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
|
|
}
|
|
|
|
_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
|
|
}
|
|
|
|
_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
|
|
}
|
|
|
|
_file_has_exact_line() {
|
|
local line="$1"
|
|
local file="$2"
|
|
[[ -f "$file" ]] || return 1
|
|
grep -qFx "$line" "$file"
|
|
}
|
|
|
|
evaluate_host_reboot_requirement() {
|
|
if [[ "$IOMMU_PENDING_REBOOT" == "true" ]]; then
|
|
PREFLIGHT_HOST_REBOOT_REQUIRED=true
|
|
return 0
|
|
fi
|
|
|
|
# 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
|
|
_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
|
|
_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
|
|
[[ -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
|
|
;;
|
|
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
|
|
}
|
|
|
|
_get_vm_run_title() {
|
|
if [[ "$SWITCH_FROM_LXC" == "true" && "$SWITCH_FROM_VM" == "true" ]]; then
|
|
echo "$(translate 'GPU Passthrough to VM (reassign from LXC and another VM)')"
|
|
elif [[ "$SWITCH_FROM_LXC" == "true" ]]; then
|
|
echo "$(translate 'GPU Passthrough to VM (from LXC)')"
|
|
elif [[ "$SWITCH_FROM_VM" == "true" ]]; then
|
|
echo "$(translate 'GPU Passthrough to VM (reassign from another VM)')"
|
|
else
|
|
echo "$(translate 'GPU Passthrough to VM')"
|
|
fi
|
|
}
|
|
|
|
_is_pci_slot_assigned_to_vm() {
|
|
if declare -F _pci_slot_assigned_to_vm >/dev/null 2>&1; then
|
|
_pci_slot_assigned_to_vm "$1" "$2"
|
|
return $?
|
|
fi
|
|
|
|
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() {
|
|
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
|
|
_pci_function_assigned_to_vm "$1" "$2"
|
|
return $?
|
|
fi
|
|
|
|
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)
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "${popup_title}" \
|
|
--msgbox "\n$(translate 'The selected GPU is already assigned to this VM, but the host is not currently using vfio-pci for this device.')\n\n$(translate 'Current driver'): ${current_driver}\n\n$(translate 'The script will continue to restore VM passthrough mode on the host and reuse existing hostpci entries.')" \
|
|
13 78
|
|
return 0
|
|
fi
|
|
|
|
# Single GPU system: nothing else to choose
|
|
if [[ $GPU_COUNT -le 1 ]]; then
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'GPU Already Added')" \
|
|
--msgbox "\n$(translate 'The selected GPU is already assigned to this VM.')\n\n$(translate 'No changes are required.')" \
|
|
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
|
|
label="${ALL_GPU_NAMES[$i]} [${ALL_GPU_DRIVERS[$i]}] — ${pci}"
|
|
menu_items+=("$i" "$label")
|
|
available=$((available + 1))
|
|
done
|
|
|
|
if [[ $available -eq 0 ]]; then
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'All GPUs Already Assigned')" \
|
|
--msgbox "\n$(translate 'All detected GPUs are already assigned to this VM.')\n\n$(translate 'No additional GPU can be added.')" \
|
|
10 70
|
|
exit 0
|
|
fi
|
|
|
|
local choice
|
|
local -a clear_opt=()
|
|
[[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear)
|
|
choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'GPU Already Assigned to This VM')" \
|
|
--menu "\n$(translate 'The selected GPU is already present in this VM. Select another GPU to continue:')" \
|
|
18 82 10 \
|
|
"${menu_items[@]}" \
|
|
2>&1 >/dev/tty) || exit 0
|
|
|
|
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
|
|
local pci_short pci_full name type driver pci_info
|
|
pci_short=$(echo "$line" | awk '{print $1}')
|
|
pci_full="0000:${pci_short}"
|
|
|
|
# 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')"
|
|
|
|
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
|
|
_set_wizard_result "no_gpu"
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'No GPU Detected')" \
|
|
--msgbox "\n$(translate 'No compatible GPU was detected on this host.')" 8 60
|
|
exit 0
|
|
fi
|
|
|
|
[[ $GPU_COUNT -eq 1 ]] && SINGLE_GPU_SYSTEM=true
|
|
}
|
|
|
|
|
|
# ==========================================================
|
|
# Phase 1 — Step 2: Check IOMMU, offer to enable it
|
|
# ==========================================================
|
|
check_iommu_enabled() {
|
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
|
return 0
|
|
fi
|
|
|
|
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
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'IOMMU Pending Reboot')" \
|
|
--msgbox "\n$(translate 'IOMMU is already configured for next boot, but it is not active yet.')\n\n$(translate 'GPU passthrough configuration will continue now and will become effective after host reboot.')" \
|
|
11 78
|
|
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"
|
|
msg+="$(translate 'Configuration will continue now and be effective after reboot.')"
|
|
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'IOMMU Required')" \
|
|
--yesno "$msg" 15 72
|
|
|
|
local response=$?
|
|
[[ "$WIZARD_CALL" != "true" ]] && clear
|
|
|
|
if [[ $response -eq 0 ]]; then
|
|
[[ "$WIZARD_CALL" != "true" ]] && show_proxmenux_logo
|
|
msg_title "$(translate 'Enabling IOMMU')"
|
|
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
|
|
echo
|
|
msg_success "$(translate 'IOMMU configured. GPU passthrough setup will continue now and will be effective after reboot.')"
|
|
echo
|
|
if [[ "$WIZARD_CALL" != "true" ]]; then
|
|
msg_success "$(translate 'Press Enter to continue...')"
|
|
read -r
|
|
fi
|
|
return 0
|
|
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
|
|
local label="${ALL_GPU_NAMES[$i]} [${ALL_GPU_DRIVERS[$i]}] — ${ALL_GPU_PCIS[$i]}"
|
|
menu_items+=("$i" "$label")
|
|
done
|
|
|
|
local choice
|
|
local -a clear_opt=()
|
|
[[ "$WIZARD_CALL" != "true" ]] && clear_opt+=(--clear)
|
|
choice=$(dialog "${clear_opt[@]}" --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Select GPU for VM Passthrough')" \
|
|
--menu "\n$(translate 'Select the GPU to pass through to the VM:')" \
|
|
18 82 10 \
|
|
"${menu_items[@]}" \
|
|
2>&1 >/dev/tty) || exit 0
|
|
|
|
SELECTED_GPU="${ALL_GPU_TYPES[$choice]}"
|
|
SELECTED_GPU_PCI="${ALL_GPU_PCIS[$choice]}"
|
|
SELECTED_GPU_NAME="${ALL_GPU_NAMES[$choice]}"
|
|
}
|
|
|
|
|
|
# ==========================================================
|
|
# 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"
|
|
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"
|
|
msg+="$(translate 'Make sure you have SSH or Web UI access before rebooting.')\n\n"
|
|
msg+="$(translate 'Do you want to continue?')"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Single GPU Warning')" \
|
|
--yesno "$msg" 22 76
|
|
|
|
[[ $? -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"
|
|
local gpu_subtype reset_method power_state vendor device viddid
|
|
|
|
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:]')
|
|
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.')"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Blocked GPU ID')" \
|
|
--msgbox "$msg" 20 84
|
|
exit 0
|
|
fi
|
|
|
|
# ── 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"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'High-Risk GPU Power State')" \
|
|
--msgbox "$msg" 20 80
|
|
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"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Reset Capability Blocked')" \
|
|
--msgbox "$msg" 20 80
|
|
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"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Reset Capability Blocked')" \
|
|
--msgbox "$msg" 20 80
|
|
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?')"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Reset Capability Warning')" \
|
|
--yesno "$msg" 18 78
|
|
[[ $? -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"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'High-Risk GPU Power State')" \
|
|
--msgbox "$msg" 22 80
|
|
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" ]] && \
|
|
msg+=" • \Z1$(translate 'Current power state: D3cold (device currently inaccessible)')\Zn\n"
|
|
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"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Incompatible GPU for VM Passthrough')" \
|
|
--msgbox "$msg" 26 80
|
|
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"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Reset Capability Blocked')" \
|
|
--msgbox "$msg" 20 80
|
|
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?')"
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'Reset Capability Warning')" \
|
|
--yesno "$msg" 18 78
|
|
[[ $? -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
|
|
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}")
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'IOMMU Group Pending')" \
|
|
--msgbox "\n$(translate 'IOMMU groups are not available yet because reboot is pending.')\n\n$(translate 'The script will preconfigure the selected GPU now and finalize hardware binding after reboot.')\n\n$(translate 'Selected GPU function'):\n • ${pci_full}" \
|
|
14 82
|
|
return 0
|
|
fi
|
|
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'IOMMU Group Error')" \
|
|
--msgbox "\n$(translate 'Could not determine the IOMMU group for the selected GPU.')\n\n$(translate 'Make sure IOMMU is properly enabled and the system has been rebooted after activation.')" \
|
|
10 72
|
|
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)
|
|
if [[ "$dev_class" == "0x0604" || "$dev_class" == "0x0600" ]]; then
|
|
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
|
|
|
|
local msg
|
|
msg="$(translate 'IOMMU Group'): ${IOMMU_GROUP}\n\n"
|
|
msg+="$(translate 'The following devices will all be passed to the VM') "
|
|
msg+="($(translate 'IOMMU isolation rule')):\n\n"
|
|
msg+="${display_lines}"
|
|
|
|
if [[ $extra_devices -gt 0 ]]; then
|
|
msg+="\n\Z1$(translate 'All devices in the same IOMMU group must be passed together.')\Zn"
|
|
fi
|
|
|
|
dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'IOMMU Group') ${IOMMU_GROUP}" \
|
|
--msgbox "\n${msg}" 22 82
|
|
}
|
|
|
|
detect_optional_gpu_audio() {
|
|
EXTRA_AUDIO_DEVICES=()
|
|
|
|
local sibling_audio="${SELECTED_GPU_PCI%.*}.1"
|
|
local dev_path="/sys/bus/pci/devices/${sibling_audio}"
|
|
[[ -d "$dev_path" ]] || return 0
|
|
|
|
local class_hex
|
|
class_hex=$(cat "${dev_path}/class" 2>/dev/null | sed 's/^0x//')
|
|
[[ "${class_hex:0:2}" == "04" ]] || return 0
|
|
|
|
local already_in_group=false dev
|
|
for dev in "${IOMMU_DEVICES[@]}"; do
|
|
if [[ "$dev" == "$sibling_audio" ]]; then
|
|
already_in_group=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ "$already_in_group" == "true" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
EXTRA_AUDIO_DEVICES+=("$sibling_audio")
|
|
|
|
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
|
|
}
|
|
|
|
|
|
# ==========================================================
|
|
# Phase 1 — Step 6: VM selection
|
|
# ==========================================================
|
|
select_vm() {
|
|
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
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'Invalid VMID')" \
|
|
--msgbox "\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72
|
|
exit 1
|
|
fi
|
|
|
|
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
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'No VMs Found')" \
|
|
--msgbox "\n$(translate 'No Virtual Machines found on this system.')\n\n$(translate 'Create a VM first (machine type q35 + UEFI BIOS), then run this option again.')" \
|
|
10 68
|
|
exit 0
|
|
fi
|
|
|
|
SELECTED_VMID=$(dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'Select Virtual Machine')" \
|
|
--menu "\n$(translate 'Select the VM to add the GPU to:')" \
|
|
20 72 12 \
|
|
"${menu_items[@]}" \
|
|
2>&1 >/dev/tty) || exit 0
|
|
|
|
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')"
|
|
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'Incompatible Machine Type')" \
|
|
--msgbox "$msg" 20 78
|
|
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 ────────────────────────────────
|
|
LXC_AFFECTED_CTIDS=()
|
|
LXC_AFFECTED_NAMES=()
|
|
LXC_AFFECTED_RUNNING=()
|
|
LXC_AFFECTED_ONBOOT=()
|
|
LXC_SWITCH_ACTION=""
|
|
|
|
local lxc_affected=()
|
|
local running_count=0
|
|
local onboot_count=0
|
|
|
|
for conf in /etc/pve/lxc/*.conf; do
|
|
[[ -f "$conf" ]] || continue
|
|
_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))
|
|
done
|
|
|
|
if [[ ${#lxc_affected[@]} -gt 0 ]]; then
|
|
SWITCH_FROM_LXC=true
|
|
SWITCH_LXC_LIST=$(IFS=', '; echo "${lxc_affected[*]}")
|
|
|
|
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"
|
|
done
|
|
msg+="\n$(translate 'VM passthrough requires exclusive VFIO binding of the GPU.')\n"
|
|
msg+="$(translate 'Choose how to handle affected LXC containers before switching to VM mode.')\n\n"
|
|
[[ "$running_count" -gt 0 ]] && \
|
|
msg+="\Z1$(translate 'Running containers detected'): ${running_count}\Zn\n"
|
|
[[ "$onboot_count" -gt 0 ]] && \
|
|
msg+="\Z1\Zb$(translate 'Start on boot enabled (onboot=1)'): ${onboot_count}\Zn\n"
|
|
msg+="\n\Z1$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn"
|
|
|
|
action_choice=$(dialog --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'GPU Used in LXC Containers')" \
|
|
--default-item "2" \
|
|
--menu "$msg" 25 96 8 \
|
|
"1" "$(translate 'Keep GPU in LXC config (disable Start on boot)')" \
|
|
"2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')" \
|
|
2>&1 >/dev/tty) || exit 0
|
|
|
|
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
|
|
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
|
|
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.')"
|
|
|
|
dialog --backtitle "ProxMenux" \
|
|
--title "$(translate 'GPU Busy in Running VM')" \
|
|
--msgbox "$msg" 16 78
|
|
exit 0
|
|
fi
|
|
|
|
SWITCH_FROM_VM=true
|
|
SWITCH_VM_SRC="$vm_src_id"
|
|
SWITCH_VM_ACTION="remove_gpu_keep_onboot"
|
|
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"
|
|
|
|
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"
|
|
msg+="$(translate 'That VM is currently stopped, so the GPU can be reassigned now.')\n"
|
|
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"
|
|
if [[ "$src_onboot" == "1" && "$target_onboot" == "1" ]]; then
|
|
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"
|
|
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
|
|
msg+="$(translate 'Choose conflict policy for the source VM:')"
|
|
|
|
local vm_action_choice
|
|
vm_action_choice=$(dialog --clear --backtitle "ProxMenux" --colors \
|
|
--title "$(translate 'GPU Already Assigned to Another VM')" \
|
|
--default-item "1" \
|
|
--menu "$msg" 24 98 8 \
|
|
"1" "$(translate 'Keep GPU in source VM config (disable Start on boot if enabled)')" \
|
|
"2" "$(translate 'Remove GPU from source VM config (keep Start on boot)')" \
|
|
2>&1 >/dev/tty) || exit 0
|
|
|
|
case "$vm_action_choice" in
|
|
1) SWITCH_VM_ACTION="keep_gpu_disable_onboot" ;;
|
|
2) SWITCH_VM_ACTION="remove_gpu_keep_onboot" ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
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"
|
|
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
|
|
msg+=" $(translate 'Target VM') : ${VM_NAME:-VM-${SELECTED_VMID}} (${SELECTED_VMID})\n"
|
|
msg+=" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
|
msg+=" \Zb$(translate 'Host'):\Zn\n"
|
|
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
|
|
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"
|
|
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
|
|
[[ ${#EXTRA_AUDIO_DEVICES[@]} -gt 0 ]] && \
|
|
msg+=" • $(translate 'Additional GPU audio function will be added'): ${EXTRA_AUDIO_DEVICES[*]}\n"
|
|
[[ "$SELECTED_GPU" == "nvidia" ]] && \
|
|
msg+=" • $(translate 'NVIDIA KVM hiding (cpu hidden=1)')\n"
|
|
if [[ "$SWITCH_FROM_LXC" == "true" ]]; then
|
|
msg+="\n \Z1• $(translate 'Affected LXC containers'): ${SWITCH_LXC_LIST}\Zn\n"
|
|
msg+=" \Z1• $(translate 'Selected LXC action'): $(_lxc_switch_action_label)\Zn\n"
|
|
if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" ]]; then
|
|
msg+=" \Z1• $(translate 'To use the GPU again in LXC, run Add GPU to LXC from GPUs and Coral-TPU Menu')\Zn\n"
|
|
fi
|
|
fi
|
|
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
|
|
msg+="\n$(translate 'Do you want to proceed?')"
|
|
|
|
local run_title
|
|
run_title=$(_get_vm_run_title)
|
|
|
|
dialog --clear --backtitle "ProxMenux" --colors \
|
|
--title "${run_title}" \
|
|
--yesno "$msg" 28 78
|
|
[[ $? -ne 0 ]] && exit 0
|
|
}
|
|
|
|
|
|
# ==========================================================
|
|
# 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() {
|
|
msg_info "$(translate 'Configuring vfio-pci device IDs...')"
|
|
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
|
touch "$vfio_conf"
|
|
|
|
# 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)
|
|
_add_line_if_missing "blacklist nouveau" "$blacklist_file"
|
|
_add_line_if_missing "blacklist nvidia" "$blacklist_file"
|
|
_add_line_if_missing "blacklist nvidia_drm" "$blacklist_file"
|
|
_add_line_if_missing "blacklist nvidia_modeset" "$blacklist_file"
|
|
_add_line_if_missing "blacklist nvidia_uvm" "$blacklist_file"
|
|
_add_line_if_missing "blacklist nvidiafb" "$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"
|
|
}
|
|
|
|
sanitize_nvidia_host_stack_for_vfio() {
|
|
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 [[ -f /etc/modules-load.d/nvidia-vfio.conf ]]; then
|
|
mv /etc/modules-load.d/nvidia-vfio.conf /etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio >>"$LOG_FILE" 2>&1 || true
|
|
changed=true
|
|
fi
|
|
|
|
if grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null; then
|
|
sed -i '/^nvidia$/d;/^nvidia_uvm$/d;/^nvidia_drm$/d;/^nvidia_modeset$/d' /etc/modules
|
|
changed=true
|
|
fi
|
|
|
|
if $changed; then
|
|
HOST_CONFIG_CHANGED=true
|
|
msg_ok "$(translate 'NVIDIA host services/autoload disabled for VFIO mode')" | tee -a "$screen_capture"
|
|
else
|
|
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for VFIO mode')" | tee -a "$screen_capture"
|
|
fi
|
|
}
|
|
|
|
|
|
# ── 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.')"
|
|
}
|
|
|
|
|
|
_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"
|
|
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf"
|
|
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf"
|
|
;;
|
|
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
|
|
|
|
msg_info "$(translate 'Applying selected LXC switch action...')"
|
|
|
|
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}..."
|
|
if pct stop "$ctid" >>"$LOG_FILE" 2>&1; then
|
|
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"
|
|
fi
|
|
done
|
|
|
|
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
|
|
}
|
|
|
|
|
|
# ── 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
|
|
|
|
if [[ "$VM_SWITCH_ACTION" == "keep_gpu_disable_onboot" ]]; then
|
|
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
|
|
|
|
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}..."
|
|
sed -i "/^hostpci[0-9]\+:.*${pci_slot}/d" "$src_conf"
|
|
msg_ok "$(translate 'GPU removed from VM') ${SWITCH_VM_SRC}" | tee -a "$screen_capture"
|
|
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
|
|
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
|
|
|
|
# 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
|
|
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
|
|
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
|
|
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
|
|
idx=$((idx + 1))
|
|
done
|
|
|
|
# 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
|
|
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
|
|
idx=$((idx + 1))
|
|
done
|
|
|
|
# NVIDIA: hide KVM hypervisor from guest
|
|
[[ "$SELECTED_GPU" == "nvidia" ]] && _configure_nvidia_kvm_hide
|
|
return 0
|
|
}
|
|
|
|
_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() {
|
|
parse_cli_args "$@"
|
|
|
|
: >"$LOG_FILE"
|
|
: >"$screen_capture"
|
|
[[ "$WIZARD_CALL" == "true" ]] && _set_wizard_result "cancelled"
|
|
|
|
# ── Phase 1: all dialogs (no terminal output) ─────────
|
|
detect_host_gpus
|
|
check_iommu_enabled
|
|
select_gpu
|
|
warn_single_gpu
|
|
select_vm
|
|
ensure_selected_gpu_not_already_in_target_vm
|
|
check_gpu_vm_compatibility
|
|
analyze_iommu_group
|
|
detect_optional_gpu_audio
|
|
check_vm_machine_type
|
|
check_switch_mode
|
|
evaluate_host_reboot_requirement
|
|
confirm_summary
|
|
|
|
# ── Phase 2: processing ───────────────────────────────
|
|
local run_title
|
|
run_title=$(_get_vm_run_title)
|
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
|
echo
|
|
else
|
|
clear
|
|
show_proxmenux_logo
|
|
msg_title "${run_title}"
|
|
fi
|
|
|
|
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
|
|
[[ "$SELECTED_GPU" == "nvidia" ]] && sanitize_nvidia_host_stack_for_vfio
|
|
cleanup_lxc_configs
|
|
cleanup_vm_config
|
|
ensure_vm_display_std
|
|
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
|
|
if declare -F attach_proxmenux_gpu_guard_to_vm >/dev/null 2>&1; then
|
|
ensure_proxmenux_gpu_guard_hookscript
|
|
attach_proxmenux_gpu_guard_to_vm "$SELECTED_VMID"
|
|
sync_proxmenux_gpu_guard_hooks
|
|
fi
|
|
[[ "$HOST_CONFIG_CHANGED" == "true" ]] && update_initramfs_host
|
|
|
|
# ── Phase 3: summary ─────────────────────────────────
|
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
|
echo
|
|
else
|
|
show_proxmenux_logo
|
|
msg_title "${run_title}"
|
|
cat "$screen_capture"
|
|
echo
|
|
fi
|
|
|
|
if [[ "$WIZARD_CALL" == "true" ]]; then
|
|
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
|
|
_set_wizard_result "applied_reboot_required"
|
|
else
|
|
_set_wizard_result "applied"
|
|
fi
|
|
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}"
|
|
fi
|
|
|
|
case "$SELECTED_GPU" in
|
|
nvidia)
|
|
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}"
|
|
;;
|
|
amd)
|
|
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}"
|
|
[[ -n "$AMD_ROM_FILE" ]] && \
|
|
echo -e "${TAB}${DGN}- $(translate 'ROM file used'): /usr/share/kvm/${AMD_ROM_FILE}${CL}"
|
|
;;
|
|
intel)
|
|
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}"
|
|
;;
|
|
esac
|
|
|
|
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.')"
|
|
|
|
echo
|
|
msg_success "$(translate 'GPU passthrough configured for VM') ${SELECTED_VMID} (${VM_NAME})."
|
|
echo
|
|
|
|
rm -f "$screen_capture"
|
|
|
|
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
|
|
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
|
|
}
|
|
|
|
main "$@"
|