mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
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.
1379 lines
47 KiB
Bash
1379 lines
47 KiB
Bash
#!/bin/bash
|
|
# ==========================================================
|
|
# ProxMenux - GPU Switch Mode Direct (VM <-> LXC)
|
|
# ==========================================================
|
|
# Author : MacRimi
|
|
# Copyright : (c) 2024 MacRimi
|
|
# License : GPL-3.0
|
|
# Version : 1.0
|
|
# Last Updated: 09/04/2026
|
|
# ==========================================================
|
|
# This script is a hybrid version for ProxMenux Monitor.
|
|
# It accepts parameters to skip GPU selection and uses
|
|
# hybrid dialogs for web rendering.
|
|
# ==========================================================
|
|
|
|
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
|
|
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
|
|
load_language
|
|
initialize_cache
|
|
|
|
# ==========================================================
|
|
# Global Variables
|
|
# ==========================================================
|
|
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
|
|
|
|
# Parameters from command line
|
|
PARAM_GPU_SLOT=""
|
|
PARAM_TARGET_MODE=""
|
|
|
|
# ==========================================================
|
|
# Helper Functions (same as original)
|
|
# ==========================================================
|
|
_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.
|
|
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
|
|
|
|
if [[ -f "$disabled_file" ]]; then
|
|
mv "$disabled_file" "$active_file" >>"$LOG_FILE" 2>&1 || true
|
|
changed=true
|
|
fi
|
|
|
|
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 idx t
|
|
local -a seen=()
|
|
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
|
t="${ALL_GPU_TYPES[$idx]}"
|
|
_contains_in_array "$t" "${seen[@]}" || { seen+=("$t"); echo "$t"; }
|
|
done
|
|
}
|
|
|
|
# ==========================================================
|
|
# GPU Detection
|
|
# ==========================================================
|
|
detect_host_gpus() {
|
|
ALL_GPU_PCIS=()
|
|
ALL_GPU_TYPES=()
|
|
ALL_GPU_NAMES=()
|
|
ALL_GPU_DRIVERS=()
|
|
ALL_GPU_VIDDID=()
|
|
|
|
local line pci name vendor vid did drv
|
|
while IFS= read -r line; do
|
|
pci=$(echo "$line" | awk '{print $1}')
|
|
[[ ! "$pci" =~ ^[0-9a-f]{4}: ]] && pci="0000:$pci"
|
|
name=$(echo "$line" | sed 's/^[^ ]* //')
|
|
|
|
vendor=""
|
|
if echo "$name" | grep -qi "nvidia"; then
|
|
vendor="nvidia"
|
|
elif echo "$name" | grep -qiE "amd|radeon"; then
|
|
vendor="amd"
|
|
elif echo "$name" | grep -qi "intel"; then
|
|
vendor="intel"
|
|
else
|
|
vendor="other"
|
|
fi
|
|
|
|
vid=$(cat "/sys/bus/pci/devices/${pci}/vendor" 2>/dev/null | sed 's/0x//')
|
|
did=$(cat "/sys/bus/pci/devices/${pci}/device" 2>/dev/null | sed 's/0x//')
|
|
drv=$(_get_pci_driver "$pci")
|
|
|
|
ALL_GPU_PCIS+=("$pci")
|
|
ALL_GPU_TYPES+=("$vendor")
|
|
ALL_GPU_NAMES+=("$name")
|
|
ALL_GPU_DRIVERS+=("$drv")
|
|
ALL_GPU_VIDDID+=("${vid}:${did}")
|
|
done < <(lspci -D | grep -iE "VGA|3D|Display" | grep -v "Audio")
|
|
|
|
GPU_COUNT=${#ALL_GPU_PCIS[@]}
|
|
}
|
|
|
|
# ==========================================================
|
|
# Find GPU by PCI Slot (new function for direct mode)
|
|
# ==========================================================
|
|
find_gpu_by_slot() {
|
|
local target_slot="$1"
|
|
SELECTED_GPU_IDX=()
|
|
|
|
# Normalize slot format (ensure 0000: prefix)
|
|
[[ ! "$target_slot" =~ ^[0-9a-f]{4}: ]] && target_slot="0000:$target_slot"
|
|
|
|
local i
|
|
for i in "${!ALL_GPU_PCIS[@]}"; do
|
|
if [[ "${ALL_GPU_PCIS[$i]}" == "$target_slot"* ]]; then
|
|
SELECTED_GPU_IDX+=("$i")
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
msg_error "$(translate 'GPU not found with slot'): $target_slot"
|
|
return 1
|
|
}
|
|
|
|
# ==========================================================
|
|
# SR-IOV guard — abort mode switch when SR-IOV is active
|
|
# ==========================================================
|
|
# Same policy as the interactive switch_gpu_mode.sh: refuse to operate on
|
|
# a Virtual Function or on a Physical Function that already has active
|
|
# VFs, since flipping drivers in that state collapses the VF tree and
|
|
# breaks every guest that was consuming a VF.
|
|
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 msg entry bdf parent cnt
|
|
msg="<div style='color:#f0ad4e;font-weight:bold;margin-bottom:10px;'>$(translate 'SR-IOV Configuration Detected')</div>"
|
|
|
|
if [[ ${#vf_list[@]} -gt 0 ]]; then
|
|
msg+="<p>$(translate 'The following selected device(s) are SR-IOV Virtual Functions (VFs):')</p><ul>"
|
|
for entry in "${vf_list[@]}"; do
|
|
bdf="${entry%%|*}"
|
|
parent="${entry#*|}"
|
|
msg+="<li><code>${bdf}</code> — $(translate 'parent PF:') <code>${parent}</code></li>"
|
|
done
|
|
msg+="</ul>"
|
|
fi
|
|
|
|
if [[ ${#pf_list[@]} -gt 0 ]]; then
|
|
msg+="<p>$(translate 'The following selected device(s) are Physical Functions with active Virtual Functions:')</p><ul>"
|
|
for entry in "${pf_list[@]}"; do
|
|
bdf="${entry%%|*}"
|
|
cnt="${entry#*|}"
|
|
msg+="<li><code>${bdf}</code> — ${cnt} $(translate 'active VF(s)')</li>"
|
|
done
|
|
msg+="</ul>"
|
|
fi
|
|
|
|
msg+="<p>$(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.')</p>"
|
|
|
|
hybrid_msgbox "$(translate 'SR-IOV Configuration Detected')" "$msg"
|
|
return 1
|
|
}
|
|
|
|
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="<div style='color:#ff6b6b;font-weight:bold;margin-bottom:10px;'>$(translate 'Blocked GPU ID for VM Mode')</div>"
|
|
msg+="<p>$(translate 'At least one selected GPU is blocked by policy for GPU -> VM mode due to passthrough instability risk.')</p>"
|
|
msg+="<p><strong>$(translate 'Blocked device(s)'):</strong></p><ul>"
|
|
local line
|
|
for line in "${blocked_lines[@]}"; do
|
|
msg+="<li>${line}</li>"
|
|
done
|
|
msg+="</ul>"
|
|
msg+="<p>$(translate 'Recommended: use GPU -> LXC mode for these devices.')</p>"
|
|
|
|
hybrid_msgbox "$(translate 'GPU Switch Mode Blocked')" "$msg"
|
|
return 1
|
|
}
|
|
|
|
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 Detection and Handling (hybrid dialogs)
|
|
# ==========================================================
|
|
_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
|
|
}
|
|
|
|
# HYBRID: LXC conflict policy prompt
|
|
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
|
|
msg="$(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+="$(translate 'Running containers detected'): ${running_count}\n"
|
|
[[ "$onboot_count" -gt 0 ]] && msg+="$(translate 'Start on boot enabled'): ${onboot_count}\n"
|
|
msg+="\n$(translate 'Choose conflict policy'):"
|
|
|
|
local choice
|
|
choice=$(hybrid_menu "$(translate 'LXC Conflict Policy')" "$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)')")
|
|
|
|
case "$choice" in
|
|
1) LXC_ACTION="keep_gpu_disable_onboot" ;;
|
|
2) LXC_ACTION="remove_gpu_keep_onboot" ;;
|
|
*)
|
|
msg_warn "$(translate 'Operation cancelled by user')"
|
|
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
|
|
}
|
|
|
|
# ==========================================================
|
|
# VM Detection and Handling (hybrid dialogs)
|
|
# ==========================================================
|
|
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
|
|
}
|
|
|
|
# HYBRID: VM conflict policy prompt
|
|
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
|
|
msg="$(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+="$(translate 'Running VM detected'): ${running_count}\n"
|
|
[[ "$onboot_count" -gt 0 ]] && msg+="$(translate 'Start on boot enabled'): ${onboot_count}\n"
|
|
msg+="\n$(translate 'Choose conflict policy'):"
|
|
|
|
local choice
|
|
choice=$(hybrid_menu "$(translate 'VM Conflict Policy')" "$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)')")
|
|
|
|
case "$choice" in
|
|
1) VM_ACTION="keep_gpu_disable_onboot" ;;
|
|
2) VM_ACTION="remove_gpu_keep_onboot" ;;
|
|
*)
|
|
msg_warn "$(translate 'Operation cancelled by user')"
|
|
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
|
|
# sibling audio/HDMI codecs (.1, typical for discrete cards).
|
|
#
|
|
# Precise regex: the slot must be followed by ".<function>" and a
|
|
# delimiter. Kept in sync with switch_gpu_mode.sh — a looser
|
|
# substring match would wipe unrelated hostpci entries (e.g. slot
|
|
# "00:02" matching as a substring inside a dGPU BDF 0000:02:00.0).
|
|
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 for the web flow: auto-remove any PCI audio
|
|
# hostpci entries at a slot DIFFERENT from the GPU (typical Intel
|
|
# iGPU case where 00:1f.3 chipset audio was paired with the iGPU
|
|
# at 00:02.0). The helper skips audio devices whose slot already
|
|
# has a display sibling in the same VM (HDMI codec of another
|
|
# still-present dGPU), so those are not touched. The web runner
|
|
# has no good way to render a multi-select checklist, so the
|
|
# eligible ones are auto-removed and reported verbatim in the log.
|
|
if declare -F _vm_list_orphan_audio_hostpci >/dev/null 2>&1; then
|
|
local _orphan_audio _line _o_idx _o_bdf _o_name _removed=""
|
|
local _vd_hex _dd_hex _vd_id
|
|
_orphan_audio=$(_vm_list_orphan_audio_hostpci "$vmid" "${SELECTED_PCI_SLOTS[0]}")
|
|
if [[ -n "$_orphan_audio" ]]; then
|
|
while IFS= read -r _line; do
|
|
[[ -z "$_line" ]] && continue
|
|
_o_idx="${_line%%|*}"
|
|
_line="${_line#*|}"
|
|
_o_bdf="${_line%%|*}"
|
|
_o_name="${_line#*|}"
|
|
if _vm_remove_hostpci_index "$vmid" "$_o_idx" "$LOG_FILE"; then
|
|
_removed+=" • hostpci${_o_idx}: ${_o_bdf} ${_o_name}\n"
|
|
|
|
# Fix B: also surface the audio's vendor:device to the
|
|
# upcoming vfio.conf cleanup if no other VM still uses
|
|
# this BDF. Ensures e.g. 8086:51c8 (Intel chipset audio)
|
|
# is stripped from /etc/modprobe.d/vfio.conf when the
|
|
# iGPU it was paired with leaves VM mode.
|
|
if declare -F _pci_bdf_in_any_vm >/dev/null 2>&1 \
|
|
&& ! _pci_bdf_in_any_vm "$_o_bdf" "${VM_AFFECTED_IDS[@]}"; then
|
|
_vd_hex=$(cat "/sys/bus/pci/devices/${_o_bdf}/vendor" 2>/dev/null | sed 's/^0x//')
|
|
_dd_hex=$(cat "/sys/bus/pci/devices/${_o_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 <<< "$_orphan_audio"
|
|
if [[ -n "$_removed" ]]; then
|
|
msg_ok "$(translate 'Associated audio removed from VM'): ${vmid}" \
|
|
| tee -a "$screen_capture"
|
|
echo -e "$_removed" | tee -a "$screen_capture"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ==========================================================
|
|
# Switch Mode Functions
|
|
# ==========================================================
|
|
_register_iommu_tool() {
|
|
local tools_json="${BASE_DIR:-/usr/local/share/proxmenux}/installed_tools.json"
|
|
command -v jq >/dev/null 2>&1 || return 0
|
|
[[ -f "$tools_json" ]] || echo "{}" > "$tools_json"
|
|
jq '.vfio_iommu=true' "$tools_json" > "$tools_json.tmp" \
|
|
&& mv "$tools_json.tmp" "$tools_json" || true
|
|
}
|
|
|
|
_enable_iommu_cmdline() {
|
|
local cpu_vendor
|
|
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
|
|
|
local iommu_param
|
|
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
|
|
iommu_param="intel_iommu=on"
|
|
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
|
|
iommu_param="amd_iommu=on"
|
|
else
|
|
return 1
|
|
fi
|
|
|
|
local cmdline_file="/etc/kernel/cmdline"
|
|
local grub_file="/etc/default/grub"
|
|
|
|
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
|
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
|
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
|
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
|
proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true
|
|
fi
|
|
elif [[ -f "$grub_file" ]]; then
|
|
if ! grep -q "$iommu_param" "$grub_file"; then
|
|
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
|
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
|
update-grub >>"$LOG_FILE" 2>&1 || true
|
|
fi
|
|
else
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
switch_to_vm_mode() {
|
|
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
|
|
_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
|
|
}
|
|
|
|
# HYBRID: Confirmation prompt
|
|
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="${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?')"
|
|
|
|
if ! hybrid_yesno "$(translate 'Confirm GPU Switch Mode')" "$msg" 18 88; then
|
|
msg_warn "$(translate 'Operation cancelled by user')"
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
# HYBRID: Final summary with reboot prompt
|
|
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}"
|
|
|
|
if hybrid_yesno "$(translate 'Reboot Required')" "$(translate 'A reboot is required to apply the new GPU mode. Do you want to restart now?')" 10 74; then
|
|
msg_warn "$(translate 'Rebooting the system...')"
|
|
reboot
|
|
else
|
|
msg_info2 "$(translate 'Please reboot manually to complete the switch.')"
|
|
hybrid_msgbox "$(translate 'Reboot Required')" "$(translate 'Please reboot the system manually to complete the GPU switch.')" 8 60
|
|
fi
|
|
else
|
|
echo -e "${TAB}${DGN}- $(translate 'No host VFIO/native binding changes were required.')${CL}"
|
|
hybrid_msgbox "$(translate 'Complete')" "$(translate 'GPU switch mode completed. No reboot required.')" 8 60
|
|
fi
|
|
}
|
|
|
|
# ==========================================================
|
|
# Parse Arguments (supports both CLI args and env vars)
|
|
# ==========================================================
|
|
# Send notification when GPU mode switch completes
|
|
# ==========================================================
|
|
_send_gpu_mode_notification() {
|
|
local new_mode="$1"
|
|
local gpu_name="$2"
|
|
local gpu_pci="$3"
|
|
local old_mode="$4"
|
|
local notify_script="/usr/bin/notification_manager.py"
|
|
|
|
[[ ! -f "$notify_script" ]] && return 0
|
|
|
|
local hostname_short
|
|
hostname_short=$(hostname -s)
|
|
|
|
local mode_label details
|
|
if [[ "$new_mode" == "vm" ]]; then
|
|
mode_label="GPU -> VM (VFIO passthrough)"
|
|
details="GPU is now ready for VM passthrough. A host reboot may be required."
|
|
else
|
|
mode_label="GPU -> LXC (native driver)"
|
|
details="GPU is now available for LXC containers with native drivers."
|
|
fi
|
|
|
|
python3 "$notify_script" --action send-raw --severity INFO \
|
|
--title "${hostname_short}: GPU mode changed to ${mode_label}" \
|
|
--message "GPU passthrough mode switched.
|
|
GPU: ${gpu_name} (${gpu_pci})
|
|
Previous: ${old_mode}
|
|
New: ${mode_label}
|
|
${details}" 2>/dev/null || true
|
|
}
|
|
|
|
# ==========================================================
|
|
parse_arguments() {
|
|
# First, check combined parameter (format: "SLOT|MODE")
|
|
# This is the primary method used by ProxMenux Monitor
|
|
if [[ -n "$GPU_SWITCH_PARAMS" ]]; then
|
|
PARAM_GPU_SLOT="${GPU_SWITCH_PARAMS%%|*}"
|
|
PARAM_TARGET_MODE="${GPU_SWITCH_PARAMS##*|}"
|
|
fi
|
|
|
|
# Also check individual environment variables as fallback
|
|
[[ -n "$GPU_SLOT" ]] && PARAM_GPU_SLOT="$GPU_SLOT"
|
|
[[ -n "$TARGET_MODE" ]] && PARAM_TARGET_MODE="$TARGET_MODE"
|
|
|
|
# Then, parse command line arguments (override env vars if provided)
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--gpu-slot=*)
|
|
PARAM_GPU_SLOT="${1#*=}"
|
|
;;
|
|
--target-mode=*)
|
|
PARAM_TARGET_MODE="${1#*=}"
|
|
;;
|
|
*)
|
|
# Ignore unknown arguments
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
# ==========================================================
|
|
# Main Entry Point
|
|
# ==========================================================
|
|
check_stale_vfio_config_switch_mode_direct() {
|
|
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 idx viddid drv
|
|
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
|
viddid="${ALL_GPU_VIDDID[$idx]}"
|
|
drv="${ALL_GPU_DRIVERS[$idx]}"
|
|
[[ -z "$viddid" ]] && continue
|
|
[[ "$drv" == "vfio-pci" ]] && continue
|
|
if echo ",${ids_part}," | grep -q ",${viddid},"; then
|
|
legacy_ids+=("$viddid")
|
|
fi
|
|
done
|
|
|
|
[[ ${#legacy_ids[@]} -eq 0 ]] && return 0
|
|
|
|
msg_info "$(translate 'Removing stale VFIO entries from vfio.conf...')"
|
|
if declare -F _clean_vfio_conf_ids >/dev/null 2>&1 \
|
|
&& _clean_vfio_conf_ids "${legacy_ids[@]}"; then
|
|
update-initramfs -u >/dev/null 2>&1 || true
|
|
HOST_CONFIG_CHANGED=true
|
|
msg_ok "$(translate 'Stale VFIO entries removed and initramfs rebuilt.')" | tee -a "$screen_capture"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
: >"$LOG_FILE"
|
|
: >"$screen_capture"
|
|
|
|
parse_arguments "$@"
|
|
|
|
# Validate required parameters
|
|
if [[ -z "$PARAM_GPU_SLOT" ]]; then
|
|
msg_error "$(translate 'Missing required parameter'): --gpu-slot"
|
|
echo "Usage: $0 --gpu-slot=0000:01:00.0 --target-mode=vm|lxc"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$PARAM_TARGET_MODE" ]] || [[ ! "$PARAM_TARGET_MODE" =~ ^(vm|lxc)$ ]]; then
|
|
msg_error "$(translate 'Missing or invalid parameter'): --target-mode (must be 'vm' or 'lxc')"
|
|
echo "Usage: $0 --gpu-slot=0000:01:00.0 --target-mode=vm|lxc"
|
|
exit 1
|
|
fi
|
|
|
|
TARGET_MODE="$PARAM_TARGET_MODE"
|
|
|
|
# Detect all GPUs
|
|
detect_host_gpus
|
|
|
|
if [[ "$GPU_COUNT" -eq 0 ]]; then
|
|
msg_error "$(translate 'No GPUs detected on this host.')"
|
|
exit 1
|
|
fi
|
|
|
|
# Find the specific GPU by slot
|
|
if ! find_gpu_by_slot "$PARAM_GPU_SLOT"; then
|
|
exit 1
|
|
fi
|
|
|
|
# SR-IOV guard: refuse to toggle the driver on a VF or on a PF with
|
|
# active VFs. Manual handling via Proxmox web UI is required.
|
|
if ! check_sriov_and_block_if_needed; then
|
|
exit 1
|
|
fi
|
|
|
|
# Validate if GPU is blocked for VM mode (certain Intel GPUs)
|
|
if ! validate_vm_mode_blocked_ids; then
|
|
exit 1
|
|
fi
|
|
|
|
|
|
# Confirm the operation
|
|
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
|
|
|
|
# Get GPU info for notification
|
|
local gpu_idx="${SELECTED_GPU_IDX[0]}"
|
|
local gpu_name="${ALL_GPU_NAMES[$gpu_idx]}"
|
|
local gpu_pci="${ALL_GPU_PCIS[$gpu_idx]}"
|
|
|
|
if [[ "$TARGET_MODE" == "lxc" ]]; then
|
|
check_stale_vfio_config_switch_mode_direct
|
|
fi
|
|
|
|
# Execute the switch
|
|
if [[ "$TARGET_MODE" == "vm" ]]; then
|
|
switch_to_vm_mode
|
|
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
|
|
_send_gpu_mode_notification "vm" "$gpu_name" "$gpu_pci" "$old_mode_label"
|
|
else
|
|
switch_to_lxc_mode
|
|
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
|
|
_send_gpu_mode_notification "lxc" "$gpu_name" "$gpu_pci" "$old_mode_label"
|
|
fi
|
|
|
|
final_summary
|
|
rm -f "$screen_capture"
|
|
}
|
|
|
|
main "$@"
|