#!/bin/bash # ========================================================== # ProxMenux - Add Controller or NVMe PCIe to VM # ========================================================== # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : GPL-3.0 # Version : 1.0 # Last Updated: 06/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" TOOLS_JSON="$BASE_DIR/installed_tools.json" 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_add_controller_nvme_vm.log" screen_capture="/tmp/proxmenux_add_controller_nvme_vm_screen_$$.txt" if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" fi if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" ]]; then source "$LOCAL_SCRIPTS_LOCAL/global/vm_storage_helpers.sh" elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" ]]; then source "$LOCAL_SCRIPTS_DEFAULT/global/vm_storage_helpers.sh" 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 SELECTED_VMID="" SELECTED_VM_NAME="" declare -a SELECTED_CONTROLLER_PCIS=() IOMMU_PENDING_REBOOT=0 IOMMU_ALREADY_ACTIVE=0 NEED_HOOK_SYNC=false WIZARD_CONFLICT_POLICY="" WIZARD_CONFLICT_SCOPE="" set_title() { show_proxmenux_logo msg_title "$(translate "Add Controller or NVMe PCIe to VM")" } ensure_tools_json() { [[ -f "$TOOLS_JSON" ]] || echo "{}" > "$TOOLS_JSON" } register_tool() { local tool="$1" local state="$2" command -v jq >/dev/null 2>&1 || return 0 ensure_tools_json jq --arg t "$tool" --argjson v "$state" \ '.[$t]=$v' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" \ && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON" } register_vfio_iommu_tool() { register_tool "vfio_iommu" true || true } enable_iommu_cmdline() { local silent="${1:-}" local cpu_vendor iommu_param cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}') if [[ "$cpu_vendor" == "GenuineIntel" ]]; then iommu_param="intel_iommu=on" [[ "$silent" != "silent" ]] && msg_info "$(translate "Intel CPU detected")" elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then iommu_param="amd_iommu=on" [[ "$silent" != "silent" ]] && msg_info "$(translate "AMD CPU detected")" else msg_error "$(translate "Unknown CPU vendor. Cannot determine IOMMU parameter.")" return 1 fi local cmdline_file="/etc/kernel/cmdline" local grub_file="/etc/default/grub" if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then if ! grep -q "$iommu_param" "$cmdline_file" || ! grep -q "iommu=pt" "$cmdline_file"; then cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)" sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file" proxmox-boot-tool refresh >/dev/null 2>&1 || true [[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")" else [[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")" fi elif [[ -f "$grub_file" ]]; then if ! grep -q "$iommu_param" "$grub_file" || ! grep -q "iommu=pt" "$grub_file"; then cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)" sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file" update-grub >/dev/null 2>&1 || true [[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to GRUB")" else [[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in GRUB")" fi else msg_error "$(translate "Neither /etc/kernel/cmdline nor /etc/default/grub found.")" return 1 fi } check_iommu_or_offer_enable() { if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then register_vfio_iommu_tool return 0 fi if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \ grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then IOMMU_PENDING_REBOOT=1 register_vfio_iommu_tool return 0 fi if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then IOMMU_ALREADY_ACTIVE=1 register_vfio_iommu_tool return 0 fi if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \ [[ -d /sys/kernel/iommu_groups ]] && \ [[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then IOMMU_ALREADY_ACTIVE=1 register_vfio_iommu_tool return 0 fi local msg msg="\n$(translate "IOMMU is not active on this system.")\n\n" msg+="$(translate "Controller/NVMe passthrough to VMs requires IOMMU to be enabled in the kernel.")\n\n" msg+="$(translate "Do you want to enable IOMMU now?")\n\n" msg+="$(translate "Note: A system reboot will be required after enabling IOMMU.")\n" msg+="$(translate "Configuration can continue now and will be effective after reboot.")" dialog --backtitle "ProxMenux" \ --title "$(translate "IOMMU Required")" \ --yesno "$msg" 15 74 local response=$? [[ $response -ne 0 ]] && return 1 set_title msg_title "$(translate "Enabling IOMMU")" if ! enable_iommu_cmdline; then echo msg_error "$(translate "Failed to configure IOMMU automatically.")" msg_success "$(translate "Press Enter to continue...")" read -r return 1 fi register_vfio_iommu_tool IOMMU_PENDING_REBOOT=1 return 0 } select_target_vm() { local -a vm_menu=() local line vmid vmname vmstatus vm_machine status_label local max_name_len=0 padded_name while IFS= read -r line; do vmid=$(awk '{print $1}' <<< "$line") vmname=$(awk '{print $2}' <<< "$line") [[ -z "$vmid" || "$vmid" == "VMID" ]] && continue [[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue [[ ${#vmname} -gt $max_name_len ]] && max_name_len=${#vmname} done < <(qm list 2>/dev/null) while IFS= read -r line; do vmid=$(awk '{print $1}' <<< "$line") vmname=$(awk '{print $2}' <<< "$line") vmstatus=$(awk '{print $3}' <<< "$line") [[ -z "$vmid" || "$vmid" == "VMID" ]] && continue [[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue vm_machine=$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/ {print $2}') [[ -z "$vm_machine" ]] && vm_machine="unknown" status_label="${vmstatus}, ${vm_machine}" printf -v padded_name "%-${max_name_len}s" "$vmname" vm_menu+=("$vmid" "${padded_name} [${status_label}]") done < <(qm list 2>/dev/null) if [[ ${#vm_menu[@]} -eq 0 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate "Add Controller or NVMe PCIe to VM")" \ --msgbox "\n$(translate "No VMs available on this host.")" 8 64 return 1 fi SELECTED_VMID=$(dialog --backtitle "ProxMenux" \ --title "$(translate "Select VM")" \ --menu "\n$(translate "Select the target VM for PCI passthrough:")" 20 82 12 \ "${vm_menu[@]}" \ 2>&1 >/dev/tty) || return 1 SELECTED_VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | awk '/^name:/ {print $2}') [[ -z "$SELECTED_VM_NAME" ]] && SELECTED_VM_NAME="VM-${SELECTED_VMID}" return 0 } validate_vm_requirements() { local status status=$(qm status "$SELECTED_VMID" 2>/dev/null | awk '{print $2}') if [[ "$status" == "running" ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate "VM Must Be Stopped")" \ --msgbox "\n$(translate "The selected VM is running.")\n\n$(translate "Stop it first and run this option again.")" 10 72 return 1 fi if ! _vm_is_q35 "$SELECTED_VMID"; then dialog --backtitle "ProxMenux" --colors \ --title "$(translate "Incompatible Machine Type")" \ --msgbox "\n\Zb\Z1$(translate "Controller/NVMe passthrough requires machine type q35.")\Zn\n\n$(translate "Selected VM"): ${SELECTED_VM_NAME} (${SELECTED_VMID})\n\n$(translate "Edit the VM machine type to q35 and try again.")" 12 80 return 1 fi check_iommu_or_offer_enable || return 1 return 0 } select_controller_nvme() { # Show progress during potentially slow PCIe + disk detection set_title msg_info "$(translate "Analyzing system for available PCIe storage devices...")" _refresh_host_storage_cache local -a menu_items=() local blocked_report="" local pci_path pci_full class_hex name controller_desc disk state slot_base local -a controller_disks=() local safe_count=0 blocked_count=0 hidden_target_count=0 while IFS= read -r pci_path; do pci_full=$(basename "$pci_path") class_hex=$(cat "$pci_path/class" 2>/dev/null | sed 's/^0x//') [[ -z "$class_hex" ]] && continue [[ "${class_hex:0:2}" != "01" ]] && continue slot_base=$(_pci_slot_base "$pci_full") # Already attached to target VM: hide from selection. if _vm_has_pci_slot "$SELECTED_VMID" "$slot_base"; then hidden_target_count=$((hidden_target_count + 1)) continue fi name=$(lspci -nn -s "${pci_full#0000:}" 2>/dev/null | sed 's/^[^ ]* //') [[ -z "$name" ]] && name="$(translate "Unknown storage controller")" controller_disks=() while IFS= read -r disk; do [[ -z "$disk" ]] && continue _array_contains "$disk" "${controller_disks[@]}" || controller_disks+=("$disk") done < <(_controller_block_devices "$pci_full") # blocked_reasons: system disk OR disk in RUNNING guest → hide controller # warn_reasons: disk in STOPPED guest only → show with ⚠ but allow selection local -a blocked_reasons=() local -a warn_reasons=() for disk in "${controller_disks[@]}"; do if _disk_is_host_system_used "$disk"; then blocked_reasons+=("${disk} (${DISK_USAGE_REASON})") elif _disk_used_in_guest_configs "$disk"; then if _disk_used_in_running_guest "$disk"; then blocked_reasons+=("${disk} ($(translate "In use by running VM/LXC — stop it first"))") else warn_reasons+=("$disk") fi fi done if [[ ${#blocked_reasons[@]} -gt 0 ]]; then blocked_count=$((blocked_count + 1)) blocked_report+=" • ${pci_full} — $(_shorten_text "$name" 56)\n" continue fi local short_name display_name display_name=$(_pci_storage_display_name "$pci_full") short_name=$(_shorten_text "$display_name" 56) local assigned_suffix="" if [[ -n "$(_pci_assigned_vm_ids "$pci_full" "$SELECTED_VMID" 2>/dev/null | head -1)" ]]; then assigned_suffix=" | $(translate "Assigned to VM")" fi # Warn if some disks are referenced in stopped VM/CT configs local warn_suffix="" if [[ ${#warn_reasons[@]} -gt 0 ]]; then warn_suffix=" ⚠" fi controller_desc="${short_name}${assigned_suffix}${warn_suffix}" state="off" menu_items+=("$pci_full" "$controller_desc" "$state") safe_count=$((safe_count + 1)) done < <(ls -d /sys/bus/pci/devices/* 2>/dev/null | sort) stop_spinner if [[ "$safe_count" -eq 0 ]]; then local msg if [[ "$hidden_target_count" -gt 0 && "$blocked_count" -eq 0 ]]; then msg="$(translate "All detected controllers/NVMe are already present in the selected VM.")\n\n$(translate "No additional device needs to be added.")" else msg="$(translate "No available Controllers/NVMe devices were found.")\n\n" fi if [[ "$blocked_count" -gt 0 ]]; then msg+="$(translate "Hidden for safety"):\n${blocked_report}" fi dialog --backtitle "ProxMenux" \ --title "$(translate "Controller + NVMe")" \ --msgbox "$msg" 18 84 return 1 fi local raw selected raw=$(dialog --backtitle "ProxMenux" \ --title "$(translate "Controller + NVMe")" \ --checklist "\n$(translate "Select available Controllers/NVMe to add:")" 20 96 12 \ "${menu_items[@]}" \ 2>&1 >/dev/tty) || return 1 selected=$(echo "$raw" | tr -d '"') SELECTED_CONTROLLER_PCIS=() local pci for pci in $selected; do _array_contains "$pci" "${SELECTED_CONTROLLER_PCIS[@]}" || SELECTED_CONTROLLER_PCIS+=("$pci") done if [[ ${#SELECTED_CONTROLLER_PCIS[@]} -eq 0 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate "Controller + NVMe")" \ --msgbox "\n$(translate "No controller/NVMe selected.")" 8 62 return 1 fi # SR-IOV guard: drop VFs / active PFs and inform the user. Same policy # as add_gpu_vm.sh and the VM creators — refuse to rewrite host VFIO # config for an SR-IOV device since it would collapse the VF tree. if declare -F _pci_sriov_filter_array >/dev/null 2>&1; then local sriov_removed="" sriov_removed=$(_pci_sriov_filter_array SELECTED_CONTROLLER_PCIS) if [[ -n "$sriov_removed" ]]; then local sriov_msg="" sriov_msg="\n$(translate "The following devices were excluded because they are part of an SR-IOV configuration:")\n" local entry bdf role first while IFS= read -r entry; do [[ -z "$entry" ]] && continue bdf="${entry%%|*}" role="${entry#*|}" first="${role%% *}" if [[ "$first" == "vf" ]]; then sriov_msg+="\n • ${bdf} — $(translate "Virtual Function")" else sriov_msg+="\n • ${bdf} — $(translate "Physical Function with") ${role#pf-active } $(translate "active VFs")" fi done <<< "$sriov_removed" sriov_msg+="\n\n$(translate "To pass SR-IOV Virtual Functions to a VM, edit the VM configuration manually via the Proxmox web interface.")" dialog --backtitle "ProxMenux" --colors \ --title "$(translate "SR-IOV Configuration Detected")" \ --msgbox "$sriov_msg" 18 82 fi if [[ ${#SELECTED_CONTROLLER_PCIS[@]} -eq 0 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate "Controller + NVMe")" \ --msgbox "\n$(translate "No eligible controllers remain after SR-IOV filtering.")" 8 70 return 1 fi fi return 0 } _prompt_raw_disk_conflict_policy() { local disk="$1" shift local -a guest_ids=("$@") local msg gid gtype gid_num gname gstatus msg="$(translate "Disk") ${disk} $(translate "is referenced in the following stopped VM(s)/CT(s):")\\n\\n" for gid in "${guest_ids[@]}"; do gtype="${gid%%:*}"; gid_num="${gid##*:}" if [[ "$gtype" == "VM" ]]; then gname=$(_vm_name_by_id "$gid_num") gstatus=$(qm status "$gid_num" 2>/dev/null | awk '{print $2}') msg+=" - VM $gid_num ($gname) [${gstatus}]\\n" else gname=$(pct config "$gid_num" 2>/dev/null | awk '/^hostname:/ {print $2}') [[ -z "$gname" ]] && gname="CT-$gid_num" gstatus=$(pct status "$gid_num" 2>/dev/null | awk '{print $2}') msg+=" - CT $gid_num ($gname) [${gstatus}]\\n" fi done msg+="\\n$(translate "Choose action:")" local choice choice=$(dialog --backtitle "ProxMenux" \ --title "$(translate "Disk Reference Conflict")" \ --menu "$msg" 22 84 3 \ "1" "$(translate "Disable onboot on affected VM(s)/CT(s)")" \ "2" "$(translate "Remove disk references from affected VM(s)/CT(s) config")" \ "3" "$(translate "Skip — leave as-is")" \ 2>&1 >/dev/tty) || { echo "skip"; return; } case "$choice" in 1) echo "disable_onboot" ;; 2) echo "remove_refs" ;; *) echo "skip" ;; esac } confirm_summary() { # ── Risk detection ───────────────────────────────────────────────────────── local reinforce_limited_firmware="no" local bios_date bios_year current_year bios_age cpu_model risk_detail="" bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null) bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1) current_year=$(date +%Y 2>/dev/null) if [[ -n "$bios_year" && -n "$current_year" ]]; then bios_age=$(( current_year - bios_year )) if (( bios_age >= 7 )); then reinforce_limited_firmware="yes" risk_detail="$(translate "BIOS from") ${bios_year} (${bios_age} $(translate "years old")) — $(translate "older firmware may increase passthrough instability")" fi fi cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs) if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then reinforce_limited_firmware="yes" [[ -z "$risk_detail" ]] && risk_detail="$(translate "Low-power CPU platform"): ${cpu_model}" fi # ── Build unified message ────────────────────────────────────────────────── local msg pci display_name msg="\n" # Devices to add msg+="\Zb$(translate "Devices to add to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\Zn\n" for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do display_name=$(_pci_storage_display_name "$pci") msg+=" \Zb•\Zn ${pci} ${display_name}\n" done msg+="\n" # Compatibility notice (always shown) msg+="\Zb\Z4⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\Zn\n\n" msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n" msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n" # Detected risk (only when applicable) if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then msg+="\n\Z1$(translate "Detected risk factor"): ${risk_detail}\Zn\n" fi msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${SELECTED_VMID}.conf\n" msg+="\n\Zb$(translate "Do you want to continue?")\Zn" local height=22 [[ "$reinforce_limited_firmware" == "yes" ]] && height=25 if ! dialog --backtitle "ProxMenux" --colors \ --title "$(translate "Confirm Controller + NVMe Assignment")" \ --yesno "$msg" $height 90; then return 1 fi return 0 } prompt_controller_conflict_policy() { local pci="$1" shift local -a source_vms=("$@") local msg vmid vm_name st ob msg="\n$(translate "Selected device is already assigned to other VM(s):")\n\n" for vmid in "${source_vms[@]}"; do vm_name=$(_vm_name_by_id "$vmid") st="stopped"; _vm_status_is_running "$vmid" && st="running" ob="0"; _vm_onboot_is_enabled "$vmid" && ob="1" msg+=" - VM ${vmid} (${vm_name}) [${st}, onboot=${ob}]\n" done msg+="\n$(translate "Choose action for this controller/NVMe:")" local choice choice=$(dialog --backtitle "ProxMenux" \ --title "$(translate "Controller/NVMe Conflict Policy")" \ --menu "$msg" 20 80 10 \ "1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \ "2" "$(translate "Move to target VM (remove from source VM config)")" \ "3" "$(translate "Skip this device")" \ 2>&1 >/dev/tty) || { echo "skip"; return; } case "$choice" in 1) echo "keep_disable_onboot" ;; 2) echo "move_remove_source" ;; *) echo "skip" ;; esac } # ── DIALOG PHASE: resolve all conflicts before terminal ─────────────────────── resolve_disk_conflicts() { local -a new_pci_list=() local pci vmid action slot_base scope_key has_running # ── hostpci conflicts: controller already assigned to another VM ────────── for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do local -a source_vms=() mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null) if [[ ${#source_vms[@]} -eq 0 ]]; then new_pci_list+=("$pci") continue fi has_running=false for vmid in "${source_vms[@]}"; do if _vm_status_is_running "$vmid"; then has_running=true dialog --backtitle "ProxMenux" \ --title "$(translate "Device In Use")" \ --msgbox "\n$(translate "Controller") $pci $(translate "is in use by running VM") $vmid.\n\n$(translate "Stop it first and run this option again.")" \ 10 72 break fi done $has_running && continue scope_key=$(printf '%s,' "${source_vms[@]}") if [[ -n "$WIZARD_CONFLICT_POLICY" && "$WIZARD_CONFLICT_SCOPE" == "$scope_key" ]]; then action="$WIZARD_CONFLICT_POLICY" else action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}") WIZARD_CONFLICT_POLICY="$action" WIZARD_CONFLICT_SCOPE="$scope_key" fi case "$action" in keep_disable_onboot) for vmid in "${source_vms[@]}"; do _vm_onboot_is_enabled "$vmid" && qm set "$vmid" -onboot 0 >/dev/null 2>&1 done NEED_HOOK_SYNC=true new_pci_list+=("$pci") ;; move_remove_source) slot_base=$(_pci_slot_base "$pci") for vmid in "${source_vms[@]}"; do _remove_pci_slot_from_vm_config "$vmid" "$slot_base" done new_pci_list+=("$pci") ;; *) ;; # skip — do not add to new_pci_list esac done SELECTED_CONTROLLER_PCIS=("${new_pci_list[@]}") if [[ ${#SELECTED_CONTROLLER_PCIS[@]} -eq 0 ]]; then dialog --backtitle "ProxMenux" \ --title "$(translate "Controller + NVMe")" \ --msgbox "\n$(translate "No controllers remaining after conflict resolution.")" 8 64 return 1 fi # ── Raw disk passthrough conflicts ─────────────────────────────────────── local raw_disk_policy="" raw_disk_scope="" for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do local -a cdisks=() while IFS= read -r disk; do [[ -z "$disk" ]] && continue _array_contains "$disk" "${cdisks[@]}" || cdisks+=("$disk") done < <(_controller_block_devices "$pci") for disk in "${cdisks[@]}"; do _disk_used_in_guest_configs "$disk" || continue _disk_used_in_running_guest "$disk" && continue local -a guest_ids=() mapfile -t guest_ids < <(_disk_guest_ids "$disk") [[ ${#guest_ids[@]} -eq 0 ]] && continue local gscope gaction gscope=$(printf '%s,' "${guest_ids[@]}") if [[ -n "$raw_disk_policy" && "$raw_disk_scope" == "$gscope" ]]; then gaction="$raw_disk_policy" else gaction=$(_prompt_raw_disk_conflict_policy "$disk" "${guest_ids[@]}") raw_disk_policy="$gaction" raw_disk_scope="$gscope" fi local gid gtype gid_num slot case "$gaction" in disable_onboot) for gid in "${guest_ids[@]}"; do gtype="${gid%%:*}"; gid_num="${gid##*:}" if [[ "$gtype" == "VM" ]]; then _vm_onboot_is_enabled "$gid_num" && qm set "$gid_num" -onboot 0 >/dev/null 2>&1 else grep -qE '^onboot:\s*1' "/etc/pve/lxc/$gid_num.conf" 2>/dev/null && \ pct set "$gid_num" -onboot 0 >/dev/null 2>&1 fi done ;; remove_refs) for gid in "${guest_ids[@]}"; do gtype="${gid%%:*}"; gid_num="${gid##*:}" if [[ "$gtype" == "VM" ]]; then while IFS= read -r slot; do [[ -z "$slot" ]] && continue qm set "$gid_num" -delete "$slot" >/dev/null 2>&1 done < <(_find_disk_slots_in_vm "$gid_num" "$disk") else while IFS= read -r slot; do [[ -z "$slot" ]] && continue pct set "$gid_num" -delete "$slot" >/dev/null 2>&1 done < <(_find_disk_slots_in_ct "$gid_num" "$disk") fi done ;; esac done done return 0 } apply_assignment() { : >"$LOG_FILE" set_title msg_info "$(translate "Applying Controller/NVMe passthrough to VM") ${SELECTED_VMID}..." msg_ok "$(translate "Target VM validated") (${SELECTED_VM_NAME} / ${SELECTED_VMID})" msg_ok "$(translate "Selected devices"): ${#SELECTED_CONTROLLER_PCIS[@]}" local hostpci_idx=0 if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then hostpci_idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0) else local hostpci_existing hostpci_existing=$(qm config "$SELECTED_VMID" 2>/dev/null) while grep -q "^hostpci${hostpci_idx}:" <<< "$hostpci_existing"; do hostpci_idx=$((hostpci_idx + 1)) done fi local pci bdf assigned_count=0 for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do bdf="${pci#0000:}" if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then if _pci_function_assigned_to_vm "$pci" "$SELECTED_VMID"; then msg_warn "$(translate "Controller/NVMe already present in VM config") ($pci)" continue fi elif qm config "$SELECTED_VMID" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*(0000:)?${bdf}([,[:space:]]|$)"; then msg_warn "$(translate "Controller/NVMe already present in VM config") ($pci)" continue fi local display_name display_name=$(_pci_storage_display_name "$pci") msg_info "$(translate "Adding") ${display_name} (${pci}) → hostpci${hostpci_idx}..." if qm set "$SELECTED_VMID" "--hostpci${hostpci_idx}" "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})" assigned_count=$((assigned_count + 1)) hostpci_idx=$((hostpci_idx + 1)) else msg_error "$(translate "Failed to assign Controller/NVMe") (${pci})" fi done if $NEED_HOOK_SYNC && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then ensure_proxmenux_gpu_guard_hookscript sync_proxmenux_gpu_guard_hooks msg_ok "$(translate "VM hook guard synced for shared controller/NVMe protection")" fi echo "" echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}" if [[ "$assigned_count" -gt 0 ]]; then msg_ok "$(translate "Completed.") $assigned_count $(translate "device(s) added to VM") ${SELECTED_VMID}." else msg_warn "$(translate "No new Controller/NVMe entries were added.")" fi if [[ "${IOMMU_ALREADY_ACTIVE:-0}" == "1" ]]; then msg_ok "$(translate "IOMMU is enabled on the system")" elif [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")" echo "" if whiptail --title "$(translate "Reboot Required")" \ --yesno "\n$(translate "IOMMU has been enabled on this system. A reboot is required to apply the changes. Reboot now?")" 11 64; then msg_success "$(translate "Press Enter to continue...")" read -r msg_warn "$(translate "Rebooting the system...")" reboot else msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")" msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")" fi fi echo "" msg_success "$(translate "Press Enter to continue...")" read -r } main() { export WIZARD_CONFLICT_POLICY export WIZARD_CONFLICT_SCOPE select_target_vm || exit 0 validate_vm_requirements || exit 0 select_controller_nvme || exit 0 resolve_disk_conflicts || exit 0 confirm_summary || exit 0 apply_assignment } main "$@"