mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer. The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded. The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
766 lines
28 KiB
Bash
766 lines
28 KiB
Bash
#!/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/global/pmx_journal.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
|
|
fi
|
|
load_language
|
|
initialize_cache
|
|
|
|
SELECTED_VMID=""
|
|
SELECTED_VM_NAME=""
|
|
declare -a SELECTED_CONTROLLER_PCIS=()
|
|
IOMMU_PENDING_REBOOT=0
|
|
IOMMU_ALREADY_ACTIVE=0
|
|
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 FUNC_VERSION="1.0"
|
|
pmx_journal_context "enable_iommu_cmdline" "$FUNC_VERSION"
|
|
|
|
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)"
|
|
pmx_edit_file "$cmdline_file" "s|\\s*$| ${iommu_param} iommu=pt|"
|
|
pmx_record_execution "refresh Proxmox boot entries" "proxmox-boot-tool refresh"
|
|
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)"
|
|
pmx_edit_file "$grub_file" "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|"
|
|
pmx_record_execution "regenerate GRUB configuration" "update-grub"
|
|
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 FUNC_VERSION="1.0"
|
|
pmx_journal_context "resolve_disk_conflicts" "$FUNC_VERSION"
|
|
|
|
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
|
|
if _vm_onboot_is_enabled "$vmid"; then
|
|
pmx_record_execution "disable autostart for source VM ${vmid}" "qm set ${vmid} -onboot 0"
|
|
qm set "$vmid" -onboot 0 >/dev/null 2>&1
|
|
fi
|
|
done
|
|
new_pci_list+=("$pci")
|
|
;;
|
|
move_remove_source)
|
|
slot_base=$(_pci_slot_base "$pci")
|
|
for vmid in "${source_vms[@]}"; do
|
|
pmx_record_execution "remove PCI slot ${slot_base} from source VM ${vmid}" \
|
|
"_remove_pci_slot_from_vm_config ${vmid} ${slot_base}"
|
|
_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
|
|
if _vm_onboot_is_enabled "$gid_num"; then
|
|
pmx_record_execution "disable autostart for VM ${gid_num}" "qm set ${gid_num} -onboot 0"
|
|
qm set "$gid_num" -onboot 0 >/dev/null 2>&1
|
|
fi
|
|
else
|
|
if grep -qE '^onboot:\s*1' "/etc/pve/lxc/$gid_num.conf" 2>/dev/null; then
|
|
pmx_record_execution "disable autostart for CT ${gid_num}" "pct set ${gid_num} -onboot 0"
|
|
pct set "$gid_num" -onboot 0 >/dev/null 2>&1
|
|
fi
|
|
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
|
|
pmx_record_execution "remove disk slot ${slot} from VM ${gid_num}" \
|
|
"qm set ${gid_num} -delete ${slot}"
|
|
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
|
|
pmx_record_execution "remove disk slot ${slot} from CT ${gid_num}" \
|
|
"pct set ${gid_num} -delete ${slot}"
|
|
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() {
|
|
local FUNC_VERSION="1.0"
|
|
pmx_journal_context "apply_assignment" "$FUNC_VERSION"
|
|
|
|
: >"$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}..."
|
|
pmx_record_execution "assign PCI device ${pci} to VM ${SELECTED_VMID} as hostpci${hostpci_idx}" \
|
|
"qm set ${SELECTED_VMID} --hostpci${hostpci_idx} ${pci},pcie=1"
|
|
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
|
|
|
|
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...")"
|
|
pmx_record_execution "reboot host after enabling IOMMU" "reboot"
|
|
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 "$@"
|