add_controller_nvme_vm.sh

This commit is contained in:
MacRimi
2026-04-06 13:39:07 +02:00
parent 5be4bd2ae9
commit 10ce9dbcde
14 changed files with 2390 additions and 237 deletions

View File

@@ -28,6 +28,11 @@ NVIDIA_VID_DID=""
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
if [[ -f "$LOCAL_SCRIPTS/global/gpu_hook_guard_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS/global/gpu_hook_guard_helpers.sh"
elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/global/gpu_hook_guard_helpers.sh" ]]; then
source "$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/global/gpu_hook_guard_helpers.sh"
fi
load_language
initialize_cache
@@ -827,7 +832,7 @@ _remove_vfio_modules() {
}
# Detects if any selected GPU is currently in GPU → VM mode (VFIO binding).
# If so, guides the user through the switch to GPU → LXC mode and exits.
# If so, delegates switch handling to switch_gpu_mode.sh and exits.
check_vfio_switch_mode() {
local vfio_conf="/etc/modprobe.d/vfio.conf"
[[ ! -f "$vfio_conf" ]] && return 0
@@ -857,155 +862,39 @@ check_vfio_switch_mode() {
[[ ${#vfio_types[@]} -eq 0 ]] && return 0
# ── One or more GPUs are in GPU → VM mode ─────────────────────
local msg
msg="\n$(translate 'The following GPU(s) are currently in GPU VM passthrough mode (VFIO)'):\n\n"
msg="\n$(translate 'The following selected GPU(s) are currently in GPU -> VM mode (vfio-pci):')\n\n"
for i in "${!vfio_types[@]}"; do
msg+="${vfio_names[$i]} (${vfio_pcis[$i]})\n"
done
msg+="\n$(translate 'VFIO gives the VM exclusive ownership of the GPU. The native driver is blacklisted.')\n"
msg+="$(translate 'To use this GPU in an LXC container the VFIO binding must be removed')\n"
msg+="$(translate 'and the native driver reloaded. This requires a system reboot.')\n"
# Check for VM configs that still reference this GPU
local -a affected_vms=() affected_vm_names=()
for pci in "${vfio_pcis[@]}"; do
local pci_slot="${pci#0000:}"
pci_slot="${pci_slot%.*}"
for conf in /etc/pve/qemu-server/*.conf; do
[[ -f "$conf" ]] || continue
if grep -qE "hostpci[0-9]+:.*${pci_slot}" "$conf"; then
local vmid vm_name
vmid=$(basename "$conf" .conf)
vm_name=$(grep "^name:" "$conf" 2>/dev/null | awk '{print $2}')
local dup=false
for v in "${affected_vms[@]}"; do [[ "$v" == "$vmid" ]] && dup=true && break; done
$dup || { affected_vms+=("$vmid"); affected_vm_names+=("${vm_name:-VM-${vmid}}"); }
fi
done
done
if [[ ${#affected_vms[@]} -gt 0 ]]; then
msg+="\n\Z1\Zb$(translate 'Warning: This GPU is assigned to the following VM(s)'):\Zn\n\n"
for i in "${!affected_vms[@]}"; do
msg+=" • VM ${affected_vms[$i]} (${affected_vm_names[$i]})\n"
done
msg+="\n$(translate 'Starting those VMs after the switch will cause errors on the system and in the VM.')\n"
msg+="$(translate 'You will be asked to stop them or remove the GPU from their config.')\n"
fi
msg+="\n$(translate 'Do you want to switch to GPU → LXC mode?')"
msg+="\n$(translate 'To continue with Add GPU to LXC, first switch the host to GPU -> LXC mode and reboot.')\n"
msg+="$(translate 'Do you want to open Switch GPU Mode now?')"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'GPU VM Mode Detected')" \
--yesno "$msg" 26 80
--title "$(translate 'GPU -> VM Mode Detected')" \
--yesno "$msg" 18 84
[[ $? -ne 0 ]] && exit 0
# ── User confirmed switch — enter processing phase ─────────
LXC_SWITCH_MODE=true
# Handle VM conflicts: stop VM or remove GPU from config
if [[ ${#affected_vms[@]} -gt 0 ]]; then
local vm_msg
vm_msg="\n$(translate 'The following VM(s) have this GPU assigned'):\n\n"
for i in "${!affected_vms[@]}"; do
vm_msg+=" • VM ${affected_vms[$i]} (${affected_vm_names[$i]})\n"
done
vm_msg+="\n$(translate 'YES — Stop the VM(s) now (GPU entry stays in config, reusable for passthrough later)')\n"
vm_msg+="$(translate 'NO — Remove GPU from VM config (VM can start normally without GPU)')"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'VM Conflict: Choose Action')" \
--yesno "$vm_msg" 18 78
local vm_action=$?
show_proxmenux_logo
msg_title "$(_get_lxc_run_title)"
for i in "${!affected_vms[@]}"; do
local vmid="${affected_vms[$i]}"
if [[ $vm_action -eq 0 ]]; then
if qm status "$vmid" 2>/dev/null | grep -q "running"; then
msg_info "$(translate 'Stopping VM') ${vmid}..."
qm stop "$vmid" >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'VM') ${vmid} $(translate 'stopped.')"
else
msg_ok "$(translate 'VM') ${vmid} $(translate 'is already stopped.')"
fi
else
local src_conf="/etc/pve/qemu-server/${vmid}.conf"
if [[ -f "$src_conf" ]]; then
for pci in "${vfio_pcis[@]}"; do
local pci_slot="${pci#0000:}"; pci_slot="${pci_slot%.*}"
sed -i "/^hostpci[0-9]\+:.*${pci_slot}/d" "$src_conf"
done
msg_ok "$(translate 'GPU removed from VM') ${vmid} $(translate 'configuration.')"
fi
fi
done
local switch_script="$LOCAL_SCRIPTS/gpu_tpu/switch_gpu_mode.sh"
local local_switch_script
local_switch_script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/switch_gpu_mode.sh"
if [[ ! -f "$switch_script" && -f "$local_switch_script" ]]; then
switch_script="$local_switch_script"
fi
# ── Remove VFIO config for each affected GPU ───────────────
msg_info "$(translate 'Removing VFIO configuration...')"
local -a all_ids_to_remove=()
for i in "${!vfio_types[@]}"; do
local gpu_type="${vfio_types[$i]}"
local pci="${vfio_pcis[$i]}"
# Collect all IOMMU group IDs (GPU + audio function, etc.)
local -a group_ids=()
mapfile -t group_ids < <(_get_iommu_group_ids "$pci")
if [[ ${#group_ids[@]} -gt 0 ]]; then
all_ids_to_remove+=("${group_ids[@]}")
else
# IOMMU not active: fall back to the GPU's own vendor:device ID
case "$gpu_type" in
intel) all_ids_to_remove+=("$INTEL_VID_DID") ;;
amd) all_ids_to_remove+=("$AMD_VID_DID") ;;
nvidia) all_ids_to_remove+=("$NVIDIA_VID_DID") ;;
esac
fi
_remove_gpu_blacklist "$gpu_type"
msg_ok "$(translate 'Driver blacklist removed for') ${gpu_type}"
if [[ "$gpu_type" == "amd" ]]; then
_remove_amd_softdep
msg_ok "$(translate 'AMD softdep entries removed')"
fi
done
local remaining_count
remaining_count=$(_remove_vfio_ids "${all_ids_to_remove[@]}")
msg_ok "$(translate 'VFIO device IDs removed from /etc/modprobe.d/vfio.conf')"
if [[ "$remaining_count" -eq 0 ]]; then
_remove_vfio_modules
msg_ok "$(translate 'VFIO modules removed from /etc/modules')"
else
msg_ok "$(translate 'VFIO modules kept (other GPUs remain in VFIO mode)')"
if [[ ! -f "$switch_script" ]]; then
dialog --backtitle "ProxMenux" \
--title "$(translate 'Switch Script Not Found')" \
--msgbox "\n$(translate 'switch_gpu_mode.sh was not found.')\n\n$(translate 'Expected path:')\n${LOCAL_SCRIPTS}/gpu_tpu/switch_gpu_mode.sh" 10 84
exit 0
fi
msg_info "$(translate 'Updating initramfs (this may take a minute)...')"
update-initramfs -u -k all >>"$LOG_FILE" 2>&1
msg_ok "$(translate 'initramfs updated')"
bash "$switch_script"
echo
msg_success "$(translate 'GPU → LXC switch complete. A reboot is required to load the native GPU driver.')"
echo
whiptail --title "$(translate 'Reboot Required')" \
--yesno "$(translate 'A reboot is required to complete the switch to GPU → LXC 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 before adding the GPU to an LXC container.')"
msg_success "$(translate 'Press Enter to continue...')"
read -r
fi
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'Next Step Required')" \
--msgbox "\n$(translate 'After switching mode, reboot the host if requested.')\n\n$(translate 'Then run this option again:')\n\n Add GPU to LXC\n\n$(translate 'This guarantees that device nodes are available before applying LXC GPU config.')" \
12 84
exit 0
}
@@ -1034,6 +923,11 @@ main() {
msg_title "$(_get_lxc_run_title)"
configure_passthrough "$CONTAINER_ID"
if declare -F attach_proxmenux_gpu_guard_to_lxc >/dev/null 2>&1; then
ensure_proxmenux_gpu_guard_hookscript
attach_proxmenux_gpu_guard_to_lxc "$CONTAINER_ID"
sync_proxmenux_gpu_guard_hooks
fi
if start_container_and_wait "$CONTAINER_ID"; then
install_drivers "$CONTAINER_ID"

View File

@@ -46,6 +46,11 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh"
fi
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh"
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh"
fi
load_language
initialize_cache
@@ -89,6 +94,12 @@ PRESELECT_VMID=""
WIZARD_CALL=false
GPU_WIZARD_RESULT_FILE=""
declare -a LXC_AFFECTED_CTIDS=()
declare -a LXC_AFFECTED_NAMES=()
declare -a LXC_AFFECTED_RUNNING=() # 1 or 0
declare -a LXC_AFFECTED_ONBOOT=() # 1 or 0
LXC_SWITCH_ACTION="" # keep_gpu_disable_onboot | remove_gpu_keep_onboot
# ==========================================================
# Helpers
@@ -133,6 +144,42 @@ _vm_onboot_enabled() {
qm config "$vmid" 2>/dev/null | grep -qE "^onboot:\s*1"
}
_ct_is_running() {
local ctid="$1"
pct status "$ctid" 2>/dev/null | grep -q "status: running"
}
_ct_onboot_enabled() {
local ctid="$1"
pct config "$ctid" 2>/dev/null | grep -qE "^onboot:\s*1"
}
_lxc_conf_uses_selected_gpu() {
local conf="$1"
case "$SELECTED_GPU" in
nvidia)
grep -qE "dev[0-9]+:.*(/dev/nvidia|/dev/nvidia-caps)" "$conf" 2>/dev/null
;;
amd)
grep -qE "dev[0-9]+:.*(/dev/dri|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" "$conf" 2>/dev/null
;;
intel)
grep -qE "dev[0-9]+:.*(/dev/dri)|lxc\.mount\.entry:.*dev/dri" "$conf" 2>/dev/null
;;
*)
grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)|lxc\.mount\.entry:.*dev/dri" "$conf" 2>/dev/null
;;
esac
}
_lxc_switch_action_label() {
case "$LXC_SWITCH_ACTION" in
keep_gpu_disable_onboot) echo "$(translate 'Keep GPU in LXC config + disable Start on boot')" ;;
remove_gpu_keep_onboot) echo "$(translate 'Remove GPU from LXC config + keep Start on boot unchanged')" ;;
*) echo "$(translate 'No specific LXC action selected')" ;;
esac
}
_set_wizard_result() {
local result="$1"
[[ -z "${GPU_WIZARD_RESULT_FILE:-}" ]] && return 0
@@ -562,6 +609,7 @@ warn_single_gpu() {
msg+=" • Proxmox Web UI (https)\n"
msg+=" • Serial console\n\n"
msg+="$(translate 'The VM guest will have exclusive access to the GPU.')\n\n"
msg+="\Z3$(translate 'Important: some GPUs may still fail in passthrough and can affect host stability or overall performance depending on hardware/firmware quality.')\Zn\n\n"
msg+="$(translate 'Make sure you have SSH or Web UI access before rebooting.')\n\n"
msg+="$(translate 'Do you want to continue?')"
@@ -645,11 +693,31 @@ _detect_intel_gpu_subtype() {
check_intel_vm_compatibility() {
local pci_full="$SELECTED_GPU_PCI"
local gpu_subtype reset_method power_state
local gpu_subtype reset_method power_state vendor device viddid
gpu_subtype=$(_detect_intel_gpu_subtype)
reset_method=$(_check_pci_reset_method "$pci_full")
power_state=$(cat "/sys/bus/pci/devices/${pci_full}/power_state" 2>/dev/null | tr -d '[:space:]')
vendor=$(cat "/sys/bus/pci/devices/${pci_full}/vendor" 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]')
device=$(cat "/sys/bus/pci/devices/${pci_full}/device" 2>/dev/null | sed 's/0x//' | tr '[:upper:]' '[:lower:]')
viddid="${vendor}:${device}"
# ── BLOCKER: Known unsupported Intel Apollo Lake iGPU IDs ────────────
if [[ "$viddid" == "8086:5a84" || "$viddid" == "8086:5a85" ]]; then
local msg
msg="\n\Zb\Z1$(translate 'GPU Not Compatible with VM Passthrough')\Zn\n\n"
msg+="$(translate 'The selected Intel GPU belongs to Apollo Lake generation and is blocked by policy for VM passthrough due to host instability risk.')\n\n"
msg+=" ${SELECTED_GPU_NAME}\n"
msg+=" ${SELECTED_GPU_PCI}\n"
msg+=" \ZbID: ${viddid}\Zn\n\n"
msg+="$(translate 'This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.')\n\n"
msg+="$(translate 'Recommended: use GPU with LXC workloads instead of VM passthrough on this hardware.')"
dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'Blocked GPU ID')" \
--msgbox "$msg" 20 84
exit 0
fi
# ── BLOCKER: Intel GPU in D3cold ──────────────────────────────────────
if [[ "$power_state" == "D3cold" ]]; then
@@ -1016,35 +1084,78 @@ check_switch_mode() {
pci_slot="${pci_slot%.*}" # 01:00
# ── LXC conflict check ────────────────────────────────
LXC_AFFECTED_CTIDS=()
LXC_AFFECTED_NAMES=()
LXC_AFFECTED_RUNNING=()
LXC_AFFECTED_ONBOOT=()
LXC_SWITCH_ACTION=""
local lxc_affected=()
local running_count=0
local onboot_count=0
for conf in /etc/pve/lxc/*.conf; do
[[ -f "$conf" ]] || continue
if grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)" "$conf"; then
local ctid ct_name
ctid=$(basename "$conf" .conf)
ct_name=$(pct config "$ctid" 2>/dev/null | grep "^hostname:" | awk '{print $2}')
lxc_affected+=("CT ${ctid} (${ct_name:-CT-${ctid}})")
fi
_lxc_conf_uses_selected_gpu "$conf" || continue
local ctid ct_name running_flag onboot_flag
ctid=$(basename "$conf" .conf)
ct_name=$(pct config "$ctid" 2>/dev/null | awk '/^hostname:/ {print $2}')
[[ -z "$ct_name" ]] && ct_name="CT-${ctid}"
running_flag=0
onboot_flag=0
_ct_is_running "$ctid" && running_flag=1
_ct_onboot_enabled "$ctid" && onboot_flag=1
LXC_AFFECTED_CTIDS+=("$ctid")
LXC_AFFECTED_NAMES+=("$ct_name")
LXC_AFFECTED_RUNNING+=("$running_flag")
LXC_AFFECTED_ONBOOT+=("$onboot_flag")
lxc_affected+=("CT ${ctid} (${ct_name})")
[[ "$running_flag" == "1" ]] && running_count=$((running_count + 1))
[[ "$onboot_flag" == "1" ]] && onboot_count=$((onboot_count + 1))
done
if [[ ${#lxc_affected[@]} -gt 0 ]]; then
SWITCH_FROM_LXC=true
SWITCH_LXC_LIST=$(IFS=', '; echo "${lxc_affected[*]}")
local msg
msg="\n$(translate 'The selected GPU is currently shared with the following LXC containers via device passthrough:')\n\n"
for ct in "${lxc_affected[@]}"; do
msg+="${ct}\n"
local msg action_choice
msg="\n$(translate 'The selected GPU is currently used by the following LXC container(s):')\n\n"
local i
for i in "${!LXC_AFFECTED_CTIDS[@]}"; do
local status_txt onboot_txt
status_txt="$(translate 'stopped')"
onboot_txt="onboot=0"
[[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]] && status_txt="$(translate 'running')"
[[ "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]] && onboot_txt="onboot=1"
msg+=" • CT ${LXC_AFFECTED_CTIDS[$i]} (${LXC_AFFECTED_NAMES[$i]}) [${status_txt}, ${onboot_txt}]\n"
done
msg+="\n$(translate 'VM passthrough requires exclusive VFIO binding of the GPU.')\n"
msg+="$(translate 'GPU device access will be removed from those LXC containers.')\n\n"
msg+="\Z3$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn\n\n"
msg+="$(translate 'Do you want to continue?')"
msg+="$(translate 'Choose how to handle affected LXC containers before switching to VM mode.')\n\n"
[[ "$running_count" -gt 0 ]] && \
msg+="\Z3$(translate 'Running containers detected'): ${running_count}\Zn\n"
[[ "$onboot_count" -gt 0 ]] && \
msg+="\Z1\Zb$(translate 'Start on boot enabled (onboot=1)'): ${onboot_count}\Zn\n"
msg+="\n\Z3$(translate 'After this LXC → VM switch, reboot the host so the new binding state is applied cleanly.')\Zn"
dialog --backtitle "ProxMenux" \
action_choice=$(dialog --backtitle "ProxMenux" --colors \
--title "$(translate 'GPU Used in LXC Containers')" \
--yesno "$msg" 18 76
[[ $? -ne 0 ]] && exit 0
--default-item "2" \
--menu "$msg" 25 96 8 \
"1" "$(translate 'Keep GPU in LXC config (disable Start on boot)')" \
"2" "$(translate 'Remove GPU from LXC config (keep Start on boot)')" \
2>&1 >/dev/tty) || exit 0
case "$action_choice" in
1) LXC_SWITCH_ACTION="keep_gpu_disable_onboot" ;;
2) LXC_SWITCH_ACTION="remove_gpu_keep_onboot" ;;
*) exit 0 ;;
esac
else
SWITCH_FROM_LXC=false
fi
# ── VM conflict check (different VM than selected) ────
@@ -1155,8 +1266,13 @@ confirm_summary() {
msg+="$(translate 'Additional GPU audio function will be added'): ${EXTRA_AUDIO_DEVICES[*]}\n"
[[ "$SELECTED_GPU" == "nvidia" ]] && \
msg+="$(translate 'NVIDIA KVM hiding (cpu hidden=1)')\n"
[[ "$SWITCH_FROM_LXC" == "true" ]] && \
msg+="\n \Z3• $(translate 'GPU will be removed from LXC containers'): ${SWITCH_LXC_LIST}\Zn\n"
if [[ "$SWITCH_FROM_LXC" == "true" ]]; then
msg+="\n \Z3• $(translate 'Affected LXC containers'): ${SWITCH_LXC_LIST}\Zn\n"
msg+=" \Z3• $(translate 'Selected LXC action'): $(_lxc_switch_action_label)\Zn\n"
if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" ]]; then
msg+=" \Z3• $(translate 'To use the GPU again in LXC, run Add GPU to LXC from GPUs and Coral-TPU Menu')\Zn\n"
fi
fi
[[ "$SWITCH_FROM_VM" == "true" ]] && \
msg+="\n \Z3• $(translate 'GPU will be removed from VM') ${SWITCH_VM_SRC}\Zn\n"
msg+="\n$(translate 'Do you want to proceed?')"
@@ -1350,24 +1466,76 @@ dump_amd_rom() {
}
# ── Remove GPU from LXC configs (switch mode) ────────────
cleanup_lxc_configs() {
[[ "$SWITCH_FROM_LXC" != "true" ]] && return 0
msg_info "$(translate 'Removing GPU device access from LXC containers...')"
for conf in /etc/pve/lxc/*.conf; do
[[ -f "$conf" ]] || continue
if grep -qE "dev[0-9]+:.*(/dev/dri|/dev/nvidia|/dev/kfd)" "$conf"; then
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf"
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' "$conf"
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' "$conf"
_remove_selected_gpu_from_lxc_conf() {
local conf="$1"
case "$SELECTED_GPU" in
nvidia)
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' "$conf"
;;
amd)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf"
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' "$conf"
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf"
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf"
local ctid
ctid=$(basename "$conf" .conf)
msg_ok "$(translate 'GPU removed from LXC') ${ctid}" | tee -a "$screen_capture"
;;
intel)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf"
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf"
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf"
;;
*)
sed -i '/dev[0-9]\+:.*\/dev\/dri/d' "$conf"
sed -i '/dev[0-9]\+:.*\/dev\/nvidia/d' "$conf"
sed -i '/dev[0-9]\+:.*\/dev\/kfd/d' "$conf"
sed -i '/lxc\.mount\.entry:.*dev\/dri/d' "$conf"
sed -i '/lxc\.cgroup2\.devices\.allow:.*226/d' "$conf"
;;
esac
}
# ── Apply selected action for affected LXC (switch mode) ─
cleanup_lxc_configs() {
[[ "$SWITCH_FROM_LXC" != "true" ]] && return 0
[[ ${#LXC_AFFECTED_CTIDS[@]} -eq 0 ]] && return 0
msg_info "$(translate 'Applying selected LXC switch action...')"
local i
for i in "${!LXC_AFFECTED_CTIDS[@]}"; do
local ctid conf
ctid="${LXC_AFFECTED_CTIDS[$i]}"
conf="/etc/pve/lxc/${ctid}.conf"
if [[ "${LXC_AFFECTED_RUNNING[$i]}" == "1" ]]; then
msg_info "$(translate 'Stopping LXC') ${ctid}..."
if pct stop "$ctid" >>"$LOG_FILE" 2>&1; then
msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture"
else
msg_warn "$(translate 'Could not stop LXC') ${ctid}" | tee -a "$screen_capture"
fi
else
msg_ok "$(translate 'LXC already stopped') ${ctid}" | tee -a "$screen_capture"
fi
if [[ "$LXC_SWITCH_ACTION" == "keep_gpu_disable_onboot" ]]; then
if [[ "${LXC_AFFECTED_ONBOOT[$i]}" == "1" ]]; then
if pct set "$ctid" -onboot 0 >>"$LOG_FILE" 2>&1; then
msg_warn "$(translate 'Start on boot disabled for LXC') ${ctid}" | tee -a "$screen_capture"
else
msg_error "$(translate 'Failed to disable Start on boot for LXC') ${ctid}" | tee -a "$screen_capture"
fi
fi
fi
if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" && -f "$conf" ]]; then
_remove_selected_gpu_from_lxc_conf "$conf"
msg_ok "$(translate 'GPU access removed from LXC') ${ctid}" | tee -a "$screen_capture"
fi
done
if [[ "$LXC_SWITCH_ACTION" == "remove_gpu_keep_onboot" ]]; then
msg_warn "$(translate 'If needed again, re-add GPU to LXC from GPUs and Coral-TPU Menu → Add GPU to LXC.')" | tee -a "$screen_capture"
fi
}
@@ -1562,6 +1730,11 @@ main() {
cleanup_vm_config
ensure_vm_display_std
configure_vm
if declare -F attach_proxmenux_gpu_guard_to_vm >/dev/null 2>&1; then
ensure_proxmenux_gpu_guard_hookscript
attach_proxmenux_gpu_guard_to_vm "$SELECTED_VMID"
sync_proxmenux_gpu_guard_hooks
fi
[[ "$HOST_CONFIG_CHANGED" == "true" ]] && update_initramfs_host
# ── Phase 3: summary ─────────────────────────────────

View File

@@ -0,0 +1,945 @@
#!/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
# ==========================================================
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
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS_LOCAL/global/gpu_hook_guard_helpers.sh"
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh" ]]; then
source "$LOCAL_SCRIPTS_DEFAULT/global/gpu_hook_guard_helpers.sh"
fi
load_language
initialize_cache
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|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 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 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
}
_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_short=$(echo "$line" | awk '{print $1}')
pci_full="0000:${pci_short}"
name=$(echo "$line" | sed 's/^[^:]*[^:]: //' | sed 's/ \[.*//' | cut -c1-62)
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"
}
select_target_mode() {
CURRENT_MODE=$(_selected_gpu_current_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"
}
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+="\Z3$(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}..."
pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true
msg_ok "$(translate 'LXC stopped') ${ctid}" | tee -a "$screen_capture"
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+="\Z3$(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
local slot
for slot in "${SELECTED_PCI_SLOTS[@]}"; do
sed -i "/^hostpci[0-9]\+:.*${slot}/d" "$conf"
done
msg_ok "$(translate 'GPU removed from VM config') ${vmid}" | tee -a "$screen_capture"
fi
done
}
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...')"
_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 "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
msg_ok "$(translate 'initramfs updated')" | tee -a "$screen_capture"
fi
if declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
sync_proxmenux_gpu_guard_hooks
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
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
msg_ok "$(translate 'initramfs updated')" | tee -a "$screen_capture"
fi
if declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
sync_proxmenux_gpu_guard_hooks
fi
}
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
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
if [[ "$TARGET_MODE" == "vm" ]]; then
switch_to_vm_mode
msg_success "$(translate 'GPU switch complete: VM mode prepared.')"
else
switch_to_lxc_mode
msg_success "$(translate 'GPU switch complete: LXC mode prepared.')"
fi
final_summary
rm -f "$screen_capture"
}
main "$@"