Files
ProxMenux/scripts/gpu_tpu/switch_gpu_mode.sh
T
MacRimi b5476c75fa gpu: nvidia install drops VFIO autoload; GPU flows self-repair stale vfio.conf
The nvidia driver installer now writes only `nvidia` and `nvidia_uvm` into `/etc/modules-load.d/nvidia-vfio.conf`, leaving the vfio-pci modules for `switch_gpu_mode.sh` to add and remove when the user explicitly transitions between LXC and VM mode. A host with residual `/etc/modprobe.d/vfio.conf` entries from an earlier VM passthrough setup no longer has vfio-pci racing nvidia at boot and capturing the GPU by ID.

All three GPU-facing entry points also detect a stale `vfio.conf` entry whose vid:did belongs to a GPU currently bound to a non-vfio driver, and offer to clean it in place. `nvidia_installer.sh` runs the check as a pre-flight before install. `switch_gpu_mode.sh` presents an interactive dialog and rebuilds initramfs. `switch_gpu_mode_direct.sh` performs the same cleanup transparently for the Monitor web variant. Once cleaned, "Add GPU to LXC", the driver install and Switch Mode all proceed without any manual intervention.

The two supporting helpers `_pci_driver_of` and `_clean_vfio_conf_ids` previously local to `add_gpu_lxc.sh` move to `scripts/global/pci_passthrough_helpers.sh`, so every GPU-related script resolves them from a single source without duplication.
2026-08-17 12:05:40 +02:00

1503 lines
52 KiB
Bash

#!/bin/bash
# ==========================================================
# ProxMenux - GPU Switch Mode (VM <-> LXC)
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# Version : 1.0
# Last Updated: 05/04/2026
# ==========================================================
# Description:
# Moves an already-assigned GPU between the two modes it can
# live in on a Proxmox host:
# - VM mode (bound to vfio-pci, exclusive to one VM)
# - LXC mode (bound to the native driver, shared with CTs)
#
# Detects the current mode of each selected GPU and applies
# the host-side changes needed to switch (vfio.conf,
# blacklist.conf, /etc/modules, initramfs). Also handles the
# VM/LXC side so the switch doesn't leave dangling config
# pointing at a GPU the workload can no longer access.
#
# Features:
# - Multi-GPU selection (uniform current mode enforced)
# - SR-IOV guard (blocks VF / active-PF passthrough)
# - Blocked-ID policy list (e.g. Intel Arc A770)
# - IOMMU-group aware ID collection (sweeps siblings)
# - Conflict policy per affected VM/LXC
# (keep + disable onboot OR remove from config)
# - Orphan audio cascade: when a GPU leaves a VM, offer
# to remove companion audio hostpci entries and clean
# vfio.conf if no other VM still uses those IDs
# - Precise BDF regex for hostpci removal
# (no substring collision between unrelated GPUs)
# - NVIDIA stack sanitize/restore (udev, module-load,
# hard-blacklist) depending on target mode
# - Rebuilds initramfs only if host config actually changed
# - Reboot prompt at the end
# ==========================================================
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/proxmenux_gpu_switch_mode.log"
screen_capture="/tmp/proxmenux_gpu_switch_mode_screen_$$.txt"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# Both helper libraries are required for the SR-IOV guard and the audio
# orphan cascade to work. Surface a loud warning if neither path resolves
# — the previous behaviour evaluated `declare -F` later and silently
# disabled the validations, leaving the user thinking they were
# protected. Audit Tier 6 — `switch_gpu_mode.sh` silent helper loss.
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"
else
msg_warn "$(translate 'pci_passthrough_helpers.sh missing — SR-IOV / orphan-audio guards will be skipped')"
fi
load_language
initialize_cache
declare -a ALL_GPU_PCIS=()
declare -a ALL_GPU_TYPES=()
declare -a ALL_GPU_NAMES=()
declare -a ALL_GPU_DRIVERS=()
declare -a ALL_GPU_VIDDID=()
declare -a SELECTED_GPU_IDX=()
declare -a SELECTED_IOMMU_IDS=()
declare -a SELECTED_PCI_SLOTS=()
declare -a LXC_AFFECTED_CTIDS=()
declare -a LXC_AFFECTED_NAMES=()
declare -a LXC_AFFECTED_RUNNING=()
declare -a LXC_AFFECTED_ONBOOT=()
declare -a VM_AFFECTED_IDS=()
declare -a VM_AFFECTED_NAMES=()
declare -a VM_AFFECTED_RUNNING=()
declare -a VM_AFFECTED_ONBOOT=()
TARGET_MODE="" # vm | lxc
CURRENT_MODE="" # vm | lxc | mixed
LXC_ACTION="" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
VM_ACTION="" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
GPU_COUNT=0
HOST_CONFIG_CHANGED=false
_set_title() {
show_proxmenux_logo
case "$TARGET_MODE" in
vm) msg_title "GPU Switch Mode (GPU -> VM)" ;;
lxc) msg_title "GPU Switch Mode (GPU -> LXC)" ;;
*) msg_title "GPU Switch Mode (VM <-> LXC)" ;;
esac
}
_add_line_if_missing() {
local line="$1"
local file="$2"
touch "$file"
if ! grep -qFx "$line" "$file" 2>/dev/null; then
echo "$line" >>"$file"
HOST_CONFIG_CHANGED=true
fi
}
_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
}
_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"
}
_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"
}
_get_iommu_group_ids() {
local pci_full="$1"
local group_link="/sys/bus/pci/devices/${pci_full}/iommu_group"
[[ ! -L "$group_link" ]] && return
local group_dir
group_dir="/sys/kernel/iommu_groups/$(basename "$(readlink "$group_link")")/devices"
for dev_path in "${group_dir}/"*; do
[[ -e "$dev_path" ]] || continue
local dev dev_class vid did
dev=$(basename "$dev_path")
dev_class=$(cat "/sys/bus/pci/devices/${dev}/class" 2>/dev/null)
[[ "$dev_class" == 0x0604* || "$dev_class" == 0x0600* ]] && continue
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" ]] && echo "${vid}:${did}"
done
}
_read_vfio_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)
[[ -z "$ids_line" ]] && return
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//')
[[ -z "$ids_part" ]] && return
tr ',' '\n' <<< "$ids_part" | sed '/^$/d'
}
_write_vfio_ids() {
local -a ids=("$@")
local vfio_conf="/etc/modprobe.d/vfio.conf"
touch "$vfio_conf"
local current_line new_line ids_str
current_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
sed -i '/^options vfio-pci ids=/d' "$vfio_conf"
if [[ ${#ids[@]} -gt 0 ]]; then
ids_str=$(IFS=','; echo "${ids[*]}")
new_line="options vfio-pci ids=${ids_str} disable_vga=1"
echo "$new_line" >>"$vfio_conf"
[[ "$current_line" != "$new_line" ]] && HOST_CONFIG_CHANGED=true
else
[[ -n "$current_line" ]] && HOST_CONFIG_CHANGED=true
fi
}
_contains_in_array() {
local needle="$1"
shift
local x
for x in "$@"; do
[[ "$x" == "$needle" ]] && return 0
done
return 1
}
_remove_gpu_blacklist() {
local gpu_type="$1"
local blacklist_file="/etc/modprobe.d/blacklist.conf"
[[ ! -f "$blacklist_file" ]] && return
local changed=false
case "$gpu_type" in
nvidia)
grep -qE '^blacklist (nouveau|nvidia|nvidiafb|nvidia_drm|nvidia_modeset|nvidia_uvm|lbm-nouveau)$|^options nouveau modeset=0$' "$blacklist_file" 2>/dev/null && changed=true
sed -i '/^blacklist nouveau$/d' "$blacklist_file"
sed -i '/^blacklist nvidia$/d' "$blacklist_file"
sed -i '/^blacklist nvidiafb$/d' "$blacklist_file"
sed -i '/^blacklist nvidia_drm$/d' "$blacklist_file"
sed -i '/^blacklist nvidia_modeset$/d' "$blacklist_file"
sed -i '/^blacklist nvidia_uvm$/d' "$blacklist_file"
sed -i '/^blacklist lbm-nouveau$/d' "$blacklist_file"
sed -i '/^options nouveau modeset=0$/d' "$blacklist_file"
;;
amd)
grep -qE '^blacklist (radeon|amdgpu)$' "$blacklist_file" 2>/dev/null && changed=true
sed -i '/^blacklist radeon$/d' "$blacklist_file"
sed -i '/^blacklist amdgpu$/d' "$blacklist_file"
;;
intel)
grep -qE '^blacklist i915$' "$blacklist_file" 2>/dev/null && changed=true
sed -i '/^blacklist i915$/d' "$blacklist_file"
;;
esac
$changed && HOST_CONFIG_CHANGED=true
$changed
}
_add_gpu_blacklist() {
local gpu_type="$1"
local blacklist_file="/etc/modprobe.d/blacklist.conf"
touch "$blacklist_file"
case "$gpu_type" in
nvidia)
_add_line_if_missing "blacklist nouveau" "$blacklist_file"
_add_line_if_missing "blacklist nvidia" "$blacklist_file"
_add_line_if_missing "blacklist nvidiafb" "$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 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
}
_sanitize_nvidia_host_stack_for_vfio() {
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
# Disable NVIDIA udev rules that trigger nvidia-smi (causes conflict with vfio-pci)
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
if [[ -f "$udev_rules" ]]; then
mv "$udev_rules" "${udev_rules}.proxmenux-disabled" >>"$LOG_FILE" 2>&1 || true
udevadm control --reload-rules >>"$LOG_FILE" 2>&1 || true
changed=true
fi
# Create hard blacklist to prevent ANY nvidia module loading (even via modprobe/nvidia-smi)
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
if [[ ! -f "$nvidia_blacklist" ]]; then
cat > "$nvidia_blacklist" <<'EOF'
# ProxMenux: Hard blacklist to prevent ANY nvidia module loading in VFIO mode
# This prevents nvidia-smi and other tools from triggering module load attempts
install nvidia /bin/false
install nvidia_uvm /bin/false
install nvidia_drm /bin/false
install nvidia_modeset /bin/false
EOF
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
# Sync components_status.json — the host driver stays on disk but is
# not in use because the GPU now belongs to a VM. Prevents the update
# notification path (and any future logic gated on nvidia_driver.status)
# from acting on a state that no longer matches reality.
if declare -F update_component_status >/dev/null 2>&1; then
local _nvd_ver
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
update_component_status "nvidia_driver" "vfio_passthrough" \
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
fi
}
_restore_nvidia_host_stack_for_lxc() {
local changed=false
local state_file="/var/lib/proxmenux/nvidia-host-services.state"
local disabled_file="/etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio"
local active_file="/etc/modules-load.d/nvidia-vfio.conf"
# New per-BDF model: drop every NVIDIA BDF from the initramfs binder so
# the nvidia module reclaims the GPU after the next reboot. Idempotent:
# no-op if no NVIDIA BDFs are tracked. Vendor 10de = NVIDIA.
if declare -F _proxmenux_vfio_bind_purge_vendor >/dev/null 2>&1; then
_proxmenux_vfio_bind_purge_vendor "10de" && changed=true
fi
# Remove hard blacklist that was preventing nvidia module loading
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
if [[ -f "$nvidia_blacklist" ]]; then
rm -f "$nvidia_blacklist" >>"$LOG_FILE" 2>&1 || true
changed=true
fi
# Restore NVIDIA udev rules if they were disabled
local udev_disabled="/etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled"
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
if [[ -f "$udev_disabled" ]]; then
mv "$udev_disabled" "$udev_rules" >>"$LOG_FILE" 2>&1 || true
udevadm control --reload-rules >>"$LOG_FILE" 2>&1 || true
changed=true
fi
# Restore previous modules-load policy if ProxMenux disabled it in VM mode.
if [[ -f "$disabled_file" ]]; then
mv "$disabled_file" "$active_file" >>"$LOG_FILE" 2>&1 || true
changed=true
fi
# Best effort: load NVIDIA kernel modules now that we are back in native mode.
# If not installed, these calls simply fail silently.
modprobe nvidia >/dev/null 2>&1 || true
modprobe nvidia_uvm >/dev/null 2>&1 || true
modprobe nvidia_modeset >/dev/null 2>&1 || true
modprobe nvidia_drm >/dev/null 2>&1 || true
if [[ -f "$state_file" ]]; then
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local svc enabled active
svc=$(echo "$line" | awk '{print $1}')
enabled=$(echo "$line" | awk -F'enabled=' '{print $2}' | awk '{print $1}')
active=$(echo "$line" | awk -F'active=' '{print $2}' | awk '{print $1}')
[[ "$enabled" == "1" ]] && systemctl enable "$svc" >>"$LOG_FILE" 2>&1 || true
[[ "$active" == "1" ]] && systemctl start "$svc" >>"$LOG_FILE" 2>&1 || true
done <"$state_file"
rm -f "$state_file"
changed=true
fi
if $changed; then
HOST_CONFIG_CHANGED=true
msg_ok "$(translate 'NVIDIA host services/autoload restored for native mode')" | tee -a "$screen_capture"
else
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for native mode')" | tee -a "$screen_capture"
fi
# Sync components_status.json back to installed — the host has reclaimed
# the GPU and the nvidia stack is being reloaded. Restores the state to
# what it was before the VFIO switch so the update notification path and
# the auto-reinstall gate see the driver as active on the host again.
if declare -F update_component_status >/dev/null 2>&1; then
local _nvd_ver
_nvd_ver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1)
if [[ -z "$_nvd_ver" ]]; then
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
fi
update_component_status "nvidia_driver" "installed" \
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
fi
}
_add_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"
}
_remove_amd_softdep() {
local vfio_conf="/etc/modprobe.d/vfio.conf"
[[ ! -f "$vfio_conf" ]] && return
local changed=false
grep -qE '^softdep (radeon|amdgpu|snd_hda_intel) pre: vfio-pci$' "$vfio_conf" 2>/dev/null && changed=true
sed -i '/^softdep radeon pre: vfio-pci$/d' "$vfio_conf"
sed -i '/^softdep amdgpu pre: vfio-pci$/d' "$vfio_conf"
sed -i '/^softdep snd_hda_intel pre: vfio-pci$/d' "$vfio_conf"
$changed && HOST_CONFIG_CHANGED=true
$changed
}
_add_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
local mod
for mod in "${modules[@]}"; do
_add_line_if_missing "$mod" /etc/modules
done
}
_remove_vfio_modules_if_unused() {
local vfio_count
vfio_count=$(_read_vfio_ids | wc -l | tr -d '[:space:]')
[[ "$vfio_count" != "0" ]] && return 1
local modules_file="/etc/modules"
[[ ! -f "$modules_file" ]] && return 1
local had_any=false
grep -qE '^vfio$|^vfio_iommu_type1$|^vfio_pci$|^vfio_virqfd$' "$modules_file" 2>/dev/null && had_any=true
sed -i '/^vfio$/d' "$modules_file"
sed -i '/^vfio_iommu_type1$/d' "$modules_file"
sed -i '/^vfio_pci$/d' "$modules_file"
sed -i '/^vfio_virqfd$/d' "$modules_file"
if $had_any; then
HOST_CONFIG_CHANGED=true
return 0
fi
return 1
}
_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
}
_selected_types_unique() {
local -a out=()
local idx t
for idx in "${SELECTED_GPU_IDX[@]}"; do
t="${ALL_GPU_TYPES[$idx]}"
_contains_in_array "$t" "${out[@]}" || out+=("$t")
done
printf '%s\n' "${out[@]}"
}
detect_host_gpus() {
ALL_GPU_PCIS=()
ALL_GPU_TYPES=()
ALL_GPU_NAMES=()
ALL_GPU_DRIVERS=()
ALL_GPU_VIDDID=()
while IFS= read -r line; do
local pci_short pci_full name type driver viddid pci_info
pci_short=$(echo "$line" | awk '{print $1}')
pci_full="0000:${pci_short}"
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
continue
fi
driver=$(_get_pci_driver "$pci_full")
viddid=$(echo "$line" | grep -oE '\[[0-9a-f]{4}:[0-9a-f]{4}\]' | tr -d '[]')
ALL_GPU_PCIS+=("$pci_full")
ALL_GPU_TYPES+=("$type")
ALL_GPU_NAMES+=("$name")
ALL_GPU_DRIVERS+=("$driver")
ALL_GPU_VIDDID+=("$viddid")
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
dialog --backtitle "ProxMenux" \
--title "$(translate 'GPU Switch Mode')" \
--msgbox "\n$(translate 'No compatible GPUs were detected on this host.')" 8 64
exit 0
fi
}
_selected_gpu_current_mode() {
local mode=""
local idx drv cur
for idx in "${SELECTED_GPU_IDX[@]}"; do
drv="${ALL_GPU_DRIVERS[$idx]}"
if [[ "$drv" == "vfio-pci" ]]; then
cur="vm"
else
cur="lxc"
fi
if [[ -z "$mode" ]]; then
mode="$cur"
elif [[ "$mode" != "$cur" ]]; then
echo "mixed"
return
fi
done
[[ -z "$mode" ]] && mode="lxc"
echo "$mode"
}
check_stale_vfio_config_switch_mode() {
local vfio_conf="/etc/modprobe.d/vfio.conf"
[[ ! -f "$vfio_conf" ]] && return 0
local ids_line ids_part
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
[[ -z "$ids_line" ]] && return 0
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//')
[[ -z "$ids_part" ]] && return 0
local -a legacy_ids=()
local legacy_list=""
local idx viddid drv pci name
for idx in "${SELECTED_GPU_IDX[@]}"; do
viddid="${ALL_GPU_VIDDID[$idx]}"
drv="${ALL_GPU_DRIVERS[$idx]}"
pci="${ALL_GPU_PCIS[$idx]}"
name="${ALL_GPU_NAMES[$idx]}"
[[ -z "$viddid" ]] && continue
[[ "$drv" == "vfio-pci" ]] && continue
if echo ",${ids_part}," | grep -q ",${viddid},"; then
legacy_ids+=("$viddid")
legacy_list+=" • ${name} (${pci}) [${viddid}]\n"
fi
done
[[ ${#legacy_ids[@]} -eq 0 ]] && return 0
local msg
msg="\n$(translate 'The following selected GPU(s) still have a VFIO passthrough entry in') /etc/modprobe.d/vfio.conf:\n\n"
msg+="${legacy_list}\n"
msg+="$(translate 'The active kernel driver is not vfio-pci, but the entry will rebind the GPU to vfio-pci on the next reboot.')\n\n"
msg+="\Z1\Zb$(translate 'Do you want to remove the stale entry from vfio.conf?')\Zn"
dialog --colors --backtitle "ProxMenux" \
--title "$(translate 'Stale VFIO Config Detected')" \
--yesno "$msg" 18 80 || return 0
if declare -F _clean_vfio_conf_ids >/dev/null 2>&1 \
&& _clean_vfio_conf_ids "${legacy_ids[@]}"; then
msg_info "$(translate 'Rebuilding initramfs after vfio.conf cleanup...')"
update-initramfs -u >/dev/null 2>&1 || true
dialog --backtitle "ProxMenux" \
--title "$(translate 'Stale VFIO Config Cleaned')" \
--msgbox "\n$(translate 'The vfio.conf entries have been removed and initramfs rebuilt.')\n\n$(translate 'A reboot is recommended before the GPU is guaranteed to stay on the native driver.')" \
12 78
exit 0
fi
}
select_target_mode() {
CURRENT_MODE=$(_selected_gpu_current_mode)
check_stale_vfio_config_switch_mode
if [[ "$CURRENT_MODE" == "mixed" ]]; then
local msg idx mode_label
msg="\n$(translate 'Mixed current mode detected in selected GPU(s).')\n\n"
msg+="$(translate 'Please select GPU(s) that are currently in the same mode and try again.')\n\n"
msg+="$(translate 'Selected GPU(s):')\n"
for idx in "${SELECTED_GPU_IDX[@]}"; do
if [[ "${ALL_GPU_DRIVERS[$idx]}" == "vfio-pci" ]]; then
mode_label="GPU -> VM"
else
mode_label="GPU -> LXC"
fi
msg+=" • ${ALL_GPU_NAMES[$idx]} (${ALL_GPU_PCIS[$idx]}) [${mode_label}]\n"
done
dialog --backtitle "ProxMenux" \
--title "$(translate 'Mixed GPU Modes')" \
--msgbox "$msg" 20 94
return 2
fi
local menu_title menu_option_tag menu_option_desc current_mode_label current_mode_highlight
if [[ "$CURRENT_MODE" == "vm" ]]; then
TARGET_MODE="lxc"
current_mode_label="GPU -> VM (VFIO passthrough mode)"
menu_option_tag="lxc"
menu_option_desc="$(translate 'Switch to GPU -> LXC (native driver mode)')"
else
TARGET_MODE="vm"
current_mode_label="GPU -> LXC (native driver mode)"
menu_option_tag="vm"
menu_option_desc="$(translate 'Switch to GPU -> VM (VFIO passthrough mode)')"
fi
current_mode_highlight="\\Zb\\Z4${current_mode_label}\\Zn"
menu_title="\n$(translate 'Select target mode for selected GPU(s):')\n\n$(translate 'Current mode'): ${current_mode_highlight}\n\n$(translate 'Available action'):"
local selected
selected=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'GPU Switch Mode')" \
--menu "$menu_title" 16 80 6 \
"$menu_option_tag" "$menu_option_desc" \
2>&1 >/dev/tty) || exit 0
[[ "$selected" != "$menu_option_tag" ]] && exit 0
return 0
}
# Return codes:
# 0 = compatible
# 1 = blocked and should exit
# 2 = blocked but user can reselect GPUs
validate_vm_mode_blocked_ids() {
[[ "$TARGET_MODE" != "vm" ]] && return 0
local -a blocked_lines=()
local idx viddid name pci
for idx in "${SELECTED_GPU_IDX[@]}"; do
viddid="${ALL_GPU_VIDDID[$idx]}"
name="${ALL_GPU_NAMES[$idx]}"
pci="${ALL_GPU_PCIS[$idx]}"
case "$viddid" in
8086:5a84|8086:5a85)
blocked_lines+=(" • ${name} (${pci}) [ID: ${viddid}]")
;;
esac
done
[[ ${#blocked_lines[@]} -eq 0 ]] && return 0
local msg
msg="\n\Zb\Z1$(translate 'Blocked GPU ID for VM Mode')\Zn\n\n"
msg+="$(translate 'At least one selected GPU is blocked by policy for GPU -> VM mode due to passthrough instability risk.')\n\n"
msg+="$(translate 'Blocked device(s):')\n"
local line
for line in "${blocked_lines[@]}"; do
msg+="${line}\n"
done
msg+="\n$(translate 'Recommended: use GPU -> LXC mode for these devices.')\n"
if [[ "$GPU_COUNT" -gt 1 ]]; then
msg+="\n$(translate 'Please reselect GPU(s) and choose only compatible devices for VM mode.')"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'GPU Switch Mode Blocked')" \
--msgbox "$msg" 20 88
return 2
fi
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'GPU Switch Mode Blocked')" \
--msgbox "$msg" 19 84
return 1
}
select_gpus() {
SELECTED_GPU_IDX=()
if [[ "$GPU_COUNT" -eq 1 ]]; then
SELECTED_GPU_IDX=(0)
return 0
fi
local -a menu_items=()
local i
for i in "${!ALL_GPU_PCIS[@]}"; do
menu_items+=("$i" "${ALL_GPU_NAMES[$i]} [${ALL_GPU_DRIVERS[$i]}] — ${ALL_GPU_PCIS[$i]}" "off")
done
local raw sel
raw=$(dialog --backtitle "ProxMenux" \
--title "$(translate 'Select GPU(s)')" \
--checklist "\n$(translate 'Select one or more GPU(s) to switch mode:')" 20 96 12 \
"${menu_items[@]}" \
2>&1 >/dev/tty) || exit 0
sel=$(echo "$raw" | tr -d '"')
if [[ -z "$sel" ]]; then
dialog --backtitle "ProxMenux" \
--title "$(translate 'Select GPU(s)')" \
--msgbox "\n$(translate 'No GPU selected.')" 7 52
exit 0
fi
read -ra SELECTED_GPU_IDX <<< "$sel"
}
# ==========================================================
# SR-IOV guard — abort mode switch when SR-IOV is active
# ==========================================================
# Intel i915-sriov-dkms and AMD MxGPU split a Physical Function (PF) into
# multiple Virtual Functions (VFs). Switching the PF's driver destroys
# every VF; switching a VF's driver affects only that VF. ProxMenux does
# not yet manage the SR-IOV lifecycle (create/destroy VFs, track per-VF
# ownership), so operating on a PF with active VFs — or on a VF itself —
# would leave the user's virtualization stack in an inconsistent state.
# We detect the situation early and hand the user back to the Proxmox
# web UI, which understands VFs as first-class PCI devices.
check_sriov_and_block_if_needed() {
declare -F _pci_sriov_role >/dev/null 2>&1 || return 0
local idx pci role first_word pf_bdf active_count
local -a vf_list=()
local -a pf_list=()
for idx in "${SELECTED_GPU_IDX[@]}"; do
pci="${ALL_GPU_PCIS[$idx]}"
role=$(_pci_sriov_role "$pci")
first_word="${role%% *}"
case "$first_word" in
vf)
pf_bdf="${role#vf }"
vf_list+=("${pci}|${pf_bdf}")
;;
pf-active)
active_count="${role#pf-active }"
pf_list+=("${pci}|${active_count}")
;;
esac
done
[[ ${#vf_list[@]} -eq 0 && ${#pf_list[@]} -eq 0 ]] && return 0
local title msg entry bdf parent cnt
title="$(translate 'SR-IOV Configuration Detected')"
msg="\n"
if [[ ${#vf_list[@]} -gt 0 ]]; then
msg+="$(translate 'The following selected device(s) are SR-IOV Virtual Functions (VFs):')\n\n"
for entry in "${vf_list[@]}"; do
bdf="${entry%%|*}"
parent="${entry#*|}"
msg+=" • ${bdf} $(translate '(parent PF:') ${parent})\n"
done
msg+="\n"
fi
if [[ ${#pf_list[@]} -gt 0 ]]; then
msg+="$(translate 'The following selected device(s) are Physical Functions with active Virtual Functions:')\n\n"
for entry in "${pf_list[@]}"; do
bdf="${entry%%|*}"
cnt="${entry#*|}"
msg+=" • ${bdf}${cnt} $(translate 'active VF(s)')\n"
done
msg+="\n"
fi
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.')"
dialog --backtitle "ProxMenux" \
--title "$title" \
--msgbox "$msg" 20 80
exit 0
}
collect_selected_iommu_ids() {
SELECTED_IOMMU_IDS=()
SELECTED_PCI_SLOTS=()
local idx pci viddid slot
for idx in "${SELECTED_GPU_IDX[@]}"; do
pci="${ALL_GPU_PCIS[$idx]}"
viddid="${ALL_GPU_VIDDID[$idx]}"
slot="${pci#0000:}"
slot="${slot%.*}"
SELECTED_PCI_SLOTS+=("$slot")
local -a group_ids=()
mapfile -t group_ids < <(_get_iommu_group_ids "$pci")
if [[ ${#group_ids[@]} -gt 0 ]]; then
local gid
for gid in "${group_ids[@]}"; do
_contains_in_array "$gid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$gid")
done
elif [[ -n "$viddid" ]]; then
_contains_in_array "$viddid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$viddid")
fi
done
}
_lxc_conf_uses_type() {
local conf="$1"
local gpu_type="$2"
case "$gpu_type" 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 ;;
*) return 1 ;;
esac
}
detect_affected_lxc_for_selected() {
LXC_AFFECTED_CTIDS=()
LXC_AFFECTED_NAMES=()
LXC_AFFECTED_RUNNING=()
LXC_AFFECTED_ONBOOT=()
local -a types=()
mapfile -t types < <(_selected_types_unique)
local conf
for conf in /etc/pve/lxc/*.conf; do
[[ -f "$conf" ]] || continue
local matched=false
local t
for t in "${types[@]}"; do
_lxc_conf_uses_type "$conf" "$t" && matched=true && break
done
$matched || continue
local ctid ct_name run onb
ctid=$(basename "$conf" .conf)
ct_name=$(pct config "$ctid" 2>/dev/null | awk '/^hostname:/ {print $2}')
[[ -z "$ct_name" ]] && ct_name="CT-${ctid}"
run=0; onb=0
_ct_is_running "$ctid" && run=1
_ct_onboot_enabled "$ctid" && onb=1
LXC_AFFECTED_CTIDS+=("$ctid")
LXC_AFFECTED_NAMES+=("$ct_name")
LXC_AFFECTED_RUNNING+=("$run")
LXC_AFFECTED_ONBOOT+=("$onb")
done
}
prompt_lxc_action_for_vm_mode() {
[[ ${#LXC_AFFECTED_CTIDS[@]} -eq 0 ]] && return 0
local running_count=0 onboot_count=0 i
for i in "${!LXC_AFFECTED_CTIDS[@]}"; do
[[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]] && running_count=$((running_count + 1))
[[ "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]] && onboot_count=$((onboot_count + 1))
done
local msg choice
msg="\n$(translate 'The selected GPU(s) are used in these LXC container(s):')\n\n"
for i in "${!LXC_AFFECTED_CTIDS[@]}"; do
local st ob
st="$(translate 'stopped')"; ob="onboot=0"
[[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]] && st="$(translate 'running')"
[[ "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]] && ob="onboot=1"
msg+=" • CT ${LXC_AFFECTED_CTIDS[$i]} (${LXC_AFFECTED_NAMES[$i]}) [${st}, ${ob}]\n"
done
msg+="\n$(translate 'Switching to GPU -> VM mode requires exclusive VFIO binding.')\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_count}\Zn\n"
msg+="\n$(translate 'Choose conflict policy:')"
choice=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'LXC Conflict Policy')" \
--default-item "2" \
--menu "$msg" 24 80 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 "$choice" in
1) LXC_ACTION="keep_gpu_disable_onboot" ;;
2) LXC_ACTION="remove_gpu_keep_onboot" ;;
*) exit 0 ;;
esac
}
_remove_type_from_lxc_conf() {
local conf="$1"
local gpu_type="$2"
case "$gpu_type" 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"
;;
esac
}
apply_lxc_action_for_vm_mode() {
[[ ${#LXC_AFFECTED_CTIDS[@]} -eq 0 ]] && return 0
local -a types=()
mapfile -t types < <(_selected_types_unique)
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}..."
# _pmx_stop_lxc: unlock + graceful shutdown with forceStop+timeout,
# fallback to pct stop. Prevents the indefinite hang that raw
# `pct stop` triggers on locked / stuck containers.
if _pmx_stop_lxc "$ctid" "$LOG_FILE"; 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
fi
if [[ "$LXC_ACTION" == "keep_gpu_disable_onboot" && "${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"
fi
fi
if [[ "$LXC_ACTION" == "remove_gpu_keep_onboot" && -f "$conf" ]]; then
local t
for t in "${types[@]}"; do
_remove_type_from_lxc_conf "$conf" "$t"
done
msg_ok "$(translate 'GPU access removed from LXC') ${ctid}" | tee -a "$screen_capture"
fi
done
}
detect_affected_vms_for_selected() {
VM_AFFECTED_IDS=()
VM_AFFECTED_NAMES=()
VM_AFFECTED_RUNNING=()
VM_AFFECTED_ONBOOT=()
local conf
for conf in /etc/pve/qemu-server/*.conf; do
[[ -f "$conf" ]] || continue
local matched=false slot
for slot in "${SELECTED_PCI_SLOTS[@]}"; do
if grep -qE "hostpci[0-9]+:.*(0000:)?${slot}(\\.[0-7])?([,[:space:]]|$)" "$conf"; then
matched=true
break
fi
done
$matched || continue
local vmid vm_name run onb
vmid=$(basename "$conf" .conf)
vm_name=$(grep "^name:" "$conf" 2>/dev/null | awk '{print $2}')
[[ -z "$vm_name" ]] && vm_name="VM-${vmid}"
run=0; onb=0
_vm_is_running "$vmid" && run=1
_vm_onboot_enabled "$vmid" && onb=1
VM_AFFECTED_IDS+=("$vmid")
VM_AFFECTED_NAMES+=("$vm_name")
VM_AFFECTED_RUNNING+=("$run")
VM_AFFECTED_ONBOOT+=("$onb")
done
}
prompt_vm_action_for_lxc_mode() {
[[ ${#VM_AFFECTED_IDS[@]} -eq 0 ]] && return 0
local running_count=0 onboot_count=0 i
for i in "${!VM_AFFECTED_IDS[@]}"; do
[[ "${VM_AFFECTED_RUNNING[$i]}" == "1" ]] && running_count=$((running_count + 1))
[[ "${VM_AFFECTED_ONBOOT[$i]}" == "1" ]] && onboot_count=$((onboot_count + 1))
done
local msg choice
msg="\n$(translate 'The selected GPU(s) are configured in these VM(s):')\n\n"
for i in "${!VM_AFFECTED_IDS[@]}"; do
local st ob
st="$(translate 'stopped')"; ob="onboot=0"
[[ "${VM_AFFECTED_RUNNING[$i]}" == "1" ]] && st="$(translate 'running')"
[[ "${VM_AFFECTED_ONBOOT[$i]}" == "1" ]] && ob="onboot=1"
msg+=" • VM ${VM_AFFECTED_IDS[$i]} (${VM_AFFECTED_NAMES[$i]}) [${st}, ${ob}]\n"
done
msg+="\n$(translate 'Switching to GPU -> LXC mode removes VFIO exclusivity.')\n"
[[ "$running_count" -gt 0 ]] && msg+="\Z1$(translate 'Running VM detected'): ${running_count}\Zn\n"
[[ "$onboot_count" -gt 0 ]] && msg+="\Z1\Zb$(translate 'Start on boot enabled'): ${onboot_count}\Zn\n"
msg+="\n$(translate 'Choose conflict policy:')"
choice=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'VM Conflict Policy')" \
--default-item "1" \
--menu "$msg" 24 80 8 \
"1" "$(translate 'Keep GPU in VM config (disable Start on boot)')" \
"2" "$(translate 'Remove GPU from VM config (keep Start on boot)')" \
2>&1 >/dev/tty) || exit 0
case "$choice" in
1) VM_ACTION="keep_gpu_disable_onboot" ;;
2) VM_ACTION="remove_gpu_keep_onboot" ;;
*) exit 0 ;;
esac
}
apply_vm_action_for_lxc_mode() {
[[ ${#VM_AFFECTED_IDS[@]} -eq 0 ]] && return 0
local i
for i in "${!VM_AFFECTED_IDS[@]}"; do
local vmid conf
vmid="${VM_AFFECTED_IDS[$i]}"
conf="/etc/pve/qemu-server/${vmid}.conf"
if [[ "${VM_AFFECTED_RUNNING[$i]}" == "1" ]]; then
msg_info "$(translate 'Stopping VM') ${vmid}..."
qm stop "$vmid" >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'VM stopped') ${vmid}" | tee -a "$screen_capture"
fi
if [[ "$VM_ACTION" == "keep_gpu_disable_onboot" && "${VM_AFFECTED_ONBOOT[$i]}" == "1" ]]; then
if qm set "$vmid" -onboot 0 >>"$LOG_FILE" 2>&1; then
msg_warn "$(translate 'Start on boot disabled for VM') ${vmid}" | tee -a "$screen_capture"
fi
fi
if [[ "$VM_ACTION" == "remove_gpu_keep_onboot" && -f "$conf" ]]; then
# Primary cleanup: strip hostpci lines whose BDF matches any of
# the GPU's selected slots. Matches both the PF function (.0) and
# any sibling audio or HDMI codec that shares the slot (typical
# for discrete NVIDIA/AMD cards where .1 is the HDMI audio).
#
# Precise regex: the slot must be followed by ".<function>" and
# either a delimiter or end-of-line. A looser ".*${slot}" would
# match by pure substring and delete unrelated hostpci entries —
# e.g. slot "00:02" would match inside "0000:02:00.0" (a dGPU at
# 02:00) and wipe both the iGPU and the unrelated dGPU.
local slot
for slot in "${SELECTED_PCI_SLOTS[@]}"; do
sed -E -i "/^hostpci[0-9]+:[[:space:]]*(0000:)?${slot}\.[0-7]([,[:space:]]|$)/d" "$conf"
done
msg_ok "$(translate 'GPU removed from VM config') ${vmid}" | tee -a "$screen_capture"
# Cascade cleanup: Intel iGPU passthrough typically pairs the GPU
# at 00:02.0 with chipset audio at 00:1f.3, which lives at a
# different slot and therefore survives the sed above. If it
# stays in the VM config after the GPU is gone, the VM either
# fails to start (vfio-pci no longer claims 8086:51c8 after the
# switch-back) or it steals host audio unnecessarily. Enumerate
# orphan audio hostpci entries and ask the user what to do.
if declare -F _vm_list_orphan_audio_hostpci >/dev/null 2>&1; then
# Concatenate orphan-audio entries across ALL selected GPUs.
# The previous code only checked `SELECTED_PCI_SLOTS[0]`, so when
# the user switched 2 dGPUs at once and each had its own audio
# companion, the second GPU's audio was left dangling in the VM
# config. Audit Tier 6 — orphan audio solo del primer slot.
local _orphan_audio=""
local _slot
for _slot in "${SELECTED_PCI_SLOTS[@]}"; do
local _piece
_piece=$(_vm_list_orphan_audio_hostpci "$vmid" "$_slot")
if [[ -n "$_piece" ]]; then
[[ -n "$_orphan_audio" ]] && _orphan_audio+=$'\n'
_orphan_audio+="$_piece"
fi
done
if [[ -n "$_orphan_audio" ]]; then
local -a _orph_items=()
local _line _o_idx _o_bdf _o_name
while IFS= read -r _line; do
[[ -z "$_line" ]] && continue
_o_idx="${_line%%|*}"
_line="${_line#*|}"
_o_bdf="${_line%%|*}"
_o_name="${_line#*|}"
_orph_items+=("$_o_idx" "${_o_bdf} ${_o_name}" "on")
done <<< "$_orphan_audio"
local _prompt _selected
_prompt="\n$(translate 'The GPU is being detached from VM') \Zb${vmid}\Zn.\n\n"
_prompt+="$(translate 'The VM also has these audio devices assigned via PCI passthrough — typically added together with the GPU. Remove them too?')\n\n"
_prompt+="$(translate '(Checked entries will be removed. Uncheck to keep in VM.)')"
_selected=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'Associated Audio Devices')" \
--checklist "$_prompt" 20 84 "$(( ${#_orph_items[@]} / 3 ))" \
"${_orph_items[@]}" \
2>&1 >/dev/tty) || _selected=""
_selected=$(echo "$_selected" | tr -d '"')
# Cross-reference table so we can recover each selected idx's
# original BDF (we need it for vendor:device lookup below).
declare -A _orphan_bdf_by_idx=()
local _o_line _o_i _o_b
while IFS= read -r _o_line; do
[[ -z "$_o_line" ]] && continue
_o_i="${_o_line%%|*}"
_o_line="${_o_line#*|}"
_o_b="${_o_line%%|*}"
_orphan_bdf_by_idx["$_o_i"]="$_o_b"
done <<< "$_orphan_audio"
local _sel _removed_audio="" _rem_bdf _vd_hex _dd_hex _vd_id
for _sel in $_selected; do
_rem_bdf="${_orphan_bdf_by_idx[$_sel]:-}"
if _vm_remove_hostpci_index "$vmid" "$_sel" "$LOG_FILE"; then
_removed_audio+=" hostpci${_sel}"
# Fix B: if the removed audio BDF is not referenced by any
# OTHER VM, its vendor:device can safely come out of
# /etc/modprobe.d/vfio.conf too. Without this step,
# SELECTED_IOMMU_IDS only held the GPU's own IOMMU group
# (e.g. 8086:46a3 for Intel iGPU) and the companion audio
# id (e.g. 8086:51c8 for chipset audio) survived in
# vfio.conf, so vfio-pci kept claiming it at next boot
# even though nothing used it.
[[ -z "$_rem_bdf" ]] && continue
if ! _pci_bdf_in_any_vm "$_rem_bdf" "${VM_AFFECTED_IDS[@]}"; then
_vd_hex=$(cat "/sys/bus/pci/devices/${_rem_bdf}/vendor" 2>/dev/null | sed 's/^0x//')
_dd_hex=$(cat "/sys/bus/pci/devices/${_rem_bdf}/device" 2>/dev/null | sed 's/^0x//')
if [[ -n "$_vd_hex" && -n "$_dd_hex" ]]; then
_vd_id="${_vd_hex}:${_dd_hex}"
if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then
SELECTED_IOMMU_IDS+=("$_vd_id")
fi
fi
fi
fi
done
unset _orphan_bdf_by_idx
if [[ -n "$_removed_audio" ]]; then
msg_ok "$(translate 'Associated audio removed from VM'): ${_removed_audio# }" \
| tee -a "$screen_capture"
fi
fi
fi
fi
done
}
_register_iommu_tool() {
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
command -v jq >/dev/null 2>&1 || return 0
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
&& mv "$tools_json.tmp" "$tools_json" || true
}
_enable_iommu_cmdline() {
local cpu_vendor
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
local iommu_param
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
iommu_param="intel_iommu=on"
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
iommu_param="amd_iommu=on"
else
return 1
fi
local cmdline_file="/etc/kernel/cmdline"
local grub_file="/etc/default/grub"
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
if ! grep -q "$iommu_param" "$cmdline_file"; then
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
fi
elif [[ -f "$grub_file" ]]; then
if ! grep -q "$iommu_param" "$grub_file"; then
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
update-grub >>"$LOG_FILE" 2>&1 || true
fi
else
return 1
fi
return 0
}
switch_to_vm_mode() {
detect_affected_lxc_for_selected
prompt_lxc_action_for_vm_mode
_set_title
collect_selected_iommu_ids
apply_lxc_action_for_vm_mode
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
_register_iommu_tool
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
elif grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
# Cross-check that IOMMU is *actually* active in the running kernel.
# The kernel parameter alone doesn't guarantee functional IOMMU —
# if the BIOS toggle is off, /sys/kernel/iommu_groups/ is empty even
# though intel_iommu=on is in cmdline. Without this gate we'd write
# vfio.conf and after reboot the GPU never gets claimed by VFIO.
# Audit Tier 6 — IOMMU check optimista.
if ! find /sys/kernel/iommu_groups -mindepth 1 -maxdepth 1 -name '[0-9]*' 2>/dev/null | grep -q .; then
msg_warn "$(translate 'intel_iommu/amd_iommu is set in cmdline but no IOMMU groups exist — IOMMU appears disabled in BIOS. Enable VT-d / AMD-Vi in firmware before continuing.')"
fi
_register_iommu_tool
HOST_CONFIG_CHANGED=true
msg_ok "$(translate 'IOMMU already configured in kernel parameters')" | tee -a "$screen_capture"
else
if _enable_iommu_cmdline; then
_register_iommu_tool
HOST_CONFIG_CHANGED=true
msg_ok "$(translate 'IOMMU kernel parameters configured')" | tee -a "$screen_capture"
else
msg_warn "$(translate 'Could not configure IOMMU kernel parameters automatically. Configure manually and reboot.')" | tee -a "$screen_capture"
fi
fi
_add_vfio_modules
msg_ok "$(translate 'VFIO modules configured in /etc/modules')" | tee -a "$screen_capture"
_configure_iommu_options
msg_ok "$(translate 'IOMMU interrupt remapping configured')" | tee -a "$screen_capture"
local -a current_ids=()
mapfile -t current_ids < <(_read_vfio_ids)
local id
for id in "${SELECTED_IOMMU_IDS[@]}"; do
_contains_in_array "$id" "${current_ids[@]}" || current_ids+=("$id")
done
_write_vfio_ids "${current_ids[@]}"
if [[ ${#SELECTED_IOMMU_IDS[@]} -gt 0 ]]; then
local ids_label
ids_label=$(IFS=','; echo "${SELECTED_IOMMU_IDS[*]}")
msg_ok "$(translate 'vfio-pci IDs configured') (${ids_label})" | tee -a "$screen_capture"
fi
local -a selected_types=()
mapfile -t selected_types < <(_selected_types_unique)
local t
for t in "${selected_types[@]}"; do
_add_gpu_blacklist "$t"
done
msg_ok "$(translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf')" | tee -a "$screen_capture"
_contains_in_array "nvidia" "${selected_types[@]}" && _sanitize_nvidia_host_stack_for_vfio
_contains_in_array "amd" "${selected_types[@]}" && _add_amd_softdep
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
msg_info "$(translate 'Updating initramfs (this may take a minute)...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'initramfs updated')" | tee -a "$screen_capture"
fi
}
_type_has_remaining_vfio_ids() {
local gpu_type="$1"
local -a remaining_ids=("$@")
remaining_ids=("${remaining_ids[@]:1}")
local idx viddid
for idx in "${!ALL_GPU_TYPES[@]}"; do
[[ "${ALL_GPU_TYPES[$idx]}" != "$gpu_type" ]] && continue
viddid="${ALL_GPU_VIDDID[$idx]}"
_contains_in_array "$viddid" "${remaining_ids[@]}" && return 0
done
return 1
}
switch_to_lxc_mode() {
collect_selected_iommu_ids
detect_affected_vms_for_selected
prompt_vm_action_for_lxc_mode
_set_title
apply_vm_action_for_lxc_mode
msg_info "$(translate 'Removing VFIO ownership for selected GPU(s)...')"
local -a current_ids=() remaining_ids=() removed_ids=()
mapfile -t current_ids < <(_read_vfio_ids)
local id remove
for id in "${current_ids[@]}"; do
remove=false
_contains_in_array "$id" "${SELECTED_IOMMU_IDS[@]}" && remove=true
if $remove; then
removed_ids+=("$id")
else
remaining_ids+=("$id")
fi
done
_write_vfio_ids "${remaining_ids[@]}"
if [[ ${#removed_ids[@]} -gt 0 ]]; then
local ids_label
ids_label=$(IFS=','; echo "${removed_ids[*]}")
msg_ok "$(translate 'VFIO device IDs removed from /etc/modprobe.d/vfio.conf') (${ids_label})" | tee -a "$screen_capture"
fi
local -a selected_types=()
mapfile -t selected_types < <(_selected_types_unique)
local t
for t in "${selected_types[@]}"; do
if ! _type_has_remaining_vfio_ids "$t" "${remaining_ids[@]}"; then
if _remove_gpu_blacklist "$t"; then
msg_ok "$(translate 'Driver blacklist removed for') ${t}" | tee -a "$screen_capture"
fi
if [[ "$t" == "nvidia" ]]; then
_restore_nvidia_host_stack_for_lxc
fi
fi
done
if ! _type_has_remaining_vfio_ids "amd" "${remaining_ids[@]}"; then
_remove_amd_softdep || true
fi
if _remove_vfio_modules_if_unused; then
msg_ok "$(translate 'VFIO modules removed from /etc/modules')" | tee -a "$screen_capture"
fi
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
msg_info "$(translate 'Updating initramfs (this may take a minute)...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'initramfs updated')" | tee -a "$screen_capture"
fi
}
# ==========================================================
# Send notification when GPU mode switch completes
# ==========================================================
_send_gpu_mode_notification() {
local new_mode="$1"
local old_mode="$2"
local notify_script="/usr/bin/notification_manager.py"
[[ ! -f "$notify_script" ]] && return 0
local hostname_short
hostname_short=$(hostname -s)
# Build GPU list for notification
local gpu_list=""
local idx
for idx in "${SELECTED_GPU_IDX[@]}"; do
gpu_list+="${ALL_GPU_NAMES[$idx]} (${ALL_GPU_PCIS[$idx]}), "
done
gpu_list="${gpu_list%, }"
local mode_label details
if [[ "$new_mode" == "vm" ]]; then
mode_label="GPU -> VM (VFIO passthrough)"
details="GPU(s) ready for VM passthrough. A host reboot may be required."
else
mode_label="GPU -> LXC (native driver)"
details="GPU(s) available for LXC containers with native drivers."
fi
python3 "$notify_script" --action send-raw --severity INFO \
--title "${hostname_short}: GPU mode changed to ${mode_label}" \
--message "GPU passthrough mode switched.
GPU(s): ${gpu_list}
Previous: ${old_mode}
New: ${mode_label}
${details}" 2>/dev/null || true
}
confirm_plan() {
local msg mode_line
if [[ "$TARGET_MODE" == "vm" ]]; then
mode_line="$(translate 'Target mode'): GPU -> VM (VFIO)"
else
mode_line="$(translate 'Target mode'): GPU -> LXC (native driver)"
fi
msg="\n${mode_line}\n\n$(translate 'Selected GPU(s)'):\n"
local idx
for idx in "${SELECTED_GPU_IDX[@]}"; do
msg+=" • ${ALL_GPU_NAMES[$idx]} (${ALL_GPU_PCIS[$idx]}) [${ALL_GPU_DRIVERS[$idx]}]\n"
done
msg+="\n$(translate 'Do you want to proceed?')"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'Confirm GPU Switch Mode')" \
--yesno "$msg" 18 88
[[ $? -ne 0 ]] && exit 0
}
final_summary() {
_set_title
cat "$screen_capture"
echo
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
echo -e "${TAB}${DGN}- $(translate 'Host GPU binding changed — reboot required.')${CL}"
whiptail --title "$(translate 'Reboot Required')" \
--yesno "$(translate 'A reboot is required to apply the new GPU mode. Do you want to restart now?')" 10 74
if [[ $? -eq 0 ]]; then
msg_warn "$(translate 'Rebooting the system...')"
reboot
else
msg_info2 "$(translate 'Please reboot manually to complete the switch.')"
msg_success "$(translate 'Press Enter to continue...')"
read -r
fi
else
echo -e "${TAB}${DGN}- $(translate 'No host VFIO/native binding changes were required.')${CL}"
msg_success "$(translate 'Press Enter to continue...')"
read -r
fi
}
main() {
: >"$LOG_FILE"
: >"$screen_capture"
detect_host_gpus
while true; do
select_gpus
check_sriov_and_block_if_needed
select_target_mode
[[ $? -eq 2 ]] && continue
validate_vm_mode_blocked_ids
case $? in
2) continue ;;
1) exit 0 ;;
esac
break
done
confirm_plan
clear
_set_title
echo
# Determine old mode before switch for notification
local old_mode_label
if [[ "$CURRENT_MODE" == "vm" ]]; then
old_mode_label="GPU -> VM (VFIO)"
else
old_mode_label="GPU -> LXC (native)"
fi
if [[ "$TARGET_MODE" == "vm" ]]; then
switch_to_vm_mode
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
_send_gpu_mode_notification "vm" "$old_mode_label"
else
switch_to_lxc_mode
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
_send_gpu_mode_notification "lxc" "$old_mode_label"
fi
final_summary
rm -f "$screen_capture"
}
main "$@"