From 5826b0419bece87ff0ce8ffc7dd7e553c0808f9b Mon Sep 17 00:00:00 2001 From: MacRimi Date: Sun, 5 Apr 2026 11:24:08 +0200 Subject: [PATCH] update pci_passthrough_helpers.sh --- scripts/global/pci_passthrough_helpers.sh | 52 ++ scripts/global/vm_storage_helpers.sh | 138 ++++ scripts/gpu_tpu/add_gpu_vm.sh | 423 ++++++++-- scripts/menus/create_vm_menu.sh | 96 ++- scripts/share/lxc-mount-manager.sh | 480 ----------- scripts/vm/create_vm.sh | 175 ---- scripts/vm/disk_selector.sh | 489 ++++++----- scripts/vm/select_linux_iso.sh | 26 +- scripts/vm/select_nas_iso.sh | 179 +++- scripts/vm/synology.sh | 942 ++++++++++++---------- scripts/vm/vm_creator.sh | 252 +++++- scripts/vm/vm_creator_.sh | 466 ----------- scripts/vm/zimaos.sh | 878 +++++++++++--------- 13 files changed, 2322 insertions(+), 2274 deletions(-) create mode 100644 scripts/global/pci_passthrough_helpers.sh create mode 100644 scripts/global/vm_storage_helpers.sh delete mode 100644 scripts/share/lxc-mount-manager.sh delete mode 100644 scripts/vm/create_vm.sh delete mode 100644 scripts/vm/vm_creator_.sh diff --git a/scripts/global/pci_passthrough_helpers.sh b/scripts/global/pci_passthrough_helpers.sh new file mode 100644 index 00000000..fa29ace6 --- /dev/null +++ b/scripts/global/pci_passthrough_helpers.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +if [[ -n "${__PROXMENUX_PCI_PASSTHROUGH_HELPERS__}" ]]; then + return 0 +fi +__PROXMENUX_PCI_PASSTHROUGH_HELPERS__=1 + +function _pci_is_iommu_active() { + grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null || return 1 + [[ -d /sys/kernel/iommu_groups ]] || return 1 + find /sys/kernel/iommu_groups -mindepth 1 -maxdepth 1 -type d -print -quit 2>/dev/null | grep -q . +} + +function _pci_next_hostpci_index() { + local vmid="$1" + local idx=0 + local hostpci_existing + + hostpci_existing=$(qm config "$vmid" 2>/dev/null) || return 1 + while grep -q "^hostpci${idx}:" <<< "$hostpci_existing"; do + idx=$((idx + 1)) + done + echo "$idx" +} + +function _pci_slot_assigned_to_vm() { + local pci_full="$1" + local vmid="$2" + local slot_base + slot_base="${pci_full#0000:}" + slot_base="${slot_base%.*}" + + qm config "$vmid" 2>/dev/null \ + | grep -qE "^hostpci[0-9]+:.*(0000:)?${slot_base}(\\.[0-7])?([,[:space:]]|$)" +} + +function _pci_function_assigned_to_vm() { + local pci_full="$1" + local vmid="$2" + local bdf slot func pattern + bdf="${pci_full#0000:}" + slot="${bdf%.*}" + func="${bdf##*.}" + + if [[ "$func" == "0" ]]; then + pattern="^hostpci[0-9]+:.*(0000:)?(${bdf}|${slot})([,:[:space:]]|$)" + else + pattern="^hostpci[0-9]+:.*(0000:)?${bdf}([,[:space:]]|$)" + fi + + qm config "$vmid" 2>/dev/null | grep -qE "$pattern" +} diff --git a/scripts/global/vm_storage_helpers.sh b/scripts/global/vm_storage_helpers.sh new file mode 100644 index 00000000..187eebc5 --- /dev/null +++ b/scripts/global/vm_storage_helpers.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash + +if [[ -n "${__PROXMENUX_VM_STORAGE_HELPERS__}" ]]; then + return 0 +fi +__PROXMENUX_VM_STORAGE_HELPERS__=1 + +function _array_contains() { + local needle="$1" + shift + local item + for item in "$@"; do + [[ "$item" == "$needle" ]] && return 0 + done + return 1 +} + +function _refresh_host_storage_cache() { + MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}') + SWAP_DISKS=$(swapon --noheadings --raw --show=NAME 2>/dev/null) + LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -r -n1 readlink -f | sort -u) + CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null) + + ZFS_DISKS="" + local zfs_raw entry path base_disk + zfs_raw=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror') + for entry in $zfs_raw; do + path="" + if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then + [[ -e "/dev/disk/by-id/$entry" ]] && path=$(readlink -f "/dev/disk/by-id/$entry") + elif [[ "$entry" == /dev/* ]]; then + path="$entry" + fi + if [[ -n "$path" ]]; then + base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null) + [[ -n "$base_disk" ]] && ZFS_DISKS+="/dev/$base_disk"$'\n' + fi + done + ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u) +} + +function _disk_is_host_system_used() { + local disk="$1" + local disk_real part fstype part_path + DISK_USAGE_REASON="" + + while read -r part fstype; do + [[ -z "$part" ]] && continue + part_path="/dev/$part" + + if grep -qFx "$part_path" <<< "$MOUNTED_DISKS"; then + DISK_USAGE_REASON="$(translate "Mounted filesystem detected") ($part_path)" + return 0 + fi + if grep -qFx "$part_path" <<< "$SWAP_DISKS"; then + DISK_USAGE_REASON="$(translate "Swap partition detected") ($part_path)" + return 0 + fi + case "$fstype" in + zfs_member) + DISK_USAGE_REASON="$(translate "ZFS member detected") ($part_path)" + return 0 + ;; + linux_raid_member) + DISK_USAGE_REASON="$(translate "RAID member detected") ($part_path)" + return 0 + ;; + LVM2_member) + DISK_USAGE_REASON="$(translate "LVM physical volume detected") ($part_path)" + return 0 + ;; + esac + done < <(lsblk -ln -o NAME,FSTYPE "$disk" 2>/dev/null) + + disk_real=$(readlink -f "$disk" 2>/dev/null) + if [[ -n "$disk_real" && -n "$LVM_DEVICES" ]] && grep -qFx "$disk_real" <<< "$LVM_DEVICES"; then + DISK_USAGE_REASON="$(translate "Disk is part of host LVM")" + return 0 + fi + if [[ -n "$ZFS_DISKS" && "$ZFS_DISKS" == *"$disk"* ]]; then + DISK_USAGE_REASON="$(translate "Disk is part of a host ZFS pool")" + return 0 + fi + return 1 +} + +function _disk_used_in_guest_configs() { + local disk="$1" + local real_path + real_path=$(readlink -f "$disk" 2>/dev/null) + + if [[ -n "$real_path" ]] && grep -Fq "$real_path" <<< "$CONFIG_DATA"; then + return 0 + fi + + local symlink + for symlink in /dev/disk/by-id/*; do + [[ -e "$symlink" ]] || continue + if [[ "$(readlink -f "$symlink")" == "$real_path" ]] && grep -Fq "$symlink" <<< "$CONFIG_DATA"; then + return 0 + fi + done + return 1 +} + +function _controller_block_devices() { + local pci_full="$1" + local pci_root="/sys/bus/pci/devices/$pci_full" + [[ -d "$pci_root" ]] || return 0 + + local sys_block dev_name cur base + # Walk /sys/block and resolve each block device back to its ancestor PCI device. + # This avoids unbounded recursive scans while still handling NVMe/SATA paths. + for sys_block in /sys/block/*; do + [[ -e "$sys_block/device" ]] || continue + dev_name=$(basename "$sys_block") + [[ -b "/dev/$dev_name" ]] || continue + + cur=$(readlink -f "$sys_block/device" 2>/dev/null) + [[ -n "$cur" ]] || continue + + while [[ "$cur" != "/" ]]; do + base=$(basename "$cur") + if [[ "$base" == "$pci_full" ]]; then + echo "/dev/$dev_name" + break + fi + cur=$(dirname "$cur") + done + done +} + +function _vm_is_q35() { + local vmid="$1" + local machine_line + machine_line=$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/ {print $2}') + [[ "$machine_line" == *q35* ]] +} diff --git a/scripts/gpu_tpu/add_gpu_vm.sh b/scripts/gpu_tpu/add_gpu_vm.sh index 31dadd2e..c92b6008 100644 --- a/scripts/gpu_tpu/add_gpu_vm.sh +++ b/scripts/gpu_tpu/add_gpu_vm.sh @@ -23,15 +23,29 @@ # - VM config: hostpci entries, NVIDIA KVM hiding # ========================================================== -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +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="$BASE_DIR/utils.sh" +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/add_gpu_vm.log" screen_capture="/tmp/proxmenux_add_gpu_vm_screen_$$.txt" if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" fi +if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then + source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" +elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then + source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" +fi load_language initialize_cache @@ -50,6 +64,7 @@ SELECTED_GPU_NAME="" declare -a IOMMU_DEVICES=() # all PCI addrs in IOMMU group (endpoint devices) declare -a IOMMU_VFIO_IDS=() # vendor:device for vfio-pci ids= +declare -a EXTRA_AUDIO_DEVICES=() # sibling audio function(s), typically *.1 IOMMU_GROUP="" SELECTED_VMID="" @@ -63,11 +78,17 @@ SWITCH_LXC_LIST="" SWITCH_FROM_VM=false SWITCH_VM_SRC="" TARGET_VM_ALREADY_HAS_GPU=false +VM_SWITCH_ALREADY_VFIO=false +PREFLIGHT_HOST_REBOOT_REQUIRED=true AMD_ROM_FILE="" HOST_CONFIG_CHANGED=false # set to true whenever host VFIO config is actually written +PRESELECT_VMID="" +WIZARD_CALL=false +GPU_WIZARD_RESULT_FILE="" + # ========================================================== # Helpers @@ -92,19 +113,173 @@ _add_line_if_missing() { fi } +_append_unique() { + local needle="$1" + shift + local item + for item in "$@"; do + [[ "$item" == "$needle" ]] && return 1 + done + return 0 +} + +_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" +} + +_set_wizard_result() { + local result="$1" + [[ -z "${GPU_WIZARD_RESULT_FILE:-}" ]] && return 0 + printf '%s\n' "$result" >"$GPU_WIZARD_RESULT_FILE" 2>/dev/null || true +} + +_file_has_exact_line() { + local line="$1" + local file="$2" + [[ -f "$file" ]] || return 1 + grep -qFx "$line" "$file" +} + +evaluate_host_reboot_requirement() { + # Fast path for VM-to-VM reassignment where GPU is already bound to vfio + if [[ "$VM_SWITCH_ALREADY_VFIO" == "true" ]]; then + PREFLIGHT_HOST_REBOOT_REQUIRED=false + return 0 + fi + + local needs_change=false + local current_driver + current_driver=$(_get_pci_driver "$SELECTED_GPU_PCI") + [[ "$current_driver" != "vfio-pci" ]] && needs_change=true + + # /etc/modules expected lines + local modules_file="/etc/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 + _file_has_exact_line "$mod" "$modules_file" || needs_change=true + done + + # vfio-pci 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) + if [[ -z "$ids_line" ]]; then + needs_change=true + else + [[ "$ids_line" == *"disable_vga=1"* ]] || needs_change=true + ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//') + local existing_ids=() + IFS=',' read -ra existing_ids <<< "$ids_part" + local required found existing + for required in "${IOMMU_VFIO_IDS[@]}"; do + found=false + for existing in "${existing_ids[@]}"; do + [[ "$existing" == "$required" ]] && found=true && break + done + $found || needs_change=true + done + fi + + # modprobe options files + _file_has_exact_line "options vfio_iommu_type1 allow_unsafe_interrupts=1" \ + /etc/modprobe.d/iommu_unsafe_interrupts.conf || needs_change=true + _file_has_exact_line "options kvm ignore_msrs=1" \ + /etc/modprobe.d/kvm.conf || needs_change=true + + # AMD softdep + if [[ "$SELECTED_GPU" == "amd" ]]; then + _file_has_exact_line "softdep radeon pre: vfio-pci" "$vfio_conf" || needs_change=true + _file_has_exact_line "softdep amdgpu pre: vfio-pci" "$vfio_conf" || needs_change=true + _file_has_exact_line "softdep snd_hda_intel pre: vfio-pci" "$vfio_conf" || needs_change=true + fi + + # host driver blacklist + local blacklist_file="/etc/modprobe.d/blacklist.conf" + case "$SELECTED_GPU" in + nvidia) + _file_has_exact_line "blacklist nouveau" "$blacklist_file" || needs_change=true + _file_has_exact_line "blacklist nvidia" "$blacklist_file" || needs_change=true + _file_has_exact_line "blacklist nvidiafb" "$blacklist_file" || needs_change=true + _file_has_exact_line "blacklist lbm-nouveau" "$blacklist_file" || needs_change=true + _file_has_exact_line "options nouveau modeset=0" "$blacklist_file" || needs_change=true + ;; + amd) + _file_has_exact_line "blacklist radeon" "$blacklist_file" || needs_change=true + _file_has_exact_line "blacklist amdgpu" "$blacklist_file" || needs_change=true + ;; + intel) + _file_has_exact_line "blacklist i915" "$blacklist_file" || needs_change=true + ;; + esac + + if [[ "$needs_change" == "true" ]]; then + PREFLIGHT_HOST_REBOOT_REQUIRED=true + else + PREFLIGHT_HOST_REBOOT_REQUIRED=false + fi +} + +parse_cli_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --vmid) + if [[ -n "${2:-}" ]]; then + PRESELECT_VMID="$2" + shift 2 + else + shift + fi + ;; + --wizard) + WIZARD_CALL=true + shift + ;; + --result-file) + if [[ -n "${2:-}" ]]; then + GPU_WIZARD_RESULT_FILE="$2" + shift 2 + else + shift + fi + ;; + *) + shift + ;; + esac + done +} + _get_vm_run_title() { if [[ "$SWITCH_FROM_LXC" == "true" && "$SWITCH_FROM_VM" == "true" ]]; then - echo "GPU Switch Mode (LXC/VM → VM)" + echo "$(translate 'GPU Passthrough to VM (reassign from LXC and another VM)')" elif [[ "$SWITCH_FROM_LXC" == "true" ]]; then - echo "GPU Switch Mode (LXC → VM)" + echo "$(translate 'GPU Passthrough to VM (from LXC)')" elif [[ "$SWITCH_FROM_VM" == "true" ]]; then - echo "GPU Switch Mode (VM → VM)" + echo "$(translate 'GPU Passthrough to VM (reassign from another VM)')" else echo "$(translate 'GPU Passthrough to VM')" fi } _is_pci_slot_assigned_to_vm() { + if declare -F _pci_slot_assigned_to_vm >/dev/null 2>&1; then + _pci_slot_assigned_to_vm "$1" "$2" + return $? + fi + local pci_full="$1" local vmid="$2" local slot_base @@ -118,6 +293,11 @@ _is_pci_slot_assigned_to_vm() { # Match a specific PCI function when possible. # For function .0, also accept slot-only entries (e.g. 01:00) as equivalent. _is_pci_function_assigned_to_vm() { + if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then + _pci_function_assigned_to_vm "$1" "$2" + return $? + fi + local pci_full="$1" local vmid="$2" local bdf slot func pattern @@ -234,6 +414,7 @@ detect_host_gpus() { GPU_COUNT=${#ALL_GPU_PCIS[@]} if [[ $GPU_COUNT -eq 0 ]]; then + _set_wizard_result "no_gpu" dialog --backtitle "ProxMenux" \ --title "$(translate 'No GPU Detected')" \ --msgbox "\n$(translate 'No compatible GPU was detected on this host.')" 8 60 @@ -248,6 +429,10 @@ detect_host_gpus() { # Phase 1 — Step 2: Check IOMMU, offer to enable it # ========================================================== check_iommu_enabled() { + if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then + 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 @@ -266,10 +451,10 @@ check_iommu_enabled() { --yesno "$msg" 15 72 local response=$? - clear + [[ "$WIZARD_CALL" != "true" ]] && clear if [[ $response -eq 0 ]]; then - show_proxmenux_logo + [[ "$WIZARD_CALL" != "true" ]] && show_proxmenux_logo msg_title "$(translate 'Enabling IOMMU')" _enable_iommu_cmdline echo @@ -709,11 +894,59 @@ analyze_iommu_group() { --msgbox "\n${msg}" 22 82 } +detect_optional_gpu_audio() { + EXTRA_AUDIO_DEVICES=() + + local sibling_audio="${SELECTED_GPU_PCI%.*}.1" + local dev_path="/sys/bus/pci/devices/${sibling_audio}" + [[ -d "$dev_path" ]] || return 0 + + local class_hex + class_hex=$(cat "${dev_path}/class" 2>/dev/null | sed 's/^0x//') + [[ "${class_hex:0:2}" == "04" ]] || return 0 + + local already_in_group=false dev + for dev in "${IOMMU_DEVICES[@]}"; do + if [[ "$dev" == "$sibling_audio" ]]; then + already_in_group=true + break + fi + done + + if [[ "$already_in_group" == "true" ]]; then + return 0 + fi + + EXTRA_AUDIO_DEVICES+=("$sibling_audio") + + local vid did new_id + vid=$(cat "${dev_path}/vendor" 2>/dev/null | sed 's/0x//') + did=$(cat "${dev_path}/device" 2>/dev/null | sed 's/0x//') + if [[ -n "$vid" && -n "$did" ]]; then + new_id="${vid}:${did}" + if _append_unique "$new_id" "${IOMMU_VFIO_IDS[@]}"; then + IOMMU_VFIO_IDS+=("$new_id") + fi + fi +} + # ========================================================== # Phase 1 — Step 6: VM selection # ========================================================== select_vm() { + if [[ -n "$PRESELECT_VMID" ]]; then + if qm config "$PRESELECT_VMID" >/dev/null 2>&1; then + SELECTED_VMID="$PRESELECT_VMID" + VM_NAME=$(qm config "$SELECTED_VMID" 2>/dev/null | grep "^name:" | awk '{print $2}') + return 0 + fi + dialog --backtitle "ProxMenux" \ + --title "$(translate 'Invalid VMID')" \ + --msgbox "\n$(translate 'The preselected VMID does not exist on this host:') ${PRESELECT_VMID}" 9 72 + exit 1 + fi + local menu_items=() while IFS= read -r line; do @@ -805,6 +1038,7 @@ check_switch_mode() { 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?')" dialog --backtitle "ProxMenux" \ @@ -828,19 +1062,57 @@ check_switch_mode() { done if [[ -n "$vm_src_id" ]]; then + local src_running=false + _vm_is_running "$vm_src_id" && src_running=true + + if [[ "$src_running" == "true" ]]; then + local msg + msg="\n$(translate 'The selected GPU is already assigned to another VM that is currently running:')\n\n" + msg+=" VM ${vm_src_id} (${vm_src_name:-VM-${vm_src_id}})\n\n" + msg+="$(translate 'The same GPU cannot be used by two VMs at the same time.')\n\n" + msg+="$(translate 'Next step: stop that VM first, then run')\n" + msg+=" Hardware Graphics → Add GPU to VM\n" + msg+="$(translate 'to move the GPU safely.')" + + dialog --backtitle "ProxMenux" \ + --title "$(translate 'GPU Busy in Running VM')" \ + --msgbox "$msg" 16 78 + exit 0 + fi + SWITCH_FROM_VM=true SWITCH_VM_SRC="$vm_src_id" + local selected_driver + selected_driver=$(_get_pci_driver "$SELECTED_GPU_PCI") + if [[ "$selected_driver" == "vfio-pci" && "$SWITCH_FROM_LXC" != "true" ]]; then + VM_SWITCH_ALREADY_VFIO=true + fi + + local src_onboot target_onboot + src_onboot="0" + target_onboot="0" + _vm_onboot_enabled "$vm_src_id" && src_onboot="1" + _vm_onboot_enabled "$SELECTED_VMID" && target_onboot="1" local msg msg="\n$(translate 'The selected GPU is already configured for passthrough to:')\n\n" msg+=" VM ${vm_src_id} (${vm_src_name:-VM-${vm_src_id}})\n\n" + msg+="$(translate 'That VM is currently stopped, so the GPU can be reassigned now.')\n" + msg+="\Z3$(translate 'Important: both VMs cannot be running at the same time with the same GPU.')\Zn\n\n" msg+="$(translate 'The existing hostpci entry will be removed from that VM and configured on'): " msg+="VM ${SELECTED_VMID} (${VM_NAME:-VM-${SELECTED_VMID}})\n\n" + if [[ "$src_onboot" == "1" && "$target_onboot" == "1" ]]; then + msg+="\Z3$(translate 'Warning: both VMs have autostart enabled (onboot=1).')\Zn\n" + msg+="\Z3$(translate 'Disable autostart on one VM to avoid startup conflicts.')\Zn\n\n" + fi + if [[ "$VM_SWITCH_ALREADY_VFIO" == "true" ]]; then + msg+="$(translate 'Host GPU is already bound to vfio-pci. Host reconfiguration/reboot should not be required for this VM-to-VM reassignment.')\n\n" + fi msg+="$(translate 'Do you want to continue?')" - dialog --backtitle "ProxMenux" \ + dialog --backtitle "ProxMenux" --colors \ --title "$(translate 'GPU Already Assigned to Another VM')" \ - --yesno "$msg" 14 76 + --yesno "$msg" 24 88 [[ $? -ne 0 ]] && exit 0 fi } @@ -859,20 +1131,28 @@ confirm_summary() { msg+=" $(translate 'Target VM') : ${VM_NAME:-VM-${SELECTED_VMID}} (${SELECTED_VMID})\n" msg+=" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" msg+=" \Zb$(translate 'Host'):\Zn\n" - msg+=" • $(translate 'VFIO modules in /etc/modules')\n" - msg+=" • $(translate 'vfio-pci IDs in /etc/modprobe.d/vfio.conf')\n" - [[ "$SELECTED_GPU" == "amd" ]] && \ - msg+=" • $(translate 'AMD softdep configured')\n" - [[ "$SELECTED_GPU" == "amd" ]] && \ - msg+=" • $(translate 'GPU ROM dump to /usr/share/kvm/')\n" - msg+=" • $(translate 'GPU driver blacklisted')\n" - msg+=" • $(translate 'initramfs updated')\n" - msg+=" • \Zb$(translate 'System reboot required')\Zn\n\n" + if [[ "$PREFLIGHT_HOST_REBOOT_REQUIRED" != "true" ]]; then + msg+=" • $(translate 'Host VFIO configuration already up to date')\n" + msg+=" • $(translate 'No host VFIO reconfiguration expected')\n" + msg+=" • $(translate 'No host reboot expected')\n\n" + else + msg+=" • $(translate 'VFIO modules in /etc/modules')\n" + msg+=" • $(translate 'vfio-pci IDs in /etc/modprobe.d/vfio.conf')\n" + [[ "$SELECTED_GPU" == "amd" ]] && \ + msg+=" • $(translate 'AMD softdep configured')\n" + [[ "$SELECTED_GPU" == "amd" ]] && \ + msg+=" • $(translate 'GPU ROM dump to /usr/share/kvm/')\n" + msg+=" • $(translate 'GPU driver blacklisted')\n" + msg+=" • $(translate 'initramfs updated')\n" + msg+=" • \Zb$(translate 'System reboot required')\Zn\n\n" + fi msg+=" \Zb$(translate 'VM') ${SELECTED_VMID}:\Zn\n" [[ "$TARGET_VM_ALREADY_HAS_GPU" == "true" ]] && \ msg+=" • $(translate 'Existing hostpci entries detected — they will be reused')\n" msg+=" • $(translate 'Virtual display normalized to vga: std (compatibility)')\n" msg+=" • $(translate 'hostpci entries for all IOMMU group devices')\n" + [[ ${#EXTRA_AUDIO_DEVICES[@]} -gt 0 ]] && \ + 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" ]] && \ @@ -1144,9 +1424,13 @@ configure_vm() { # Find next free hostpciN index local idx=0 - while qm config "$SELECTED_VMID" 2>/dev/null | grep -q "^hostpci${idx}:"; do - idx=$((idx + 1)) - done + if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then + idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0) + else + while qm config "$SELECTED_VMID" 2>/dev/null | grep -q "^hostpci${idx}:"; do + idx=$((idx + 1)) + done + fi # Primary GPU: pcie=1, x-vga=1 only for NVIDIA/AMD (not Intel iGPU), romfile if AMD local gpu_opts="pcie=1" @@ -1173,6 +1457,17 @@ configure_vm() { idx=$((idx + 1)) done + # Optional sibling GPU audio function (typically *.1) when split from IOMMU group + for dev in "${EXTRA_AUDIO_DEVICES[@]}"; do + if _is_pci_function_assigned_to_vm "$dev" "$SELECTED_VMID"; then + msg_ok "$(translate 'GPU audio already present in target VM — existing hostpci entry reused'): ${dev}" | tee -a "$screen_capture" + continue + fi + qm set "$SELECTED_VMID" --hostpci${idx} "${dev},pcie=1" >>"$LOG_FILE" 2>&1 + msg_ok "$(translate 'GPU audio added'): hostpci${idx}: ${dev},pcie=1" | tee -a "$screen_capture" + idx=$((idx + 1)) + done + # NVIDIA: hide KVM hypervisor from guest [[ "$SELECTED_GPU" == "nvidia" ]] && _configure_nvidia_kvm_hide } @@ -1221,8 +1516,11 @@ update_initramfs_host() { # Main # ========================================================== main() { + parse_cli_args "$@" + : >"$LOG_FILE" : >"$screen_capture" + [[ "$WIZARD_CALL" == "true" ]] && _set_wizard_result "cancelled" # ── Phase 1: all dialogs (no terminal output) ───────── detect_host_gpus @@ -1233,23 +1531,33 @@ main() { ensure_selected_gpu_not_already_in_target_vm check_gpu_vm_compatibility analyze_iommu_group + detect_optional_gpu_audio check_vm_machine_type check_switch_mode + evaluate_host_reboot_requirement confirm_summary # ── Phase 2: processing ─────────────────────────────── - clear - show_proxmenux_logo local run_title run_title=$(_get_vm_run_title) - msg_title "${run_title}" + if [[ "$WIZARD_CALL" == "true" ]]; then + echo + else + clear + show_proxmenux_logo + msg_title "${run_title}" + fi - add_vfio_modules - configure_vfio_pci_ids - configure_iommu_options - [[ "$SELECTED_GPU" == "amd" ]] && add_softdep_amd - blacklist_gpu_drivers - [[ "$SELECTED_GPU" == "amd" ]] && dump_amd_rom + if [[ "$VM_SWITCH_ALREADY_VFIO" == "true" ]]; then + msg_ok "$(translate 'Host already in VFIO mode — skipping host reconfiguration for VM reassignment')" | tee -a "$screen_capture" + else + add_vfio_modules + configure_vfio_pci_ids + configure_iommu_options + [[ "$SELECTED_GPU" == "amd" ]] && add_softdep_amd + blacklist_gpu_drivers + [[ "$SELECTED_GPU" == "amd" ]] && dump_amd_rom + fi cleanup_lxc_configs cleanup_vm_config ensure_vm_display_std @@ -1257,43 +1565,50 @@ main() { [[ "$HOST_CONFIG_CHANGED" == "true" ]] && update_initramfs_host # ── Phase 3: summary ───────────────────────────────── - show_proxmenux_logo - msg_title "${run_title}" - cat "$screen_capture" - - echo - echo -e "${TAB}${BL}📄 Log: ${LOG_FILE}${CL}" - echo - - if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then - msg_info2 "$(translate 'After rebooting, verify VFIO binding with:')" - echo " lspci -nnk | grep -A2 vfio-pci" + if [[ "$WIZARD_CALL" == "true" ]]; then echo - msg_info2 "$(translate 'Next steps after reboot:')" - echo " 1. $(translate 'Start the VM')" else - msg_info2 "$(translate 'Host VFIO config was already up to date — no reboot needed.')" - msg_info2 "$(translate 'Next steps:')" - echo " 1. $(translate 'Start the VM')" + show_proxmenux_logo + msg_title "${run_title}" + cat "$screen_capture" + echo + fi + + if [[ "$WIZARD_CALL" == "true" ]]; then + _set_wizard_result "applied" + rm -f "$screen_capture" + return 0 + fi + + echo -e "${TAB}${BL}📄 Log: ${LOG_FILE}${CL}" + if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then + echo -e "${TAB}${DGN}- $(translate 'Host VFIO configuration changed — reboot required before starting the VM.')${CL}" + else + echo -e "${TAB}${DGN}- $(translate 'Host VFIO config was already up to date — no reboot needed.')${CL}" fi case "$SELECTED_GPU" in nvidia) - echo " 2. $(translate 'Install NVIDIA drivers from nvidia.com inside the guest')" - echo " 3. $(translate 'If Code 43 error: KVM hiding is already configured')" + echo -e "${TAB}${DGN}- $(translate 'Install NVIDIA drivers from nvidia.com inside the guest.')${CL}" + echo -e "${TAB}${DGN}- $(translate 'If Code 43 error appears, KVM hiding is already configured.')${CL}" ;; amd) - echo " 2. $(translate 'Install AMD GPU drivers inside the guest')" - echo " 3. $(translate 'If passthrough fails on Windows: install RadeonResetBugFix')" + echo -e "${TAB}${DGN}- $(translate 'Install AMD GPU drivers inside the guest.')${CL}" + echo -e "${TAB}${DGN}- $(translate 'If passthrough fails on Windows: install RadeonResetBugFix.')${CL}" [[ -n "$AMD_ROM_FILE" ]] && \ - echo " $(translate 'ROM file used'): /usr/share/kvm/${AMD_ROM_FILE}" + echo -e "${TAB}${DGN}- $(translate 'ROM file used'): /usr/share/kvm/${AMD_ROM_FILE}${CL}" ;; intel) - echo " 2. $(translate 'Install Intel Graphics Driver inside the guest')" - echo " 3. $(translate 'Enable Remote Desktop (RDP) before disabling the virtual display')" + echo -e "${TAB}${DGN}- $(translate 'Install Intel Graphics Driver inside the guest.')${CL}" + echo -e "${TAB}${DGN}- $(translate 'Enable Remote Desktop (RDP) before disabling the virtual display.')${CL}" ;; esac + echo + msg_info2 "$(translate 'If you want to use a physical monitor on the passthrough GPU:')" + echo " • $(translate 'First install the GPU drivers inside the guest and verify remote access (RDP/SSH).')" + echo " • $(translate 'Then change the VM display to none (vga: none) when the guest is stable.')" + echo msg_success "$(translate 'GPU passthrough configured for VM') ${SELECTED_VMID} (${VM_NAME})." echo @@ -1317,4 +1632,4 @@ main() { fi } -main +main "$@" diff --git a/scripts/menus/create_vm_menu.sh b/scripts/menus/create_vm_menu.sh index fd38a12e..60526f84 100644 --- a/scripts/menus/create_vm_menu.sh +++ b/scripts/menus/create_vm_menu.sh @@ -15,7 +15,7 @@ # configurations, streamlining the deployment of Linux, Windows, and other systems. # # Key features: -# - Supports both virtual disk creation and physical disk passthrough. +# - Supports virtual disks, import disks, and Controller + NVMe passthrough. # - Automates CPU, RAM, BIOS, network and storage configuration. # - Provides a user-friendly menu to select OS type, ISO image and disk interface. # - Automatically generates a detailed and styled HTML description for each VM. @@ -24,14 +24,22 @@ # consistent and maintainable way, using ProxMenux standards. # ========================================================== +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts" +LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)" +if [[ -f "$LOCAL_SCRIPTS_LOCAL/vm/disk_selector.sh" ]]; then + LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL" +else + LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT" +fi -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" VM_REPO="$LOCAL_SCRIPTS/vm" ISO_REPO="$LOCAL_SCRIPTS/vm" MENU_REPO="$LOCAL_SCRIPTS/menus" BASE_DIR="/usr/local/share/proxmenux" -UTILS_FILE="$BASE_DIR/utils.sh" +UTILS_FILE="$LOCAL_SCRIPTS/utils.sh" +[[ ! -f "$UTILS_FILE" ]] && UTILS_FILE="$BASE_DIR/utils.sh" VENV_PATH="/opt/googletrans-env" # Source utilities and required scripts @@ -55,12 +63,39 @@ initialize_cache function header_info() { clear show_proxmenux_logo - echo -e "${BL}╔═══════════════════════════════════════════════╗${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}║${YWB} ProxMenux VM Creator ${BL}║${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}╚═══════════════════════════════════════════════╝${CL}" - echo -e + msg_title "ProxMenux VM Creator" +} + +VM_WIZARD_CAPTURE_FILE="" +VM_WIZARD_CAPTURE_ACTIVE=0 + +function start_vm_wizard_capture() { + [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]] && return 0 + VM_WIZARD_CAPTURE_FILE="/tmp/proxmenux_vm_wizard_screen_capture_$$.txt" + : >"$VM_WIZARD_CAPTURE_FILE" + exec 8>&1 + exec > >(tee -a "$VM_WIZARD_CAPTURE_FILE") + VM_WIZARD_CAPTURE_ACTIVE=1 +} + +function stop_vm_wizard_capture() { + if [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]]; then + exec 1>&8 + exec 8>&- + VM_WIZARD_CAPTURE_ACTIVE=0 + fi + if [[ -n "${VM_WIZARD_CAPTURE_FILE:-}" && -f "$VM_WIZARD_CAPTURE_FILE" ]]; then + rm -f "$VM_WIZARD_CAPTURE_FILE" + fi + VM_WIZARD_CAPTURE_FILE="" +} + +function has_usable_gpu_for_vm_passthrough() { + lspci -nn 2>/dev/null \ + | grep -iE "VGA compatible controller|3D controller|Display controller" \ + | grep -ivE "Ethernet|Network|Audio" \ + | grep -ivE "ASPEED|AST[0-9]{3,4}|Matrox|G200e|BMC" \ + | grep -q . } # ========================================================== @@ -77,14 +112,15 @@ function header_info() { function start_vm_configuration() { if (whiptail --title "ProxMenux" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 60); then - header_info - load_default_vm_config "$OS_TYPE" + #header_info + #load_default_vm_config "$OS_TYPE" if [[ -z "$HN" ]]; then HN=$(whiptail --inputbox "$(translate "Enter a name for the new virtual machine:")" 10 60 --title "VM Hostname" 3>&1 1>&2 2>&3) [[ -z "$HN" ]] && HN="custom-vm" fi - + header_info + load_default_vm_config "$OS_TYPE" apply_default_vm_config else header_info @@ -133,19 +169,45 @@ while true; do esac if ! confirm_vm_creation; then + stop_vm_wizard_capture continue fi + start_vm_wizard_capture - start_vm_configuration || continue + if ! start_vm_configuration; then + stop_vm_wizard_capture + continue + fi - select_disk_type - if [[ -z "$DISK_TYPE" ]]; then - msg_error "$(translate "Disk type selection failed or cancelled")" + unset DISK_TYPE + if ! select_disk_type; then + stop_vm_wizard_capture + msg_error "$(translate "Storage plan selection failed or cancelled")" continue fi - create_vm + WIZARD_ADD_GPU="no" + if has_usable_gpu_for_vm_passthrough; then + if whiptail --backtitle "ProxMenux" --title "$(translate "Optional GPU Passthrough")" \ + --yesno "$(translate "Do you want to configure GPU passthrough for this VM now?")\n\n$(translate "This will launch the GPU assistant after VM creation and may require a host reboot.")" 12 78 --defaultno; then + WIZARD_ADD_GPU="yes" + fi + else + msg_warn "$(translate "No compatible GPU detected for VM passthrough. Skipping GPU wizard option.")" + fi + export WIZARD_ADD_GPU + + if [[ "$WIZARD_ADD_GPU" != "yes" ]]; then + stop_vm_wizard_capture + fi + + if ! create_vm; then + stop_vm_wizard_capture + msg_error "$(translate "VM creation failed or was cancelled during storage setup.")" + continue + fi + stop_vm_wizard_capture break done diff --git a/scripts/share/lxc-mount-manager.sh b/scripts/share/lxc-mount-manager.sh deleted file mode 100644 index 786227b1..00000000 --- a/scripts/share/lxc-mount-manager.sh +++ /dev/null @@ -1,480 +0,0 @@ -#!/bin/bash -# ========================================================== -# ProxMenux - LXC Mount Manager -# ========================================================== -# Author : MacRimi -# Copyright : (c) 2024 MacRimi -# License : MIT -# Version : 3.1-enhanced -# Last Updated: $(date +%d/%m/%Y) -# ========================================================== - -BASE_DIR="/usr/local/share/proxmenux" -source "$BASE_DIR/utils.sh" - -SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func" -if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then - SHARE_COMMON_LOADED=false -else - SHARE_COMMON_LOADED=true -fi - -load_language -initialize_cache - -# ========================================================== - -get_container_uid_shift() { - local ctid="$1" - local conf="/etc/pve/lxc/${ctid}.conf" - local uid_shift - - if [[ ! -f "$conf" ]]; then - echo "100000" - return 0 - fi - - local unpriv - unpriv=$(grep "^unprivileged:" "$conf" | awk '{print $2}') - - if [[ "$unpriv" == "1" ]]; then - uid_shift=$(grep "^lxc.idmap" "$conf" | grep 'u 0' | awk '{print $5}' | head -1) - echo "${uid_shift:-100000}" - return 0 - fi - - echo "0" - return 0 -} - -setup_container_access() { - local ctid="$1" group_name="$2" host_gid="$3" host_dir="$4" - local uid_shift mapped_gid - - if [[ ! "$ctid" =~ ^[0-9]+$ ]]; then - msg_error "$(translate 'Invalid container ID format:') $ctid" - return 1 - fi - - uid_shift=$(get_container_uid_shift "$ctid") - - - - - # =================================================================== - # CONTAINER TYPE DETECTION AND STRATEGY - # =================================================================== - - if [[ "$uid_shift" -eq 0 ]]; then - msg_ok "$(translate "PRIVILEGED container detected - using direct UID/GID mapping")" - mapped_gid="$host_gid" - container_type="privileged" - else - msg_ok "$(translate "UNPRIVILEGED container detected - using mapped UID/GID")" - mapped_gid=$((uid_shift + host_gid)) - container_type="unprivileged" - msg_ok "UID shift: $uid_shift, Host GID: $host_gid → Container GID: $mapped_gid" - - fi - - - - - - # =================================================================== - # STEP 1: ACL TOOLS (only for unprivileged containers) - # =================================================================== - - if [[ "$container_type" == "unprivileged" ]]; then - if ! command -v setfacl >/dev/null 2>&1; then - msg_info "$(translate "Installing ACL tools (REQUIRED for unprivileged containers)...")" - apt-get update >/dev/null 2>&1 - apt-get install -y acl >/dev/null 2>&1 - if command -v setfacl >/dev/null 2>&1; then - msg_ok "$(translate "ACL tools installed successfully")" - else - msg_error "$(translate "Failed to install ACL tools - permissions may not work correctly")" - fi - else - msg_ok "$(translate "ACL tools already available")" - fi - else - msg_ok "$(translate "Privileged container - ACL tools not required (using POSIX permissions)")" - fi - - - - - - # =================================================================== - # STEP 2: CONTAINER GROUP CONFIGURATION - # =================================================================== - - msg_info "$(translate "Configuring container group with") $container_type $(translate "strategy...")" - - pct exec "$ctid" -- sh -c " - # Remove existing group if GID is wrong - if getent group $group_name >/dev/null 2>&1; then - current_gid=\$(getent group $group_name | cut -d: -f3) - if [ \"\$current_gid\" != \"$mapped_gid\" ]; then - groupdel $group_name 2>/dev/null || true - fi - fi - - # Create group with correct GID - groupadd -g $mapped_gid $group_name 2>/dev/null || true - " 2>/dev/null - - msg_ok "$(translate "Container group configured:") $group_name (GID: $mapped_gid)" - - - - - - # =================================================================== - # STEP 3: USER PROCESSING (different strategies) - # =================================================================== - - local container_users - container_users=$(pct exec "$ctid" -- getent passwd | awk -F: '{print $1 ":" $3}' 2>/dev/null) - - local users_added=0 - local acls_applied=0 - - if [[ "$container_type" == "privileged" ]]; then - - - msg_ok "$(translate "Privileged container:") $users_added $(translate "users added to group (no ACLs needed)")" - - else - - msg_info "$(translate "Using UNPRIVILEGED strategy: mapped UIDs + ACL permissions")" - - while IFS=: read -r username ct_uid; do - if [[ -n "$username" && "$ct_uid" =~ ^[0-9]+$ ]]; then - local host_uid=$((uid_shift + ct_uid)) - - if pct exec "$ctid" -- usermod -aG "$group_name" "$username" 2>/dev/null; then - users_added=$((users_added + 1)) - - if command -v setfacl >/dev/null 2>&1; then - setfacl -m u:$host_uid:rwx "$host_dir" 2>/dev/null - setfacl -m d:u:$host_uid:rwx "$host_dir" 2>/dev/null - acls_applied=$((acls_applied + 1)) - fi - - case "$username" in - root|www-data|ncp|nobody|ubuntu|debian) - msg_ok "$(translate "Configured user:") $username (CT_UID:$ct_uid → HOST_UID:$host_uid)" - ;; - esac - fi - fi - done <<< "$container_users" - - msg_ok "$(translate "Unprivileged container:") $users_added $(translate "users added,") $acls_applied $(translate "ACL entries applied")" - fi - - - - - # =================================================================== - # STEP 4: DIRECTORY PERMISSIONS - # =================================================================== - msg_info "$(translate "Setting optimal directory permissions...")" - - chmod 2775 "$host_dir" 2>/dev/null || true - chgrp "$group_name" "$host_dir" 2>/dev/null || true - - msg_ok "$(translate "Host directory permissions:") 2775 root:$group_name" - - - - - - # =================================================================== - # STEP 5: VERIFICATION - # =================================================================== - msg_info "$(translate "Verifying configuration...")" - - if [[ "$container_type" == "unprivileged" ]] && command -v getfacl >/dev/null 2>&1; then - local acl_count=$(getfacl "$host_dir" 2>/dev/null | grep "^user:" | grep -v "^user::" | wc -l) - msg_ok "$(translate "ACL entries configured:") $acl_count" - - # Show sample ACL entries - if [[ $acl_count -gt 0 ]]; then - echo -e "${TAB}${BGN}$(translate " ACL entries:")${CL}" - getfacl "$host_dir" 2>/dev/null | grep "^user:" | grep -v "^user::" | head -3 | while read acl_line; do - echo -e "${TAB} ${BL}$acl_line${CL}" - done - fi - fi - - - local test_users=("www-data" "root" "ncp" "nobody") - local successful_tests=0 - - for test_user in "${test_users[@]}"; do - if pct exec "$ctid" -- id "$test_user" >/dev/null 2>&1; then - if pct exec "$ctid" -- su -s /bin/bash "$test_user" -c "ls '$4' >/dev/null 2>&1" 2>/dev/null; then - successful_tests=$((successful_tests + 1)) - fi - fi - done - - if [[ $successful_tests -gt 0 ]]; then - msg_ok "$(translate "Access verification:") $successful_tests $(translate "users can access mount point")" - fi - - - if [[ "$container_type" == "privileged" ]]; then - msg_ok "$(translate "PRIVILEGED container configuration completed - using direct POSIX permissions")" - else - msg_ok "$(translate "UNPRIVILEGED container configuration completed - using ACL permissions")" - fi - - return 0 -} - - - - - -get_next_mp_index() { - local ctid="$1" - local conf="/etc/pve/lxc/${ctid}.conf" - - if [[ ! "$ctid" =~ ^[0-9]+$ ]] || [[ ! -f "$conf" ]]; then - echo "0" - return 0 - fi - - local used idx next=0 - used=$(awk -F: '/^mp[0-9]+:/ {print $1}' "$conf" | sed 's/mp//' | sort -n) - for idx in $used; do - [[ "$idx" -ge "$next" ]] && next=$((idx+1)) - done - echo "$next" -} - - - - - - -add_bind_mount() { - local ctid="$1" host_path="$2" ct_path="$3" - local mpidx result - - if [[ ! "$ctid" =~ ^[0-9]+$ ]]; then - msg_error "$(translate 'Invalid container ID format:') $ctid" - return 1 - fi - - if [[ -z "$ctid" || -z "$host_path" || -z "$ct_path" ]]; then - msg_error "$(translate "Missing arguments")" - return 1 - fi - - if pct config "$ctid" | grep -q "$host_path"; then - echo -e - msg_warn "$(translate "Directory already mounted in container configuration.")" - echo -e "" - msg_success "$(translate 'Press Enter to return to menu...')" - read -r - return 1 - fi - - mpidx=$(get_next_mp_index "$ctid") - - result=$(pct set "$ctid" -mp${mpidx} "$host_path,mp=$ct_path,shared=1,backup=0,acl=1" 2>&1) - - if [[ $? -eq 0 ]]; then - msg_ok "$(translate "Successfully mounted:") $host_path → $ct_path" - return 0 - else - msg_error "$(translate "Error mounting folder:") $result" - return 1 - fi -} - - - - - - -mount_host_directory_to_lxc() { - - # Step 1: Select container - local container_id - container_id=$(select_lxc_container) - if [[ $? -ne 0 || -z "$container_id" ]]; then - return 1 - fi - - - show_proxmenux_logo - msg_title "$(translate 'Mount Host Directory to LXC Container')" - - # Step 1.1: Ensure running - ct_status=$(pct status "$container_id" | awk '{print $2}') - if [[ "$ct_status" != "running" ]]; then - - msg_info "$(translate "Starting container") $container_id..." - if pct start "$container_id"; then - sleep 3 - msg_ok "$(translate "Container started")" - else - msg_error "$(translate "Failed to start container")" - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r - return 1 - fi - fi - - msg_ok "$(translate 'Container selected and running')" - sleep 2 - - # Step 2: Select host directory - local host_dir - host_dir=$(select_host_directory) - if [[ -z "$host_dir" ]]; then - return 1 - fi - - msg_ok "$(translate 'Host directory selected')" - - # Step 3: Setup group - local group_name="sharedfiles" - local group_gid - group_gid=$(pmx_ensure_host_group "$group_name") - if [[ -z "$group_gid" ]]; then - return 1 - fi - - # Set basic permissions - chown -R root:"$group_name" "$host_dir" 2>/dev/null || true - chmod -R 2775 "$host_dir" 2>/dev/null || true - - msg_ok "$(translate 'Host group configured')" - - # Step 4: Select container mount point - - local ct_mount_point - ct_mount_point=$(select_container_mount_point "$container_id" "$host_dir") - if [[ -z "$ct_mount_point" ]]; then - return 1 - fi - - - - # Step 5: Confirmation - local uid_shift container_type - uid_shift=$(get_container_uid_shift "$container_id") - if [[ "$uid_shift" -eq 0 ]]; then - container_type="$(translate 'Privileged')" - else - container_type="$(translate 'Unprivileged')" - fi - - local confirm_msg="$(translate "Mount Configuration:") - -$(translate "Container ID:"): $container_id ($container_type) -$(translate "Host Directory:"): $host_dir -$(translate "Container Mount Point:"): $ct_mount_point -$(translate "Shared Group:"): $group_name (GID: $group_gid) - -$(translate "Proceed?")" - - if ! whiptail --title "$(translate "Confirm Mount")" --yesno "$confirm_msg" 16 70; then - return 1 - fi - - - - # Step 6: Add mount - if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then - return 1 - fi - - # Step 7: Setup access (handles both privileged and unprivileged) - setup_container_access "$container_id" "$group_name" "$group_gid" "$host_dir" - - # Step 8: Final setup - pct exec "$container_id" -- chgrp "$group_name" "$ct_mount_point" 2>/dev/null || true - pct exec "$container_id" -- chmod 2775 "$ct_mount_point" 2>/dev/null || true - - # Step 9: Summary - echo -e "" - echo -e "${TAB}${BOLD}$(translate 'Mount Added Successfully:')${CL}" - echo -e "${TAB}${BGN}$(translate 'Container:')${CL} ${BL}$container_id ($container_type)${CL}" - echo -e "${TAB}${BGN}$(translate 'Host Directory:')${CL} ${BL}$host_dir${CL}" - echo -e "${TAB}${BGN}$(translate 'Mount Point:')${CL} ${BL}$ct_mount_point${CL}" - echo -e "${TAB}${BGN}$(translate 'Group:')${CL} ${BL}$group_name (GID: $group_gid)${CL}" - - if [[ "$uid_shift" -eq 0 ]]; then - echo -e "${TAB}${BGN}$(translate 'Permission Strategy:')${CL} ${BL}POSIX (direct mapping)${CL}" - else - echo -e "${TAB}${BGN}$(translate 'Permission Strategy:')${CL} ${BL}ACL (mapped UIDs)${CL}" - fi - - echo -e "" - if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then - msg_info "$(translate 'Restarting container...')" - if pct reboot "$container_id"; then - sleep 5 - msg_ok "$(translate 'Container restarted successfully')" - - echo -e - echo -e "${TAB}${BOLD}$(translate 'Testing access and read/write:')${CL}" - test_user=$(pct exec "$container_id" -- sh -c "id -u ncp >/dev/null 2>&1 && echo ncp || echo www-data") - - if pct exec "$container_id" -- su -s /bin/bash $test_user -c "touch $ct_mount_point/test_access.txt" 2>/dev/null; then - msg_ok "$(translate "Mount access and read/write successful (tested as $test_user)")" - rm -f "$host_dir/test_access.txt" 2>/dev/null || true - else - msg_warn "$(translate "⚠ Access test failed - check permissions (user: $test_user)")" - fi - - else - msg_warn "$(translate 'Failed to restart - restart manually')" - fi - fi - - echo -e "" - msg_success "$(translate 'Press Enter to continue...')" - read -r -} - -# Main menu -main_menu() { - while true; do - choice=$(dialog --title "$(translate 'LXC Mount Manager')" \ - --menu "\n$(translate 'Choose an option:')" 25 80 15 \ - "1" "$(translate 'Mount Host Directory to LXC')" \ - "2" "$(translate 'View Mount Points')" \ - "3" "$(translate 'Remove Mount Point')" \ - "4" "$(translate 'Exit')" 3>&1 1>&2 2>&3) - - case $choice in - 1) - mount_host_directory_to_lxc - ;; - 2) - msg_info2 "$(translate 'Feature coming soon...')" - read -p "$(translate 'Press Enter to continue...')" - ;; - 3) - msg_info2 "$(translate 'Feature coming soon...')" - read -p "$(translate 'Press Enter to continue...')" - ;; - 4|"") - exit 0 - ;; - esac - done -} - -#main_menu -mount_host_directory_to_lxc diff --git a/scripts/vm/create_vm.sh b/scripts/vm/create_vm.sh deleted file mode 100644 index 193aca9a..00000000 --- a/scripts/vm/create_vm.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env bash - -# ========================================================== -# ProxMenuX - Virtual Machine Creator Script -# ========================================================== -# Author : MacRimi -# Copyright : (c) 2024 MacRimi -# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) -# Version : 1.0 -# Last Updated: 07/05/2025 -# ========================================================== -# Description: -# This script is part of the central ProxMenux VM creation module. It allows users -# to create virtual machines (VMs) in Proxmox VE using either default or advanced -# configurations, streamlining the deployment of Linux, Windows, and other systems. -# -# Key features: -# - Supports both virtual disk creation and physical disk passthrough. -# - Automates CPU, RAM, BIOS, network and storage configuration. -# - Provides a user-friendly menu to select OS type, ISO image and disk interface. -# - Automatically generates a detailed and styled HTML description for each VM. -# -# All operations are designed to simplify and accelerate VM creation in a -# consistent and maintainable way, using ProxMenux standards. -# ========================================================== - - - -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" -VM_REPO="$LOCAL_SCRIPTS/vm" -ISO_REPO="$LOCAL_SCRIPTS/vm" -MENU_REPO="$LOCAL_SCRIPTS/menus" -BASE_DIR="/usr/local/share/proxmenux" -UTILS_FILE="$BASE_DIR/utils.sh" -VENV_PATH="/opt/googletrans-env" - -[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE" - - -source "$VM_REPO/vm_configurator.sh" -source "$VM_REPO/disk_selector.sh" -source "$VM_REPO/vm_creator.sh" - - - -if [[ -f "$UTILS_FILE" ]]; then - source "$UTILS_FILE" -fi - -load_language -initialize_cache - - - -function header_info() { - clear - show_proxmenux_logo - echo -e "${BL}╔═══════════════════════════════════════════════╗${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}║${YWB} ProxMenux VM Creator ${BL}║${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}╚═══════════════════════════════════════════════╝${CL}" - echo -e -} - -# ========================================================== -# MAIN EXECUTION -# ========================================================== - -header_info -echo -e "\n Loading..." -sleep 1 - - - - -function start_vm_configuration() { - - if (whiptail --title "ProxMenux" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 60); then - header_info - load_default_vm_config "$OS_TYPE" - - if [[ -z "$HN" ]]; then - HN=$(whiptail --inputbox "$(translate "Enter a name for the new virtual machine:")" 10 60 --title "VM Hostname" 3>&1 1>&2 2>&3) - [[ -z "$HN" ]] && HN="custom-vm" - fi - - apply_default_vm_config - else - header_info - echo -e "${CUS}$(translate "Using advanced configuration")${CL}" - configure_vm_advanced "$OS_TYPE" - fi -} - - - -while true; do - OS_TYPE=$(dialog --backtitle "ProxMenux" \ - --title "$(translate "Select System Type")" \ - --menu "\n$(translate "Choose the type of virtual system to install:")" 18 70 10 \ - 1 "$(translate "Create") VM System NAS" \ - 2 "$(translate "Create") VM System Windows" \ - 3 "$(translate "Create") VM System Linux" \ - 4 "$(translate "Create") VM System macOS (OSX-PROXMOX)" \ - 5 "$(translate "Create") VM System Others (based Linux)" \ - 6 "$(translate "Return to Main Menu")" \ - 3>&1 1>&2 2>&3) - - - [[ $? -ne 0 || "$OS_TYPE" == "5" ]] && exec bash "$MENU_REPO/main_menu.sh" - - case "$OS_TYPE" in - 1) - source "$ISO_REPO/select_nas_iso.sh" && select_nas_iso || continue - ;; - 2) - source "$ISO_REPO/select_windows_iso.sh" && select_windows_iso || continue - ;; - 3) - source "$ISO_REPO/select_linux_iso.sh" && select_linux_iso || continue - ;; - 4) - whiptail --title "OSX-PROXMOX" --yesno "$(translate "This is an external script that creates a macOS VM in Proxmox VE in just a few steps, whether you are using AMD or Intel hardware.")\n\n$(translate "The script clones the osx-proxmox.com repository and once the setup is complete, the server will automatically reboot.")\n\n$(translate "Make sure there are no critical services running as they will be interrupted. Ensure your server can be safely rebooted.")\n\n$(translate "Visit https://osx-proxmox.com for more information.")\n\n$(translate "Do you want to run the script now?")" 20 70 - if [[ $? -eq 0 ]]; then - bash -c "$(curl -fsSL https://install.osx-proxmox.com)" - fi - continue - ;; - 5) - source "$ISO_REPO/select_linux_iso.sh" && select_linux_other_scripts || continue - ;; - esac - - - if ! confirm_vm_creation; then - continue - fi - - - start_vm_configuration || continue - - - select_disk_type - if [[ -z "$DISK_TYPE" ]]; then - msg_error "$(translate "Disk type selection failed or cancelled")" - continue - fi - - create_vm - break -done - - - - - -function start_vm_configuration() { - - if (whiptail --title "ProxMenux" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 60); then - header_info - load_default_vm_config "$OS_TYPE" - - if [[ -z "$HN" ]]; then - HN=$(whiptail --inputbox "$(translate "Enter a name for the new virtual machine:")" 10 60 --title "VM Hostname" 3>&1 1>&2 2>&3) - [[ -z "$HN" ]] && HN="custom-vm" - fi - - apply_default_vm_config - else - header_info - echo -e "${CUS}$(translate "Using advanced configuration")${CL}" - configure_vm_advanced "$OS_TYPE" - fi -} diff --git a/scripts/vm/disk_selector.sh b/scripts/vm/disk_selector.sh index 8716cf77..ae0ecc30 100644 --- a/scripts/vm/disk_selector.sh +++ b/scripts/vm/disk_selector.sh @@ -24,132 +24,169 @@ # consistent and maintainable way, using ProxMenux standards. # ========================================================== +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)" +LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts" BASE_DIR="/usr/local/share/proxmenux" -UTILS_FILE="$BASE_DIR/utils.sh" +LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT" +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 VENV_PATH="/opt/googletrans-env" 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 + load_language initialize_cache +VIRTUAL_DISKS=() +IMPORT_DISKS=() +CONTROLLER_NVME_PCIS=() +PASSTHROUGH_DISKS=() + +function _build_storage_plan_summary() { + local virtual_count="${#VIRTUAL_DISKS[@]}" + local import_count="${#IMPORT_DISKS[@]}" + local controller_count="${#CONTROLLER_NVME_PCIS[@]}" + local separator + local summary + separator="$(printf '%*s' 70 '' | tr ' ' '-')" + summary="$(translate "Current selection:")\n" + summary+=" - $(translate "Virtual disks"): $virtual_count\n" + summary+=" - $(translate "Import disks"): $import_count\n" + summary+=" - $(translate "Controllers + NVMe"): $controller_count\n" + summary+="${separator}\n\n" + echo -e "$summary" +} + function select_disk_type() { - DISK_TYPE=$(whiptail --backtitle "ProxMenux" --title "DISK TYPE" --menu "$(translate "Choose disk type:")" 12 58 2 \ - "virtual" "$(translate "Create virtual disk")" \ - "passthrough" "$(translate "Use physical disk passthrough")" \ - --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) + VIRTUAL_DISKS=() + IMPORT_DISKS=() + CONTROLLER_NVME_PCIS=() - [[ -z "$DISK_TYPE" ]] && return 1 + while true; do + local choice + choice=$(whiptail --backtitle "ProxMenux" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \ + "1" "$(translate "Add virtual disk")" \ + "2" "$(translate "Add import disk")" \ + "3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \ + "r" "$(translate "Reset current storage selection")" \ + "d" "$(translate "[ Finish and continue ]")" \ + --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || return 1 - if [[ "$DISK_TYPE" == "virtual" ]]; then - select_virtual_disk - else - select_passthrough_disk - fi + case "$choice" in + 1) + select_virtual_disk + ;; + 2) + select_import_disk + ;; + 3) + select_controller_nvme + ;; + r) + VIRTUAL_DISKS=() + IMPORT_DISKS=() + CONTROLLER_NVME_PCIS=() + ;; + d|done) + if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then + continue + fi + if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then + msg_ok "$(translate "Virtual Disks Created:")" + for i in "${!VIRTUAL_DISKS[@]}"; do + echo -e "${TAB}${BL}- $(translate "Disk") $((i+1)): ${VIRTUAL_DISKS[$i]}GB${CL}" + done + fi + if [[ ${#IMPORT_DISKS[@]} -gt 0 ]]; then + msg_ok "$(translate "Import Disks Selected:")" + for i in "${!IMPORT_DISKS[@]}"; do + local disk_info + disk_info=$(lsblk -ndo MODEL,SIZE "${IMPORT_DISKS[$i]}" 2>/dev/null | xargs) + echo -e "${TAB}${BL}- $(translate "Disk") $((i+1)): ${IMPORT_DISKS[$i]}${disk_info:+ ($disk_info)}${CL}" + done + fi + if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then + msg_ok "$(translate "Controllers + NVMe Selected:")" + for i in "${!CONTROLLER_NVME_PCIS[@]}"; do + local pci_info + pci_info=$(lspci -nn -s "${CONTROLLER_NVME_PCIS[$i]#0000:}" 2>/dev/null | sed 's/^[^ ]* //') + echo -e "${TAB}${BL}- $(translate "Controller") $((i+1)): ${CONTROLLER_NVME_PCIS[$i]}${pci_info:+ ($pci_info)}${CL}" + done + fi + PASSTHROUGH_DISKS=("${IMPORT_DISKS[@]}") + DISK_TYPE="mixed" + export DISK_TYPE VIRTUAL_DISKS IMPORT_DISKS CONTROLLER_NVME_PCIS PASSTHROUGH_DISKS + return 0 + ;; + esac + done } # ========================================================== # Select Virtual Disks # ========================================================== function select_virtual_disk() { - - VIRTUAL_DISKS=() - - - local add_more_disks=true - while $add_more_disks; do - msg_info "Detecting available storage volumes..." + local STORAGE_MENU=() + local TAG TYPE FREE ITEM + while read -r line; do + TAG=$(echo $line | awk '{print $1}') + TYPE=$(echo $line | awk '{print $2}') + FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf( "%9sB", $6)}') + ITEM=$(printf "%-15s %-10s %-15s" "$TAG" "$TYPE" "$FREE") + STORAGE_MENU+=("$TAG" "$ITEM" "OFF") + done < <(pvesm status -content images | awk 'NR>1') - STORAGE_MENU=() - while read -r line; do - TAG=$(echo $line | awk '{print $1}') - TYPE=$(echo $line | awk '{print $2}') - FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf( "%9sB", $6)}') - ITEM=$(printf "%-15s %-10s %-15s" "$TAG" "$TYPE" "$FREE") - STORAGE_MENU+=("$TAG" "$ITEM" "OFF") - done < <(pvesm status -content images | awk 'NR>1') - - - VALID=$(pvesm status -content images | awk 'NR>1') - if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." - sleep 2 - select_disk_type - fi - - - - if [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then - STORAGE=${STORAGE_MENU[0]} - msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." - else - - kill $SPINNER_PID > /dev/null - STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "Select Storage Volume")" --radiolist \ - "$(translate "Choose the storage volume for the virtual disk:\n")" 20 78 10 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) - - if [ $? -ne 0 ] || [ -z "$STORAGE" ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "No storage selected. At least one disk is required." - select_disk_type - else - add_more_disks=false - continue - fi - fi - - - fi - - - DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) - - if [ $? -ne 0 ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "Disk size not specified. At least one disk is required." - sleep 2 - select_disk_type - - else - add_more_disks=false - continue - fi - fi - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="32" - fi - - - VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") - - - - if ! whiptail --backtitle "ProxMenuX" --title "$(translate "Add Another Disk")" \ - --yesno "$(translate "Do you want to add another virtual disk?")" 8 58; then - add_more_disks=false - fi - done - - - if [ ${#VIRTUAL_DISKS[@]} -gt 0 ]; then - - msg_ok "Virtual Disks Created:" - for i in "${!VIRTUAL_DISKS[@]}"; do - echo -e "${TAB}${BL}- Disk $((i+1)): ${VIRTUAL_DISKS[$i]}GB${CL}" - done + local VALID + VALID=$(pvesm status -content images | awk 'NR>1') + if [ -z "$VALID" ]; then + msg_error "Unable to detect a valid storage location." + return 1 fi + local STORAGE + if [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then + STORAGE=${STORAGE_MENU[0]} + else + [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" >/dev/null 2>&1 + STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "Select Storage Volume")" --radiolist \ + "$(translate "Choose the storage volume for the virtual disk:\n")" 20 78 10 \ + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) + + if [ $? -ne 0 ] || [ -z "$STORAGE" ]; then + return 0 + fi + fi + + local DISK_SIZE + cleanup + DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) + if [ $? -ne 0 ]; then + return 0 + fi + + if [ -z "$DISK_SIZE" ]; then + DISK_SIZE="32" + fi + VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") export VIRTUAL_DISKS - - } # ========================================================== @@ -160,138 +197,164 @@ function select_virtual_disk() { # ========================================================== -# Select Physical Disks +# Select Import Disks # ========================================================== -function select_passthrough_disk() { - +function select_import_disk() { msg_info "$(translate "Detecting available disks...")" - FREE_DISKS=() - - USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}') - MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}') - - ZFS_DISKS="" - ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror') - - for entry in $ZFS_RAW; do - path="" - if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then - if [ -e "/dev/disk/by-id/$entry" ]; then - path=$(readlink -f "/dev/disk/by-id/$entry") - fi - elif [[ "$entry" == /dev/* ]]; then - path="$entry" - fi - - if [ -n "$path" ]; then - base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null) - if [ -n "$base_disk" ]; then - ZFS_DISKS+="/dev/$base_disk"$'\n' - fi - fi - done - - ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u) - LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u) - - RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u) - + _refresh_host_storage_cache + local FREE_DISKS=() + local DISK INFO MODEL SIZE LABEL DESCRIPTION while read -r DISK; do - [[ "$DISK" =~ /dev/zd ]] && continue + [[ "$DISK" =~ /dev/zd ]] && continue + if _disk_is_host_system_used "$DISK"; then + continue + fi - INFO=($(lsblk -dn -o MODEL,SIZE "$DISK")) - MODEL="${INFO[@]::${#INFO[@]}-1}" - SIZE="${INFO[-1]}" - LABEL="" - SHOW_DISK=true + INFO=($(lsblk -dn -o MODEL,SIZE "$DISK")) + MODEL="${INFO[@]::${#INFO[@]}-1}" + SIZE="${INFO[-1]}" + LABEL="" - IS_MOUNTED=false - IS_RAID=false - IS_ZFS=false - IS_LVM=false + if _disk_used_in_guest_configs "$DISK"; then + LABEL+=" [⚠ $(translate "In use by VM/LXC config")]" + fi - while read -r part fstype; do - [[ "$fstype" == "zfs_member" ]] && IS_ZFS=true - [[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true - [[ "$fstype" == "LVM2_member" ]] && IS_LVM=true - if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then - IS_MOUNTED=true - fi - done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2) - - REAL_PATH=$(readlink -f "$DISK") - if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then - IS_MOUNTED=true - fi - - USED_BY="" - REAL_PATH=$(readlink -f "$DISK") - CONFIG_DATA=$(cat /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null) - - if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then - USED_BY="⚠ $(translate "In use")" - else - for SYMLINK in /dev/disk/by-id/*; do - if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then - if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then - USED_BY="⚠ $(translate "In use")" - break - fi - fi - done - fi - - if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)" && grep -q "active raid" /proc/mdstat; then - SHOW_DISK=false - fi - - if $IS_ZFS || $IS_MOUNTED || [[ "$ZFS_DISKS" == *"$DISK"* ]]; then - SHOW_DISK=false - fi - - if $SHOW_DISK; then - [[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]" - [[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID" - [[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM" - [[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS" - DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") - FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") - fi + DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") + if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then + FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON") + else + FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") + fi done < <(lsblk -dn -e 7,11 -o PATH) - - if [ "${#FREE_DISKS[@]}" -eq 0 ]; then + if [[ "${#FREE_DISKS[@]}" -eq 0 ]]; then cleanup - whiptail --title "Error" --msgbox "$(translate "No disks available for this VM.")" 8 40 - select_disk_type - return + whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70 + return 1 fi + local MAX_WIDTH TOTAL_WIDTH SELECTED_DISKS MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1) - TOTAL_WIDTH=$((MAX_WIDTH + 20)) - [ $TOTAL_WIDTH -lt 50 ] && TOTAL_WIDTH=50 + TOTAL_WIDTH=$((MAX_WIDTH + 20)) + [[ $TOTAL_WIDTH -lt 50 ]] && TOTAL_WIDTH=50 cleanup - SELECTED_DISKS=$(whiptail --title "Select Disks" --checklist \ - "$(translate "Select the disks you want to use (use spacebar to select):")" 20 $TOTAL_WIDTH 10 \ + SELECTED_DISKS=$(whiptail --title "Select Import Disks" --checklist \ + "$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 $TOTAL_WIDTH 10 \ "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) - if [ -z "$SELECTED_DISKS" ]; then - msg_error "Disk not specified. At least one disk is required." - sleep 2 - select_disk_type - return - fi + [[ $? -ne 0 ]] && return 1 - - msg_ok "Disk passthrough selected:" - PASSTHROUGH_DISKS=() + IMPORT_DISKS=() + local DISK_INFO for DISK in $(echo "$SELECTED_DISKS" | tr -d '"'); do - DISK_INFO=$(lsblk -ndo MODEL,SIZE "$DISK" | xargs) - echo -e "${TAB}${CL}${BL}- $DISK $DISK_INFO${GN}${CL}" - PASSTHROUGH_DISKS+=("$DISK") + _array_contains "$DISK" "${IMPORT_DISKS[@]}" || IMPORT_DISKS+=("$DISK") done - + if [[ ${#IMPORT_DISKS[@]} -eq 0 ]]; then + msg_warn "$(translate "No import disks selected for now.")" + return 0 + fi + + export IMPORT_DISKS + return 0 } -# ========================================================== \ No newline at end of file + +function select_passthrough_disk() { + select_import_disk +} + +function select_controller_nvme() { + msg_info "$(translate "Detecting PCI storage controllers and NVMe devices...")" + + _refresh_host_storage_cache + + local menu_items=() + local blocked_report="" + local pci_path pci_full class_hex name controller_disks controller_desc disk reason safe_count blocked_count state + safe_count=0 + blocked_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 + + 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") + + reason="" + for disk in "${controller_disks[@]}"; do + if _disk_is_host_system_used "$disk"; then + reason+="${disk} (${DISK_USAGE_REASON}); " + elif _disk_used_in_guest_configs "$disk"; then + reason+="${disk} ($(translate "In use by VM/LXC config")); " + fi + done + + if [[ -n "$reason" ]]; then + blocked_count=$((blocked_count + 1)) + blocked_report+=" • ${pci_full} — ${name}\n $(translate "Blocked because protected/in-use disks are attached"): ${reason}\n" + continue + fi + + if [[ ${#controller_disks[@]} -gt 0 ]]; then + controller_desc="$(printf "%-50s [%s]" "$name" "$(IFS=,; echo "${controller_disks[*]}")")" + else + controller_desc="$(printf "%-50s [%s]" "$name" "$(translate "No attached disks detected")")" + fi + + if _array_contains "$pci_full" "${CONTROLLER_NVME_PCIS[@]}"; then + state="ON" + else + state="OFF" + fi + + 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 + msg="$(translate "No safe controllers/NVMe devices are available for passthrough.")\n\n" + if [[ $blocked_count -gt 0 ]]; then + msg+="$(translate "Detected controllers blocked for safety:")\n\n${blocked_report}" + fi + whiptail --title "Controller + NVMe" --msgbox "$msg" 20 90 + return 1 + fi + + if [[ $blocked_count -gt 0 ]]; then + whiptail --title "Controller + NVMe" --msgbox "$(translate "Some controllers were hidden because they have host system disks attached.")\n\n${blocked_report}" 20 90 + fi + + local selected + selected=$(whiptail --title "Controller + NVMe" --checklist \ + "$(translate "Select controllers/NVMe to passthrough (safe devices only):")" 20 90 10 \ + "${menu_items[@]}" 3>&1 1>&2 2>&3) + + [[ $? -ne 0 ]] && return 1 + + CONTROLLER_NVME_PCIS=() + local pci + for pci in $(echo "$selected" | tr -d '"'); do + _array_contains "$pci" "${CONTROLLER_NVME_PCIS[@]}" || CONTROLLER_NVME_PCIS+=("$pci") + done + + if [[ ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then + msg_warn "$(translate "No Controller/NVMe selected for now.")" + return 0 + fi + + export CONTROLLER_NVME_PCIS + return 0 +} +# ========================================================== diff --git a/scripts/vm/select_linux_iso.sh b/scripts/vm/select_linux_iso.sh index 43f6333e..850ae4e7 100644 --- a/scripts/vm/select_linux_iso.sh +++ b/scripts/vm/select_linux_iso.sh @@ -101,27 +101,27 @@ function select_linux_iso() { function select_linux_iso_official() { DISTROS=( - "Ubuntu 25.04|Desktop|ProxMenux|https://releases.ubuntu.com/25.04/ubuntu-25.04-desktop-amd64.iso" - "Ubuntu 24.04|Desktop|ProxMenux|https://releases.ubuntu.com/24.04/ubuntu-24.04.2-desktop-amd64.iso" + "Ubuntu 25.10|Desktop|ProxMenux|https://releases.ubuntu.com/25.10/ubuntu-25.10-desktop-amd64.iso" + "Ubuntu 24.04|Desktop|ProxMenux|https://releases.ubuntu.com/24.04/ubuntu-24.04.4-desktop-amd64.iso" "Ubuntu 22.04|Desktop|ProxMenux|https://releases.ubuntu.com/22.04/ubuntu-22.04.5-desktop-amd64.iso" "Ubuntu 20.04|Desktop|ProxMenux|https://releases.ubuntu.com/20.04/ubuntu-20.04.6-desktop-amd64.iso" - "Ubuntu 25.04 Server|CLI|ProxMenux|https://releases.ubuntu.com/25.04/ubuntu-25.04-live-server-amd64.iso" - "Ubuntu 24.04 Server|CLI|ProxMenux|https://releases.ubuntu.com/24.04/ubuntu-24.04.2-live-server-amd64.iso" + "Ubuntu 25.10 Server|CLI|ProxMenux|https://releases.ubuntu.com/25.10/ubuntu-25.10-live-server-amd64.iso" + "Ubuntu 24.04 Server|CLI|ProxMenux|https://releases.ubuntu.com/24.04/ubuntu-24.04.4-live-server-amd64.iso" "Ubuntu 22.04 Server|CLI|ProxMenux|https://releases.ubuntu.com/22.04/ubuntu-22.04.5-live-server-amd64.iso" "Ubuntu 20.04 Server|CLI|ProxMenux|https://releases.ubuntu.com/20.04/ubuntu-20.04.6-live-server-amd64.iso" - "Debian 13|Desktop|ProxMenux|https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd/debian-13.0.0-amd64-DVD-1.iso" - "Debian 12|Desktop|ProxMenux|https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd/debian-12.10.0-amd64-DVD-1.iso" + "Debian 13|Desktop|ProxMenux|https://cdimage.debian.org/debian-cd/current/amd64/iso-dvd/debian-13.4.0-amd64-DVD-1.iso" + "Debian 12|Desktop|ProxMenux|https://cdimage.debian.org/cdimage/archive/12.13.0/amd64/iso-dvd/debian-12.13.0-amd64-DVD-1.iso" "Debian 11|Desktop|ProxMenux|https://cdimage.debian.org/cdimage/archive/11.11.0/amd64/iso-dvd/debian-11.11.0-amd64-DVD-1.iso" - "Debian 13 Netinst|CLI|ProxMenux|https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.0.0-amd64-netinst.iso" - "Debian 12 Netinst|CLI|ProxMenux|https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.10.0-amd64-netinst.iso" + "Debian 13 Netinst|CLI|ProxMenux|https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.4.0-amd64-netinst.iso" + "Debian 12 Netinst|CLI|ProxMenux|https://cdimage.debian.org/cdimage/archive/12.13.0/amd64/iso-cd/debian-12.13.0-amd64-netinst.iso" "Debian 11 Netinst|CLI|ProxMenux|https://cdimage.debian.org/cdimage/archive/11.11.0/amd64/iso-cd/debian-11.11.0-amd64-netinst.iso" "Fedora Workstation 42|Desktop|ProxMenux|https://download.fedoraproject.org/pub/fedora/linux/releases/42/Workstation/x86_64/iso/Fedora-Workstation-Live-42-1.1.x86_64.iso" - "Arch Linux|CLI|ProxMenux|https://geo.mirror.pkgbuild.com/iso/2025.07.01/archlinux-2025.07.01-x86_64.iso" - "Rocky Linux 9.5|Desktop|ProxMenux|https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9.5-x86_64-dvd.iso" + "Arch Linux|CLI|ProxMenux|https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso" + "Rocky Linux 9|Desktop|ProxMenux|https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9-latest-x86_64-dvd.iso" "Linux Mint 22.1|Desktop|ProxMenux|https://mirrors.edge.kernel.org/linuxmint/stable/22.1/linuxmint-22.1-cinnamon-64bit.iso" "openSUSE Leap 15.6|Desktop|ProxMenux|https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-DVD-x86_64-Media.iso" "Alpine Linux 3.21|Desktop|ProxMenux|https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/x86_64/alpine-virt-3.21.3-x86_64.iso" - "Kali Linux 2025.1|Desktop|ProxMenux|https://cdimage.kali.org/kali-2025.1c/kali-linux-2025.1c-installer-amd64.iso" + "Kali Linux 2026.1|Desktop|ProxMenux|https://cdimage.kali.org/kali-2026.1/kali-linux-2026.1-installer-amd64.iso" "Manjaro 25.0|Desktop|ProxMenux|https://download.manjaro.org/gnome/25.0.0/manjaro-gnome-25.0.0-250414-linux612.iso" ) @@ -219,7 +219,7 @@ function select_linux_cloudinit() { whiptail --title "Proxmox VE Helper-Scripts" \ --msgbox "$(translate "Visit the website to discover more scripts, stay updated with the latest updates, and support the project:\n\nhttps://community-scripts.github.io/ProxmoxVE")" 15 70 - exec bash "$LOCAL_SCRIPTS/vm/create_vm.sh" + exec bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" } @@ -311,5 +311,3 @@ return 1 - - diff --git a/scripts/vm/select_nas_iso.sh b/scripts/vm/select_nas_iso.sh index 1b3731c0..87754201 100644 --- a/scripts/vm/select_nas_iso.sh +++ b/scripts/vm/select_nas_iso.sh @@ -7,7 +7,7 @@ # Copyright : (c) 2024 MacRimi # License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) # Version : 1.0 -# Last Updated: 07/05/2025 +# Last Updated: 04/04/2026 # ========================================================== # Description: # This script is part of the central ProxMenux VM creation module. It allows users @@ -24,11 +24,20 @@ # consistent and maintainable way, using ProxMenux standards. # ========================================================== - +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="$BASE_DIR/utils.sh" +UTILS_FILE="$LOCAL_SCRIPTS/utils.sh" VENV_PATH="/opt/googletrans-env" -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" + +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 [[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE" load_language @@ -37,6 +46,138 @@ initialize_cache ISO_DIR="/var/lib/vz/template/iso" mkdir -p "$ISO_DIR" +function _has_curl() { + command -v curl >/dev/null 2>&1 +} + +function _latest_version_from_lines() { + awk 'NF' | sort -V | tail -n 1 +} + +function resolve_truenas_scale_iso() { + local default_ver="25.10.2.1" + local base_url="https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye" + local detected_ver="" + + if _has_curl; then + detected_ver=$( + curl -fsSL "${base_url}/" 2>/dev/null \ + | grep -Eo '>[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?/' \ + | tr -d '>/' \ + | _latest_version_from_lines + ) + fi + + [[ -z "$detected_ver" ]] && detected_ver="$default_ver" + + ISO_NAME="TrueNAS SCALE ${detected_ver} (Goldeye)" + ISO_FILE="TrueNAS-SCALE-${detected_ver}.iso" + ISO_URL="${base_url}/${detected_ver}/${ISO_FILE}" + ISO_PATH="$ISO_DIR/$ISO_FILE" + HN="TrueNAS-Scale" +} + +function resolve_truenas_core_iso() { + local default_file="TrueNAS-13.3-U1.2.iso" + local detected_file="" + local base_url="https://download.freenas.org/13.3/STABLE/latest/x64" + + if _has_curl; then + detected_file=$( + curl -fsSL "${base_url}/" 2>/dev/null \ + | grep -Eo 'TrueNAS-13\.3-[^"]+\.iso' \ + | head -n 1 + ) + fi + + [[ -z "$detected_file" ]] && detected_file="$default_file" + + ISO_NAME="TrueNAS CORE 13.3" + ISO_FILE="$detected_file" + ISO_URL="${base_url}/${ISO_FILE}" + ISO_PATH="$ISO_DIR/$ISO_FILE" + HN="TrueNAS-Core" +} + +function resolve_omv_iso() { + local default_ver="8.1.1" + local detected_ver="" + + if _has_curl; then + detected_ver=$( + curl -fsSL "https://sourceforge.net/projects/openmediavault/files/iso/" 2>/dev/null \ + | grep -Eo '/projects/openmediavault/files/iso/[0-9]+\.[0-9]+\.[0-9]+/' \ + | sed -E 's|.*/iso/([0-9]+\.[0-9]+\.[0-9]+)/$|\1|' \ + | _latest_version_from_lines + ) + fi + + [[ -z "$detected_ver" ]] && detected_ver="$default_ver" + + ISO_NAME="OpenMediaVault ${detected_ver}" + ISO_FILE="openmediavault_${detected_ver}-amd64.iso" + ISO_URL="https://sourceforge.net/projects/openmediavault/files/iso/${detected_ver}/${ISO_FILE}/download" + ISO_PATH="$ISO_DIR/$ISO_FILE" + HN="OpenMediaVault" +} + +function resolve_xigmanas_iso() { + local default_train="14.3.0.5" + local default_build="14.3.0.5.10566" + local detected_train="" + local detected_build="" + + if _has_curl; then + detected_train=$( + curl -fsSL "https://sourceforge.net/projects/xigmanas/files/" 2>/dev/null \ + | grep -Eo '/projects/xigmanas/files/XigmaNAS-[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/' \ + | sed -E 's|.*/XigmaNAS-([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/$|\1|' \ + | _latest_version_from_lines + ) + fi + + [[ -z "$detected_train" ]] && detected_train="$default_train" + + if _has_curl; then + detected_build=$( + curl -fsSL "https://sourceforge.net/projects/xigmanas/files/XigmaNAS-${detected_train}/" 2>/dev/null \ + | grep -Eo "/projects/xigmanas/files/XigmaNAS-${detected_train}/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/" \ + | sed -E "s|.*/XigmaNAS-${detected_train}/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/$|\\1|" \ + | _latest_version_from_lines + ) + fi + + [[ -z "$detected_build" ]] && detected_build="$default_build" + + ISO_NAME="XigmaNAS-${detected_train}" + ISO_FILE="XigmaNAS-x64-LiveCD-${detected_build}.iso" + ISO_URL="https://sourceforge.net/projects/xigmanas/files/XigmaNAS-${detected_train}/${detected_build}/${ISO_FILE}/download" + ISO_PATH="$ISO_DIR/$ISO_FILE" + HN="XigmaNAS" +} + +function resolve_rockstor_iso() { + local default_file="Rockstor-Leap15.6-generic.x86_64-5.0.15-0.install.iso" + local detected_file="" + local base_url="https://rockstor.com/downloads/installer/leap/15.6/x86_64" + + if _has_curl; then + detected_file=$( + curl -fsSL "${base_url}/" 2>/dev/null \ + | grep -Eo 'Rockstor-Leap15\.6-generic\.x86_64-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.install\.iso' \ + | _latest_version_from_lines + ) + fi + + [[ -z "$detected_file" ]] && detected_file="$default_file" + + ISO_NAME="Rockstor" + ISO_FILE="$detected_file" + ISO_URL="${base_url}/${ISO_FILE}" + ISO_PATH="$ISO_DIR/$ISO_FILE" + HN="Rockstor" +} + function select_nas_iso() { local NAS_OPTIONS=( @@ -68,39 +209,19 @@ function select_nas_iso() { return 1 ;; 2) - ISO_NAME="TrueNAS SCALE 25 (Goldeye)" - ISO_URL="https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye/25.10.0.1/TrueNAS-SCALE-25.10.0.1.iso" - ISO_FILE="TrueNAS-SCALE-25.10.0.1.iso" - ISO_PATH="$ISO_DIR/$ISO_FILE" - HN="TrueNAS-Scale" + resolve_truenas_scale_iso ;; 3) - ISO_NAME="TrueNAS CORE 13.3" - ISO_URL="https://download.freenas.org/13.3/STABLE/latest/x64/TrueNAS-13.3-U1.2.iso" - ISO_FILE="TrueNAS-13.3-U1.2.iso" - ISO_PATH="$ISO_DIR/$ISO_FILE" - HN="TrueNAS-Core" + resolve_truenas_core_iso ;; 4) - ISO_NAME="OpenMediaVault 7.4.17" - ISO_URL="https://sourceforge.net/projects/openmediavault/files/iso/7.4.17/openmediavault_7.4.17-amd64.iso/download" - ISO_FILE="openmediavault_7.4.17-amd64.iso" - ISO_PATH="$ISO_DIR/$ISO_FILE" - HN="OpenMediaVault" + resolve_omv_iso ;; 5) - ISO_NAME="XigmaNAS-14.3.0.5" - ISO_URL="https://sourceforge.net/projects/xigmanas/files/XigmaNAS-14.3.0.5/14.3.0.5.10566/XigmaNAS-x64-LiveCD-14.3.0.5.10566.iso/download" - ISO_FILE="XigmaNAS-x64-LiveCD-14.3.0.5.10566.iso" - ISO_PATH="$ISO_DIR/$ISO_FILE" - HN="XigmaNAS" + resolve_xigmanas_iso ;; 6) - ISO_NAME="Rockstor" - ISO_URL="https://rockstor.com/downloads/installer/leap/15.6/x86_64/Rockstor-Leap15.6-generic.x86_64-5.0.15-0.install.iso" - ISO_FILE="Rockstor-Leap15.6-generic.x86_64-5.0.15-0.install.iso" - ISO_PATH="$ISO_DIR/$ISO_FILE" - HN="Rockstor" + resolve_rockstor_iso ;; 7) bash "$LOCAL_SCRIPTS/vm/zimaos.sh" diff --git a/scripts/vm/synology.sh b/scripts/vm/synology.sh index 8f3b5b0e..3d0bd0db 100644 --- a/scripts/vm/synology.sh +++ b/scripts/vm/synology.sh @@ -29,14 +29,33 @@ # Configuration ============================================ -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +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="$BASE_DIR/utils.sh" +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 VENV_PATH="/opt/googletrans-env" 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 load_language initialize_cache # ========================================================== @@ -51,6 +70,10 @@ NEXTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100") NAME="Synology VM" IMAGES_DIR="/var/lib/vz/template/iso" ERROR_FLAG=false +WIZARD_ADD_GPU="no" +WIZARD_GPU_RESULT="not_requested" +VM_WIZARD_CAPTURE_FILE="" +VM_WIZARD_CAPTURE_ACTIVE=0 @@ -72,15 +95,99 @@ function exit_script() { function header_info() { clear show_proxmenux_logo - echo -e "${BL}╔═══════════════════════════════════════════════╗${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}║${YWB} Synology VM Creator ${BL}║${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}╚═══════════════════════════════════════════════╝${CL}" - echo -e + msg_title "Synology VM Creator" } # ========================================================== +function start_vm_wizard_capture() { + [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]] && return 0 + VM_WIZARD_CAPTURE_FILE="/tmp/proxmenux_synology_vm_wizard_capture_$$.txt" + : >"$VM_WIZARD_CAPTURE_FILE" + exec 8>&1 + exec > >(tee -a "$VM_WIZARD_CAPTURE_FILE") + VM_WIZARD_CAPTURE_ACTIVE=1 +} + +function stop_vm_wizard_capture() { + if [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]]; then + exec 1>&8 + exec 8>&- + VM_WIZARD_CAPTURE_ACTIVE=0 + fi + if [[ -n "${VM_WIZARD_CAPTURE_FILE:-}" && -f "$VM_WIZARD_CAPTURE_FILE" ]]; then + rm -f "$VM_WIZARD_CAPTURE_FILE" + fi + VM_WIZARD_CAPTURE_FILE="" +} + +function replay_vm_wizard_capture() { + if [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]]; then + stop_spinner + exec 1>&8 + exec 8>&- + VM_WIZARD_CAPTURE_ACTIVE=0 + fi + + if [[ -n "${VM_WIZARD_CAPTURE_FILE:-}" && -f "$VM_WIZARD_CAPTURE_FILE" ]]; then + show_proxmenux_logo + cat "$VM_WIZARD_CAPTURE_FILE" + rm -f "$VM_WIZARD_CAPTURE_FILE" + fi + VM_WIZARD_CAPTURE_FILE="" +} + +function has_usable_gpu_for_vm_passthrough() { + lspci -nn 2>/dev/null \ + | grep -iE "VGA compatible controller|3D controller|Display controller" \ + | grep -ivE "Ethernet|Network|Audio" \ + | grep -ivE "ASPEED|AST[0-9]{3,4}|Matrox|G200e|BMC" \ + | grep -q . +} + +function prompt_optional_gpu_passthrough() { + WIZARD_ADD_GPU="no" + if has_usable_gpu_for_vm_passthrough; then + if whiptail --backtitle "ProxMenuX" --title "$(translate "Optional GPU Passthrough")" \ + --yesno "$(translate "Do you want to configure GPU passthrough for this VM now?")\n\n$(translate "This will launch the GPU assistant after VM creation and may require a host reboot.")" 12 78 --defaultno; then + WIZARD_ADD_GPU="yes" + fi + else + msg_warn "$(translate "No compatible GPU detected for VM passthrough. Skipping GPU wizard option.")" + fi + export WIZARD_ADD_GPU +} + +function run_gpu_passthrough_wizard() { + [[ "${WIZARD_ADD_GPU:-no}" != "yes" ]] && return 0 + + local gpu_script="$LOCAL_SCRIPTS/gpu_tpu/add_gpu_vm.sh" + local local_gpu_script + local wizard_result_file="" + + if [[ ! -f "$gpu_script" ]]; then + local_gpu_script="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/gpu_tpu/add_gpu_vm.sh" + [[ -f "$local_gpu_script" ]] && gpu_script="$local_gpu_script" + fi + + if [[ ! -f "$gpu_script" ]]; then + msg_warn "$(translate "GPU passthrough assistant not found. You can run it later from Hardware Graphics.")" + WIZARD_GPU_RESULT="cancelled" + return 0 + fi + + msg_info2 "$(translate "Launching GPU passthrough assistant for VM") ${VMID}..." + wizard_result_file="/tmp/proxmenux_gpu_wizard_result_${VMID}_$$.txt" + : >"$wizard_result_file" + bash "$gpu_script" --vmid "$VMID" --wizard --result-file "$wizard_result_file" + + if [[ -s "$wizard_result_file" ]]; then + WIZARD_GPU_RESULT=$(head -n1 "$wizard_result_file" | tr -d '\r\n') + else + WIZARD_GPU_RESULT="cancelled" + fi + rm -f "$wizard_result_file" +} + @@ -93,12 +200,13 @@ function start_script() { if (whiptail --backtitle "ProxMenuX" --title "SETTINGS" --yesno "$(translate "Use Default Settings?")" --no-button Advanced 10 58); then header_info echo -e "${DEF}Using Default Settings${CL}" - default_settings + default_settings || return 1 else header_info echo -e "${CUS}Using Advanced Settings${CL}" - advanced_settings + advanced_settings || return 1 fi + return 0 } # ========================================================== @@ -142,7 +250,7 @@ function default_settings() { echo -e "${DEF}Creating a $NAME using the above default settings${CL}" sleep 1 - select_disk_type + select_disk_type || return 1 } # ========================================================== @@ -319,7 +427,7 @@ function advanced_settings() { echo -e echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}" sleep 1 - select_disk_type + select_disk_type || return 1 else header_info sleep 1 @@ -336,285 +444,253 @@ function advanced_settings() { # ========================================================== # Select Disk # ========================================================== -function select_disk_type() { +VIRTUAL_DISKS=() +IMPORT_DISKS=() +CONTROLLER_NVME_PCIS=() +PASSTHROUGH_DISKS=() - DISK_TYPE=$(whiptail --backtitle "ProxMenuX" --title "DISK TYPE" --menu "$(translate "Choose disk type:")" 12 58 2 \ - "virtual" "$(translate "Create virtual disk")" \ - "passthrough" "$(translate "Use physical disk passthrough")" \ - --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) - - EXIT_STATUS=$? - - if [[ $EXIT_STATUS -ne 0 ]]; then - clear - header_info - msg_error "Operation cancelled by user. Returning to start scrip..." - sleep 2 - if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then - start_script - else - clear - exit - fi - fi - - if [[ "$DISK_TYPE" == "virtual" ]]; then - select_virtual_disk - else - select_passthrough_disk - fi +function _build_storage_plan_summary() { + local separator + local summary + separator="$(printf '%*s' 70 '' | tr ' ' '-')" + summary="$(translate "Current selection:")\n" + summary+=" - $(translate "Virtual disks"): ${#VIRTUAL_DISKS[@]}\n" + summary+=" - $(translate "Import disks"): ${#IMPORT_DISKS[@]}\n" + summary+=" - $(translate "Controllers + NVMe"): ${#CONTROLLER_NVME_PCIS[@]}\n" + summary+="${separator}\n\n" + echo -e "$summary" } -# ========================================================== +function select_disk_type() { + VIRTUAL_DISKS=() + IMPORT_DISKS=() + CONTROLLER_NVME_PCIS=() + while true; do + local choice + choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \ + "1" "$(translate "Add virtual disk")" \ + "2" "$(translate "Add import disk")" \ + "3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \ + "r" "$(translate "Reset current storage selection")" \ + "d" "$(translate "[ Finish and continue ]")" \ + --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "Storage plan selection cancelled.")" + return 1 + } - - - -# ========================================================== -# Select Virtual Disks -# ========================================================== -function select_virtual_disk() { - - VIRTUAL_DISKS=() - - # Loop to add multiple disks - local add_more_disks=true - while $add_more_disks; do - - msg_info "Detecting available storage volumes..." - - # Get list of available storage - STORAGE_MENU=() - while read -r line; do - TAG=$(echo $line | awk '{print $1}') - TYPE=$(echo $line | awk '{print $2}') - FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf( "%9sB", $6)}') - ITEM=$(printf "%-15s %-10s %-15s" "$TAG" "$TYPE" "$FREE") - STORAGE_MENU+=("$TAG" "$ITEM" "OFF") - done < <(pvesm status -content images | awk 'NR>1') - - # Check that storage is available - VALID=$(pvesm status -content images | awk 'NR>1') - if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." - sleep 2 - select_disk_type - fi - - - # Select storage - if [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then - STORAGE=${STORAGE_MENU[0]} - msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." - else - - kill $SPINNER_PID > /dev/null - STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "Select Storage Volume")" --radiolist \ - "$(translate "Choose the storage volume for the virtual disk:\n")" 20 78 10 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) - - if [ $? -ne 0 ] || [ -z "$STORAGE" ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "No storage selected. At least one disk is required." - select_disk_type - else - add_more_disks=false + case "$choice" in + 1) + select_virtual_disk + ;; + 2) + select_import_disk + ;; + 3) + select_controller_nvme + ;; + r) + VIRTUAL_DISKS=() + IMPORT_DISKS=() + CONTROLLER_NVME_PCIS=() + ;; + d|done) + if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then continue fi - fi - - - fi - - # Request disk size - DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) - - if [ $? -ne 0 ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "Disk size not specified. At least one disk is required." - sleep 2 - select_disk_type - - else - add_more_disks=false - continue - fi - fi - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="32" - fi - - # Store the configuration in the disk list - VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") - - - # Ask if you want to create another disk - if ! whiptail --backtitle "ProxMenuX" --title "$(translate "Add Another Disk")" \ - --yesno "$(translate "Do you want to add another virtual disk?")" 8 58; then - add_more_disks=false - fi + if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then + msg_ok "$(translate "Virtual Disks Created:")" + for i in "${!VIRTUAL_DISKS[@]}"; do + echo -e "${TAB}${BL}- $(translate "Disk") $((i+1)): ${VIRTUAL_DISKS[$i]}GB${CL}" + done + fi + if [[ ${#IMPORT_DISKS[@]} -gt 0 ]]; then + msg_ok "$(translate "Import Disks Selected:")" + for i in "${!IMPORT_DISKS[@]}"; do + local disk_info + disk_info=$(lsblk -ndo MODEL,SIZE "${IMPORT_DISKS[$i]}" 2>/dev/null | xargs) + echo -e "${TAB}${BL}- $(translate "Disk") $((i+1)): ${IMPORT_DISKS[$i]}${disk_info:+ ($disk_info)}${CL}" + done + fi + if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then + msg_ok "$(translate "Controllers + NVMe Selected:")" + for i in "${!CONTROLLER_NVME_PCIS[@]}"; do + local pci_info + pci_info=$(lspci -nn -s "${CONTROLLER_NVME_PCIS[$i]#0000:}" 2>/dev/null | sed 's/^[^ ]* //') + echo -e "${TAB}${BL}- $(translate "Controller") $((i+1)): ${CONTROLLER_NVME_PCIS[$i]}${pci_info:+ ($pci_info)}${CL}" + done + fi + PASSTHROUGH_DISKS=("${IMPORT_DISKS[@]}") + DISK_TYPE="mixed" + export DISK_TYPE VIRTUAL_DISKS IMPORT_DISKS CONTROLLER_NVME_PCIS PASSTHROUGH_DISKS + select_loader || return 1 + return 0 + ;; + esac done - - # Show summary of the created disks - if [ ${#VIRTUAL_DISKS[@]} -gt 0 ]; then - - msg_ok "Virtual Disks Created:" - for i in "${!VIRTUAL_DISKS[@]}"; do - echo -e "${TAB}${BL}- Disk $((i+1)): ${VIRTUAL_DISKS[$i]}GB${CL}" - done - fi - - - export VIRTUAL_DISKS - - - select_loader } -# ========================================================== +function select_virtual_disk() { + msg_info "Detecting available storage volumes..." + local STORAGE_MENU=() + local TAG TYPE FREE ITEM + while read -r line; do + TAG=$(echo $line | awk '{print $1}') + TYPE=$(echo $line | awk '{print $2}') + FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf( "%9sB", $6)}') + ITEM=$(printf "%-15s %-10s %-15s" "$TAG" "$TYPE" "$FREE") + STORAGE_MENU+=("$TAG" "$ITEM" "OFF") + done < <(pvesm status -content images | awk 'NR>1') + local VALID + VALID=$(pvesm status -content images | awk 'NR>1') + if [[ -z "$VALID" ]]; then + msg_error "Unable to detect a valid storage location." + return 1 + fi + local STORAGE + if [[ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]]; then + STORAGE=${STORAGE_MENU[0]} + else + [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" >/dev/null 2>&1 + STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "Select Storage Volume")" --radiolist \ + "$(translate "Choose the storage volume for the virtual disk:\n")" 20 78 10 \ + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || return 0 + [[ -z "$STORAGE" ]] && return 0 + fi + local DISK_SIZE + stop_spinner + DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) || return 0 + [[ -z "$DISK_SIZE" ]] && DISK_SIZE="32" + VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") +} -# ========================================================== -# Select Physical Disks -# ========================================================== -function select_passthrough_disk() { - +function select_import_disk() { msg_info "$(translate "Detecting available disks...")" + _refresh_host_storage_cache - FREE_DISKS=() - - USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}') - MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}') - - ZFS_DISKS="" - ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror') - - for entry in $ZFS_RAW; do - path="" - if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then - if [ -e "/dev/disk/by-id/$entry" ]; then - path=$(readlink -f "/dev/disk/by-id/$entry") - fi - elif [[ "$entry" == /dev/* ]]; then - path="$entry" - fi - - if [ -n "$path" ]; then - base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null) - if [ -n "$base_disk" ]; then - ZFS_DISKS+="/dev/$base_disk"$'\n' - fi - fi - done - - ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u) - LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u) - - RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u) - + local FREE_DISKS=() + local DISK INFO MODEL SIZE LABEL DESCRIPTION while read -r DISK; do - [[ "$DISK" =~ /dev/zd ]] && continue + [[ "$DISK" =~ /dev/zd ]] && continue + _disk_is_host_system_used "$DISK" && continue - INFO=($(lsblk -dn -o MODEL,SIZE "$DISK")) - MODEL="${INFO[@]::${#INFO[@]}-1}" - SIZE="${INFO[-1]}" - LABEL="" - SHOW_DISK=true - - IS_MOUNTED=false - IS_RAID=false - IS_ZFS=false - IS_LVM=false - - while read -r part fstype; do - [[ "$fstype" == "zfs_member" ]] && IS_ZFS=true - [[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true - [[ "$fstype" == "LVM2_member" ]] && IS_LVM=true - if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then - IS_MOUNTED=true - fi - done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2) - - REAL_PATH=$(readlink -f "$DISK") - if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then - IS_MOUNTED=true - fi - - USED_BY="" - REAL_PATH=$(readlink -f "$DISK") - CONFIG_DATA=$(cat /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null) - - if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then - USED_BY="⚠ $(translate "In use")" - else - for SYMLINK in /dev/disk/by-id/*; do - if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then - if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then - USED_BY="⚠ $(translate "In use")" - break - fi - fi - done - fi - - if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)" && grep -q "active raid" /proc/mdstat; then - SHOW_DISK=false - fi - - if $IS_ZFS || $IS_MOUNTED || [[ "$ZFS_DISKS" == *"$DISK"* ]]; then - SHOW_DISK=false - fi - - if $SHOW_DISK; then - [[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]" - [[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID" - [[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM" - [[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS" - DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") - FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") - fi + INFO=($(lsblk -dn -o MODEL,SIZE "$DISK")) + MODEL="${INFO[@]::${#INFO[@]}-1}" + SIZE="${INFO[-1]}" + LABEL="" + if _disk_used_in_guest_configs "$DISK"; then + LABEL+=" [⚠ $(translate "In use by VM/LXC config")]" + fi + DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") + if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then + FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON") + else + FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") + fi done < <(lsblk -dn -e 7,11 -o PATH) - - if [ "${#FREE_DISKS[@]}" -eq 0 ]; then - cleanup - whiptail --title "Error" --msgbox "$(translate "No disks available for this VM.")" 8 40 - select_disk_type - return + if [[ ${#FREE_DISKS[@]} -eq 0 ]]; then + whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70 + return 1 fi - MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1) - TOTAL_WIDTH=$((MAX_WIDTH + 20)) - [ $TOTAL_WIDTH -lt 50 ] && TOTAL_WIDTH=50 - cleanup - SELECTED_DISKS=$(whiptail --title "Select Disks" --checklist \ - "$(translate "Select the disks you want to use (use spacebar to select):")" 20 $TOTAL_WIDTH 10 \ - "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) + local selected + selected=$(whiptail --title "Select Import Disks" --checklist \ + "$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 78 10 \ + "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) || return 1 - if [ -z "$SELECTED_DISKS" ]; then - msg_error "Disk not specified. At least one disk is required." - sleep 2 - select_disk_type - return - fi - - - msg_ok "Disk passthrough selected:" - PASSTHROUGH_DISKS=() - for DISK in $(echo "$SELECTED_DISKS" | tr -d '"'); do - DISK_INFO=$(lsblk -ndo MODEL,SIZE "$DISK" | xargs) - echo -e "${TAB}${CL}${BL}- $DISK $DISK_INFO${GN}${CL}" - PASSTHROUGH_DISKS+=("$DISK") + IMPORT_DISKS=() + local item + for item in $(echo "$selected" | tr -d '"'); do + IMPORT_DISKS+=("$item") done + export IMPORT_DISKS +} - - select_loader +function select_controller_nvme() { + msg_info "$(translate "Detecting PCI storage controllers and NVMe devices...")" + _refresh_host_storage_cache + + local menu_items=() + local blocked_report="" + local safe_count=0 blocked_count=0 + local pci_path pci_full class_hex name controller_disks disk reason state controller_desc + + 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" || "${class_hex:0:2}" != "01" ]] && continue + + 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") + + reason="" + for disk in "${controller_disks[@]}"; do + if _disk_is_host_system_used "$disk"; then + reason+="${disk} (${DISK_USAGE_REASON}); " + elif _disk_used_in_guest_configs "$disk"; then + reason+="${disk} ($(translate "In use by VM/LXC config")); " + fi + done + + if [[ -n "$reason" ]]; then + blocked_count=$((blocked_count + 1)) + blocked_report+=" • ${pci_full} — ${name}\n $(translate "Blocked because protected/in-use disks are attached"): ${reason}\n" + continue + fi + + if [[ ${#controller_disks[@]} -gt 0 ]]; then + controller_desc="$(printf "%-48s [%s]" "$name" "$(IFS=,; echo "${controller_disks[*]}")")" + else + controller_desc="$(printf "%-48s [%s]" "$name" "$(translate "No attached disks detected")")" + fi + + if _array_contains "$pci_full" "${CONTROLLER_NVME_PCIS[@]}"; then + state="ON" + else + state="OFF" + fi + + 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 + whiptail --title "Controller + NVMe" --msgbox "$(translate "No safe controllers/NVMe devices are available for passthrough.")\n\n${blocked_report}" 20 90 + return 1 + fi + + if [[ $blocked_count -gt 0 ]]; then + whiptail --title "Controller + NVMe" --msgbox "$(translate "Some controllers were hidden because they have host system disks attached.")\n\n${blocked_report}" 20 90 + fi + + local selected + selected=$(whiptail --title "Controller + NVMe" --checklist \ + "$(translate "Select controllers/NVMe to passthrough (safe devices only):")" 20 90 10 \ + "${menu_items[@]}" 3>&1 1>&2 2>&3) || return 1 + + CONTROLLER_NVME_PCIS=() + local pci + for pci in $(echo "$selected" | tr -d '"'); do + CONTROLLER_NVME_PCIS+=("$pci") + done + export CONTROLLER_NVME_PCIS +} + +function select_passthrough_disk() { + select_import_disk } # ========================================================== @@ -832,23 +908,27 @@ function select_efi_storage() { VALID=$(pvesm status -content images | awk 'NR>1') if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location for EFI disk." + msg_error "Unable to detect a valid storage location for EFI disk." >&2 + return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} else - kill $SPINNER_PID > /dev/null + [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" > /dev/null 2>&1 while [ -z "${STORAGE:+x}" ]; do STORAGE=$(whiptail --backtitle "ProxMenuX" --title "EFI Disk Storage" --radiolist \ "$(translate "Choose the storage volume for the EFI disk (4MB):\n\nUse Spacebar to select.")" \ 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "EFI storage selection cancelled.")" >&2 + return 1 + } done fi - + [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } # ========================================================== @@ -883,8 +963,8 @@ function select_storage_volume() { VALID=$(pvesm status -content images | awk 'NR>1') if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." - exit 1 + msg_error "Unable to detect a valid storage location." >&2 + return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} else @@ -892,10 +972,13 @@ function select_storage_volume() { STORAGE=$(whiptail --backtitle "ProxMenuX" --title "Storage Pools" --radiolist \ "$(translate "Choose the storage volume for $purpose:\n\nUse Spacebar to select.")" \ 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "Storage selection cancelled for $purpose.")" >&2 + return 1 + } done fi - + [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } @@ -911,7 +994,7 @@ function create_vm() { # Create the VM qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \ - -name $HN -tags proxmenux -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \ + -name $HN -tags proxmenux,nas -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \ -serial0 socket msg_ok "Create a $NAME" @@ -921,7 +1004,10 @@ function create_vm() { if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then msg_info "Configuring EFI disk" - EFI_STORAGE=$(select_efi_storage $VMID) + if ! EFI_STORAGE=$(select_efi_storage $VMID); then + msg_error "EFI storage selection failed or was cancelled." + return 1 + fi EFI_DISK_NAME="vm-${VMID}-disk-efivars" # Determine storage type and extension @@ -974,7 +1060,10 @@ function create_vm() { # Select storage volume for loader ======================= - LOADER_STORAGE=$(select_storage_volume $VMID "loader disk") + if ! LOADER_STORAGE=$(select_storage_volume $VMID "loader disk"); then + msg_error "Loader storage selection failed or was cancelled." + return 1 + fi #Run the command in the background and capture its PID @@ -1048,18 +1137,14 @@ function create_vm() { # ========================================================== -if [ "$DISK_TYPE" = "virtual" ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "No virtual disks configured." - exit_script - fi - - DISK_INFO="" - CONSOLE_DISK_INFO="" +DISK_INFO="" +CONSOLE_DISK_INFO="" +DISK_SLOT_INDEX=0 +if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then for i in "${!VIRTUAL_DISKS[@]}"; do IFS=':' read -r STORAGE SIZE <<< "${VIRTUAL_DISKS[$i]}" - + STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}') case $STORAGE_TYPE in nfs | dir) @@ -1071,146 +1156,166 @@ if [ "$DISK_TYPE" = "virtual" ]; then DISK_REF="" ;; esac - - DISK_NUM=$((i+1)) + DISK_NUM=$((DISK_SLOT_INDEX+1)) DISK_NAME="vm-${VMID}-disk-${DISK_NUM}${DISK_EXT}" - SATA_ID="sata$i" - - # Create virtual disk - if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then - - msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..." - if ! qm set "$VMID" -$SATA_ID "$STORAGE:$SIZE,format=raw" >/dev/null 2>&1; then - msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE" - ERROR_FLAG=true - continue - fi - else + SATA_ID="sata${DISK_SLOT_INDEX}" - msg_info "Allocating virtual disk for $STORAGE_TYPE..." - if ! pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME" "$SIZE"G >/dev/null 2>&1; then - msg_error "Failed to allocate virtual disk $DISK_NUM" - ERROR_FLAG=true - continue - fi - if ! qm set "$VMID" -$SATA_ID "$STORAGE:${DISK_REF}$DISK_NAME" >/dev/null 2>&1; then - msg_error "Failed to configure virtual disk as $SATA_ID" - ERROR_FLAG=true - continue - fi + if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then + msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..." + if ! qm set "$VMID" -$SATA_ID "$STORAGE:$SIZE,format=raw" >/dev/null 2>&1; then + msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE" + ERROR_FLAG=true + continue + fi + else + msg_info "Allocating virtual disk for $STORAGE_TYPE..." + if ! pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME" "$SIZE"G >/dev/null 2>&1; then + msg_error "Failed to allocate virtual disk $DISK_NUM" + ERROR_FLAG=true + continue + fi + if ! qm set "$VMID" -$SATA_ID "$STORAGE:${DISK_REF}$DISK_NAME" >/dev/null 2>&1; then + msg_error "Failed to configure virtual disk as $SATA_ID" + ERROR_FLAG=true + continue + fi fi msg_ok "Configured virtual disk as $SATA_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}" - - - # Add information to the description DISK_INFO="${DISK_INFO}

Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE}

" CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} ($SATA_ID)\n" + DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1)) done - - - - # HTML description -HTML_DESC="
- - - - - -
-ProxMenux Logo - -

Synology DSM VM

-

Created with ProxMenuX

-

Loader: $LOADER_NAME

-
- -

-Docs -Code -Loader -Ko-fi -

- -
-${DISK_INFO} -
-
" - - msg_info "Setting VM description" - if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then - msg_error "Failed to set VM description" - exit_script - fi - msg_ok "Configured VM description" - - -else - - - # Configure multiple passthrough disks - DISK_INFO="" - CONSOLE_DISK_INFO="" - - for i in "${!PASSTHROUGH_DISKS[@]}"; do - DISK="${PASSTHROUGH_DISKS[$i]}" - DISK_MODEL=$(lsblk -ndo MODEL "$DISK" | xargs) - DISK_SIZE=$(lsblk -ndo SIZE "$DISK" | xargs) - DISK_ID="sata$i" - - - result=$(qm set $VMID -${DISK_ID} ${DISK} 2>&1) - if [[ $? -eq 0 ]]; then - msg_ok "Configured disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID" - fi - # Add information to the description - DISK_INFO="${DISK_INFO}

Passthrough Disk $((i+1)): $DISK ($DISK_MODEL $DISK_SIZE)

" - CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Passthrough Disk $((i+1)): $DISK ($DISK_MODEL $DISK_SIZE) (${DISK_ID})\n" - done - - - # HTML description -HTML_DESC="
- - - - - -
-ProxMenux Logo - -

Synology DSM VM

-

Created with ProxMenuX

-

Loader: $LOADER_NAME

-
- -

-Docs -Code -Loader -Ko-fi -

- -
-${DISK_INFO} -
-
" - - - result=$(qm set $VMID -description "$HTML_DESC" 2>&1) - if [[ $? -eq 0 ]]; then - msg_ok "Configured VM description" - fi - - fi + +EFFECTIVE_IMPORT_DISKS=() +if [[ ${#IMPORT_DISKS[@]} -gt 0 ]]; then + EFFECTIVE_IMPORT_DISKS=("${IMPORT_DISKS[@]}") +elif [[ ${#PASSTHROUGH_DISKS[@]} -gt 0 ]]; then + EFFECTIVE_IMPORT_DISKS=("${PASSTHROUGH_DISKS[@]}") +fi + +if [[ ${#EFFECTIVE_IMPORT_DISKS[@]} -gt 0 ]]; then + for DISK in "${EFFECTIVE_IMPORT_DISKS[@]}"; do + DISK_MODEL=$(lsblk -ndo MODEL "$DISK" | xargs) + DISK_SIZE=$(lsblk -ndo SIZE "$DISK" | xargs) + DISK_NUM=$((DISK_SLOT_INDEX+1)) + DISK_ID="sata${DISK_SLOT_INDEX}" + + if qm set "$VMID" -${DISK_ID} ${DISK} >/dev/null 2>&1; then + msg_ok "Configured import disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID" + DISK_INFO="${DISK_INFO}

Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE)

" + CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE) (${DISK_ID})\n" + DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1)) + else + msg_error "Failed to configure import disk $DISK as $DISK_ID" + ERROR_FLAG=true + fi + done +fi + +if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then + if ! _vm_is_q35 "$VMID"; then + msg_error "$(translate "Controller + NVMe passthrough requires machine type q35. Skipping controller assignment.")" + ERROR_FLAG=true + else + HOSTPCI_INDEX=0 + if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then + HOSTPCI_INDEX=$(_pci_next_hostpci_index "$VMID" 2>/dev/null || echo 0) + else + while qm config "$VMID" | grep -q "^hostpci${HOSTPCI_INDEX}:"; do + HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1)) + done + fi + + for PCI_DEV in "${CONTROLLER_NVME_PCIS[@]}"; do + if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then + if _pci_function_assigned_to_vm "$PCI_DEV" "$VMID"; then + msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + continue + fi + else + local PCI_BDF + PCI_BDF="${PCI_DEV#0000:}" + if qm config "$VMID" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*(0000:)?${PCI_BDF}([,[:space:]]|$)"; then + msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + continue + fi + fi + + if qm set "$VMID" --hostpci${HOSTPCI_INDEX} "${PCI_DEV},pcie=1" >/dev/null 2>&1; then + msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}" + DISK_INFO="${DISK_INFO}

Controller/NVMe: ${PCI_DEV}

" + CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n" + HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1)) + else + msg_error "Failed to configure controller/NVMe: ${PCI_DEV}" + ERROR_FLAG=true + fi + done + fi +fi + +if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#EFFECTIVE_IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then + msg_error "No disks/controllers configured." + exit_script +fi + +# HTML description +HTML_DESC="
+ + + + + +
+ProxMenux Logo + +

Synology DSM VM

+

Created with ProxMenuX

+

Loader: $LOADER_NAME

+
+ +

+Docs +Code +Loader +Ko-fi +

+ +
+${DISK_INFO} +
+
" + +msg_info "Setting VM description" +if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then + msg_error "Failed to set VM description" + exit_script +fi +msg_ok "Configured VM description" if [ "$ERROR_FLAG" = true ]; then msg_error "VM created with errors. Check configuration." else -msg_success "$(translate "Completed Successfully!")" +if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then + WIZARD_GPU_RESULT="cancelled" + run_gpu_passthrough_wizard + replay_vm_wizard_capture +fi + +if [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$WIZARD_GPU_RESULT" == "applied" ]]; then + msg_success "$(translate "Completed Successfully with GPU passthrough configured!")" +else + msg_success "$(translate "Completed Successfully!")" + if [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$WIZARD_GPU_RESULT" == "no_gpu" ]]; then + msg_warn "$(translate "GPU passthrough was skipped (no compatible GPU detected).")" + elif [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$WIZARD_GPU_RESULT" != "applied" ]]; then + msg_warn "$(translate "GPU passthrough was not applied.")" + fi +fi echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}" echo -e "${TAB}1. $(translate "Start the VM")" @@ -1218,6 +1323,11 @@ echo -e "${TAB}2. $(translate "Open the VM console and wait for the loader to bo echo -e "${TAB}3. $(translate "In the loader interface, follow the instructions to select your Synology model")" echo -e "${TAB}4. $(translate "Complete the DSM installation wizard")" echo -e "${TAB}5. $(translate "Find your device using https://finds.synology.com")" +if [[ "$WIZARD_GPU_RESULT" == "applied" ]]; then + echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")" + echo -e "${TAB}• $(translate "First complete DSM setup and verify Web UI/SSH access.")" + echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")" +fi echo -e #msg_success "$(translate "Press Enter to return to the main menu...")" @@ -1240,7 +1350,16 @@ sleep 1 # Start script if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then - start_script + start_vm_wizard_capture + if ! start_script; then + stop_vm_wizard_capture + msg_warn "$(translate "VM creation cancelled before disk planning.")" + exit 0 + fi + prompt_optional_gpu_passthrough + if [[ "${WIZARD_ADD_GPU:-no}" != "yes" ]]; then + stop_vm_wizard_capture + fi else clear exit @@ -1248,5 +1367,6 @@ fi # Create VM create_vm +stop_vm_wizard_capture -# ========================================================== \ No newline at end of file +# ========================================================== diff --git a/scripts/vm/vm_creator.sh b/scripts/vm/vm_creator.sh index 6371712c..a65864a4 100644 --- a/scripts/vm/vm_creator.sh +++ b/scripts/vm/vm_creator.sh @@ -24,15 +24,35 @@ # consistent and maintainable way, using ProxMenux standards. # ========================================================== -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +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="$BASE_DIR/utils.sh" +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 VENV_PATH="/opt/googletrans-env" 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 + load_language initialize_cache @@ -67,7 +87,10 @@ function select_interface_type() { "sata" "$(translate "SATA (standard - high compatibility)")" OFF \ "virtio" "$(translate "VirtIO (advanced - high performance)")" OFF \ "ide" "IDE (legacy)" OFF \ - 3>&1 1>&2 2>&3) || exit 1 + 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "Disk interface selection cancelled.")" >&2 + return 1 + } case "$INTERFACE_TYPE" in "scsi"|"sata") @@ -102,17 +125,21 @@ function select_storage_target() { done < <(pvesm status -content images | awk 'NR>1') if [[ ${#STORAGE_MENU[@]} -eq 0 ]]; then - msg_error "$(translate "Unable to detect a valid storage location for $PURPOSE disk.")" - exit 1 + msg_error "$(translate "Unable to detect a valid storage location for $PURPOSE disk.")" >&2 + return 1 elif [[ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]]; then STORAGE="${STORAGE_MENU[0]}" else - kill $SPINNER_PID > /dev/null + [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" >/dev/null 2>&1 STORAGE=$(whiptail --backtitle "ProxMenux" --title "$(translate "$PURPOSE Disk Storage")" --radiolist \ "$(translate "Choose the storage volume for the $PURPOSE disk (4MB):\n\nUse Spacebar to select.")" 16 70 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit 1 + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "$PURPOSE disk storage selection cancelled.")" >&2 + return 1 + } fi + [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } @@ -141,6 +168,35 @@ function configure_guest_agent() { } +function run_gpu_passthrough_wizard() { + [[ "${WIZARD_ADD_GPU:-no}" != "yes" ]] && return 0 + + local gpu_script="$LOCAL_SCRIPTS/gpu_tpu/add_gpu_vm.sh" + local wizard_result_file="" + if [[ ! -f "$gpu_script" ]]; then + local local_gpu_script + local_gpu_script="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/gpu_tpu/add_gpu_vm.sh" + [[ -f "$local_gpu_script" ]] && gpu_script="$local_gpu_script" + fi + + if [[ ! -f "$gpu_script" ]]; then + msg_warn "$(translate "GPU passthrough assistant not found. You can run it later from Hardware Graphics.")" + return 0 + fi + + msg_info2 "$(translate "Launching GPU passthrough assistant for VM") ${VMID}..." + wizard_result_file="/tmp/proxmenux_gpu_wizard_result_${VMID}_$$.txt" + : >"$wizard_result_file" + bash "$gpu_script" --vmid "$VMID" --wizard --result-file "$wizard_result_file" + + if [[ -s "$wizard_result_file" ]]; then + WIZARD_GPU_RESULT=$(head -n1 "$wizard_result_file" | tr -d '\r\n') + else + WIZARD_GPU_RESULT="cancelled" + fi + rm -f "$wizard_result_file" +} + @@ -154,7 +210,6 @@ function create_vm() { local ISO_DIR="/var/lib/vz/template/iso" - if [[ -n "$ISO_PATH" && -n "$ISO_URL" && ! -f "$ISO_PATH" ]]; then if [[ "$ISO_URL" == *"sourceforge.net"* ]]; then @@ -170,7 +225,7 @@ function create_vm() { msg_ok "$(translate "ISO image downloaded")" else msg_error "$(translate "Failed to download ISO image")" - return + return 1 fi fi @@ -180,6 +235,13 @@ function create_vm() { GUEST_OS_TYPE="l26" fi + local VM_TAGS="proxmenux" + case "${OS_TYPE:-}" in + 1) VM_TAGS="proxmenux,nas" ;; + 2) VM_TAGS="proxmenux,windows" ;; + *) VM_TAGS="proxmenux,linux" ;; + esac + # qm create "$VMID" -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} \ @@ -195,7 +257,7 @@ qm create "$VMID" \ -cores "$CORE_COUNT" \ -memory "$RAM_SIZE" \ -name "$HN" \ - -tags proxmenux \ + -tags "$VM_TAGS" \ -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" \ -ostype "$GUEST_OS_TYPE" \ -scsihw virtio-scsi-pci \ @@ -213,7 +275,10 @@ fi if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then msg_info "$(translate "Configuring EFI disk")" - EFI_STORAGE=$(select_storage_target "EFI" "$VMID") + if ! EFI_STORAGE=$(select_storage_target "EFI" "$VMID"); then + msg_error "$(translate "EFI storage selection failed or was cancelled. VM creation aborted.")" + return 1 + fi STORAGE_TYPE=$(pvesm status -storage "$EFI_STORAGE" | awk 'NR>1 {print $2}') EFI_DISK_ID="efidisk0" EFI_KEYS="0" @@ -249,7 +314,10 @@ fi if [[ "$OS_TYPE" == "2" ]]; then msg_info "$(translate "Configuring TPM device")" - TPM_STORAGE=$(select_storage_target "TPM" "$VMID") + if ! TPM_STORAGE=$(select_storage_target "TPM" "$VMID"); then + msg_error "$(translate "TPM storage selection failed or was cancelled. VM creation aborted.")" + return 1 + fi STORAGE_TYPE=$(pvesm status -storage "$TPM_STORAGE" | awk 'NR>1 {print $2}') TPM_ID="tpmstate0" @@ -282,18 +350,31 @@ fi # ========================================================== -# Create Diks +# Create Disks / Import Disks / Controller + NVMe # ========================================================== + local -a EFFECTIVE_IMPORT_DISKS=() + if [[ ${#IMPORT_DISKS[@]} -gt 0 ]]; then + EFFECTIVE_IMPORT_DISKS=("${IMPORT_DISKS[@]}") + elif [[ ${#PASSTHROUGH_DISKS[@]} -gt 0 ]]; then + EFFECTIVE_IMPORT_DISKS=("${PASSTHROUGH_DISKS[@]}") + fi -select_interface_type + if [[ ${#VIRTUAL_DISKS[@]} -gt 0 || ${#EFFECTIVE_IMPORT_DISKS[@]} -gt 0 ]]; then + if ! select_interface_type; then + msg_error "$(translate "Disk interface is required to continue VM creation.")" + return 1 + fi + fi - if [[ "$DISK_TYPE" == "virtual" && ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then + local NEXT_DISK_SLOT=0 + + if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then for i in "${!VIRTUAL_DISKS[@]}"; do - DISK_INDEX=$((i+1)) + DISK_INDEX=$((NEXT_DISK_SLOT+1)) IFS=':' read -r STORAGE SIZE <<< "${VIRTUAL_DISKS[$i]}" DISK_NAME="vm-${VMID}-disk-${DISK_INDEX}" - SLOT_NAME="${INTERFACE_TYPE}${i}" + SLOT_NAME="${INTERFACE_TYPE}${NEXT_DISK_SLOT}" STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}') case "$STORAGE_TYPE" in @@ -313,6 +394,7 @@ select_interface_type msg_ok "$(translate "Virtual disk") $DISK_INDEX ${SIZE}GB - $STORAGE ($SLOT_NAME)" DISK_INFO+="

Virtual Disk $DISK_INDEX: ${SIZE}GB ($STORAGE / $SLOT_NAME)

" [[ -z "$BOOT_ORDER" ]] && BOOT_ORDER="$SLOT_NAME" + NEXT_DISK_SLOT=$((NEXT_DISK_SLOT + 1)) else msg_error "$(translate "Failed to assign virtual disk") $DISK_INDEX" fi @@ -325,6 +407,7 @@ select_interface_type msg_ok "$(translate "Virtual disk") $DISK_INDEX ${SIZE}GB - $STORAGE ($SLOT_NAME)" DISK_INFO+="

Virtual Disk $DISK_INDEX: ${SIZE}GB ($STORAGE / $SLOT_NAME)

" [[ -z "$BOOT_ORDER" ]] && BOOT_ORDER="$SLOT_NAME" + NEXT_DISK_SLOT=$((NEXT_DISK_SLOT + 1)) else msg_error "$(translate "Failed to create disk") $DISK_INDEX" fi @@ -334,19 +417,59 @@ select_interface_type - if [[ "$DISK_TYPE" == "passthrough" && ${#PASSTHROUGH_DISKS[@]} -gt 0 ]]; then - for i in "${!PASSTHROUGH_DISKS[@]}"; do - SLOT_NAME="${INTERFACE_TYPE}${i}" - DISK="${PASSTHROUGH_DISKS[$i]}" + if [[ ${#EFFECTIVE_IMPORT_DISKS[@]} -gt 0 ]]; then + for i in "${!EFFECTIVE_IMPORT_DISKS[@]}"; do + SLOT_NAME="${INTERFACE_TYPE}${NEXT_DISK_SLOT}" + DISK="${EFFECTIVE_IMPORT_DISKS[$i]}" MODEL=$(lsblk -ndo MODEL "$DISK") SIZE=$(lsblk -ndo SIZE "$DISK") qm set "$VMID" -$SLOT_NAME "$DISK${DISCARD_OPTS}" >/dev/null 2>&1 - msg_ok "$(translate "Passthrough disk assigned") ($DISK → $SLOT_NAME)" - DISK_INFO+="

Passthrough Disk $((i+1)): $DISK ($MODEL $SIZE)

" + msg_ok "$(translate "Import disk assigned") ($DISK → $SLOT_NAME)" + DISK_INFO+="

Import Disk $((NEXT_DISK_SLOT+1)): $DISK ($MODEL $SIZE)

" [[ -z "$BOOT_ORDER" ]] && BOOT_ORDER="$SLOT_NAME" + NEXT_DISK_SLOT=$((NEXT_DISK_SLOT + 1)) done fi + if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then + if ! _vm_is_q35 "$VMID"; then + msg_error "$(translate "Controller + NVMe passthrough requires machine type q35. Skipping controller assignment.")" + else + local hostpci_idx=0 + if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then + hostpci_idx=$(_pci_next_hostpci_index "$VMID" 2>/dev/null || echo 0) + else + local hostpci_existing + hostpci_existing=$(qm config "$VMID" 2>/dev/null) + while grep -q "^hostpci${hostpci_idx}:" <<< "$hostpci_existing"; do + hostpci_idx=$((hostpci_idx + 1)) + done + fi + + local pci bdf + for pci in "${CONTROLLER_NVME_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" "$VMID"; then + msg_warn "$(translate "Controller/NVMe already present in VM config") ($pci)" + continue + fi + elif qm config "$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 + + if qm set "$VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >/dev/null 2>&1; then + msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} → ${pci})" + DISK_INFO+="

Controller/NVMe: ${pci}

" + hostpci_idx=$((hostpci_idx + 1)) + else + msg_error "$(translate "Failed to assign Controller/NVMe") (${pci})" + fi + done + fi + fi + @@ -427,10 +550,17 @@ select_interface_type fi - local BOOT_FINAL="$BOOT_ORDER" - [[ -f "$ISO_PATH" ]] && BOOT_FINAL="$BOOT_ORDER;ide2" - qm set "$VMID" -boot order="$BOOT_FINAL" >/dev/null - msg_ok "$(translate "Boot order set to") $BOOT_FINAL" + local BOOT_FINAL="" + if [[ -n "$BOOT_ORDER" ]]; then + BOOT_FINAL="$BOOT_ORDER" + fi + if [[ -f "$ISO_PATH" ]]; then + BOOT_FINAL="${BOOT_FINAL:+$BOOT_FINAL;}ide2" + fi + if [[ -n "$BOOT_FINAL" ]]; then + qm set "$VMID" -boot order="$BOOT_FINAL" >/dev/null + msg_ok "$(translate "Boot order set to") $BOOT_FINAL" + fi @@ -450,7 +580,7 @@ select_interface_type

Docs -Code +Code Ko-fi

@@ -466,16 +596,75 @@ else msg_ok "$(translate "VM description configured")" fi + if [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$START_VM" == "yes" ]]; then + msg_warn "$(translate "Auto-start was skipped because GPU passthrough setup was requested.")" + msg_warn "$(translate "After completing GPU setup, start the VM manually when the host is ready.")" + START_VM="no" + fi + if [[ "$START_VM" == "yes" ]]; then qm start "$VMID" msg_ok "$(translate "VM started")" fi configure_guest_agent - msg_success "$(translate "VM creation completed")" +if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then + WIZARD_GPU_RESULT="cancelled" + run_gpu_passthrough_wizard + if [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]]; then + stop_spinner + exec 1>&8 + exec 8>&- + VM_WIZARD_CAPTURE_ACTIVE=0 + show_proxmenux_logo + cat "$VM_WIZARD_CAPTURE_FILE" + rm -f "$VM_WIZARD_CAPTURE_FILE" + VM_WIZARD_CAPTURE_FILE="" + fi + if [[ "$WIZARD_GPU_RESULT" == "applied" ]]; then + msg_success "$(translate "VM creation completed with GPU passthrough configured.")" + elif [[ "$WIZARD_GPU_RESULT" == "no_gpu" ]]; then + msg_success "$(translate "VM creation completed. GPU passthrough was skipped (no compatible GPU detected).")" + else + msg_success "$(translate "VM creation completed. GPU passthrough was not applied.")" + fi + if [[ "$OS_TYPE" == "2" ]]; then + echo -e "${TAB}$(translate "Next Steps:")" + echo -e "${TAB}1. $(translate "Start the VM to begin Windows installation from the mounted ISO.")" + echo -e "${TAB}2. $(translate "When asked to select a disk, click Load Driver and load the VirtIO drivers.")" + echo -e "${TAB} $(translate "Required if using a VirtIO or SCSI disk.")" + echo -e "${TAB}3. $(translate "Also install the VirtIO network driver during setup to enable network access.")" + echo -e "${TAB}4. $(translate "Continue the Windows installation as usual.")" + echo -e "${TAB}5. $(translate "Once installed, open the VirtIO ISO and run the installer to complete driver setup.")" + echo -e "${TAB}6. $(translate "Reboot the VM to complete the driver installation.")" + if [[ "$WIZARD_GPU_RESULT" == "applied" ]]; then + echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")" + echo -e "${TAB}• $(translate "First install the GPU drivers inside the guest and verify remote access (RDP/SSH).")" + echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the guest is stable.")" + echo -e "${TAB}• $(translate "If passthrough fails on Windows: install RadeonResetBugFix.")" + fi + echo -e + elif [[ "$OS_TYPE" == "3" ]]; then + echo -e "${TAB}${GN}$(translate "Recommended: Install the QEMU Guest Agent in the VM")${CL}" + echo -e "${TAB}$(translate "Run the following inside the VM:")" + echo -e "${TAB}apt install qemu-guest-agent -y && systemctl enable --now qemu-guest-agent" + if [[ "$WIZARD_GPU_RESULT" == "applied" ]]; then + echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")" + echo -e "${TAB}• $(translate "First install the GPU drivers inside the guest and verify remote access (RDP/SSH).")" + echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the guest is stable.")" + fi + echo -e + fi + msg_success "$(translate "Press Enter to return to the main menu...")" + read -r + bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" + exit 0 +fi + +msg_success "$(translate "VM creation completed")" if [[ "$OS_TYPE" == "2" ]]; then - echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}" + echo -e "${TAB}$(translate "Next Steps:")" echo -e "${TAB}1. $(translate "Start the VM to begin Windows installation from the mounted ISO.")" echo -e "${TAB}2. $(translate "When asked to select a disk, click Load Driver and load the VirtIO drivers.")" echo -e "${TAB} $(translate "Required if using a VirtIO or SCSI disk.")" @@ -487,11 +676,10 @@ if [[ "$OS_TYPE" == "2" ]]; then elif [[ "$OS_TYPE" == "3" ]]; then echo -e "${TAB}${GN}$(translate "Recommended: Install the QEMU Guest Agent in the VM")${CL}" echo -e "${TAB}$(translate "Run the following inside the VM:")" - echo -e "${TAB}${CY}apt install qemu-guest-agent -y && systemctl enable --now qemu-guest-agent${CL}" + echo -e "${TAB}apt install qemu-guest-agent -y && systemctl enable --now qemu-guest-agent" echo -e fi - msg_success "$(translate "Press Enter to return to the main menu...")" read -r bash "$LOCAL_SCRIPTS/menus/create_vm_menu.sh" diff --git a/scripts/vm/vm_creator_.sh b/scripts/vm/vm_creator_.sh deleted file mode 100644 index d2258438..00000000 --- a/scripts/vm/vm_creator_.sh +++ /dev/null @@ -1,466 +0,0 @@ -#!/usr/bin/env bash - -# ========================================================== -# ProxMenuX - Virtual Machine Creator Script -# ========================================================== -# Author : MacRimi -# Copyright : (c) 2024 MacRimi -# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) -# Version : 1.0 -# Last Updated: 07/05/2025 -# ========================================================== -# Description: -# This script is part of the central ProxMenux VM creation module. It allows users -# to create virtual machines (VMs) in Proxmox VE using either default or advanced -# configurations, streamlining the deployment of Linux, Windows, and other systems. -# -# Key features: -# - Supports both virtual disk creation and physical disk passthrough. -# - Automates CPU, RAM, BIOS, network and storage configuration. -# - Provides a user-friendly menu to select OS type, ISO image and disk interface. -# - Automatically generates a detailed and styled HTML description for each VM. -# -# All operations are designed to simplify and accelerate VM creation in a -# consistent and maintainable way, using ProxMenux standards. -# ========================================================== - -BASE_DIR="/usr/local/share/proxmenux" -UTILS_FILE="$BASE_DIR/utils.sh" -VENV_PATH="/opt/googletrans-env" - -if [[ -f "$UTILS_FILE" ]]; then - source "$UTILS_FILE" -fi - -load_language -initialize_cache - -# ========================================================== -# Mont ISOs -# ========================================================== -function mount_iso_to_vm() { - local vmid="$1" - local iso_path="$2" - local device="$3" - - if [[ -f "$iso_path" ]]; then - local iso_basename - iso_basename=$(basename "$iso_path") - qm set "$vmid" -$device "local:iso/$iso_basename,media=cdrom" >/dev/null 2>&1 - msg_ok "$(translate "Mounted ISO on device") $device → $iso_basename" - else - msg_warn "$(translate "ISO not found to mount on device") $device" - fi -} - - - - -# ========================================================== -# Select Interface Type -# ========================================================== -function select_interface_type() { - INTERFACE_TYPE=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select Disk Interface")" --radiolist \ - "$(translate "Select the bus type for the disks:")" 15 70 4 \ - "scsi" "$(translate "SCSI (recommended for Linux and Windows)")" ON \ - "sata" "$(translate "SATA (standard - high compatibility)")" OFF \ - "virtio" "$(translate "VirtIO (advanced - high performance)")" OFF \ - "ide" "IDE (legacy)" OFF \ - 3>&1 1>&2 2>&3) || exit 1 - - case "$INTERFACE_TYPE" in - "scsi"|"sata") - DISCARD_OPTS=",discard=on,ssd=on" - ;; - "virtio") - DISCARD_OPTS=",discard=on" - ;; - "ide") - DISCARD_OPTS="" - ;; - esac - - msg_ok "$(translate "Disk interface selected:") $INTERFACE_TYPE" -} - - -# ========================================================== -# EFI/TPM -# ========================================================== -function select_storage_target() { - local PURPOSE="$1" - local vmid="$2" - local STORAGE="" - local STORAGE_MENU=() - - while read -r line; do - TAG=$(echo "$line" | awk '{print $1}') - TYPE=$(echo "$line" | awk '{printf "%-10s", $2}') - FREE=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf("%9sB", $6)}') - STORAGE_MENU+=("$TAG" "$(translate "Type:") $TYPE $(translate "Free:") $FREE" "OFF") - done < <(pvesm status -content images | awk 'NR>1') - - if [[ ${#STORAGE_MENU[@]} -eq 0 ]]; then - msg_error "$(translate "Unable to detect a valid storage location for $PURPOSE disk.")" - exit 1 - elif [[ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]]; then - STORAGE="${STORAGE_MENU[0]}" - else - kill $SPINNER_PID > /dev/null - STORAGE=$(whiptail --backtitle "ProxMenux" --title "$(translate "$PURPOSE Disk Storage")" --radiolist \ - "$(translate "Choose the storage volume for the $PURPOSE disk (4MB):\n\nUse Spacebar to select.")" 16 70 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit 1 - fi - - echo "$STORAGE" -} - - - - -# ========================================================== -# Guest Agent Configurator -# ========================================================== -function configure_guest_agent() { - if [[ -z "$VMID" ]]; then - msg_error "$(translate "No VMID defined. Cannot apply guest agent config.")" - return 1 - fi - - msg_info "$(translate "Adding QEMU Guest Agent support...")" - - # Habilitar el agente en la VM - qm set "$VMID" -agent enabled=1 >/dev/null 2>&1 - - # Añadir canal de comunicación virtio - qm set "$VMID" -chardev socket,id=qga0,path=/var/run/qemu-server/$VMID.qga,server=on,wait=off >/dev/null 2>&1 - qm set "$VMID" -device virtio-serial-pci -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 >/dev/null 2>&1 - - msg_ok "$(translate "Guest Agent configuration applied")" - -} - - - - -# ========================================================== -# Create VM -# ========================================================== -function create_vm() { - local BOOT_ORDER="" - local DISK_INFO="" - local DISK_INDEX=0 - local ISO_DIR="/var/lib/vz/template/iso" - - - - if [[ -n "$ISO_PATH" && -n "$ISO_URL" && ! -f "$ISO_PATH" ]]; then - - if [[ "$ISO_URL" == *"sourceforge.net"* ]]; then - - wget --content-disposition --show-progress -O "$ISO_PATH" "$ISO_URL" - else - - wget --no-verbose --show-progress -O "$ISO_PATH" "$ISO_URL" - fi - - - if [[ -f "$ISO_PATH" ]]; then - msg_ok "$(translate "ISO image downloaded")" - else - msg_error "$(translate "Failed to download ISO image")" - return - fi - fi - - if [[ "$OS_TYPE" == "2" ]]; then - GUEST_OS_TYPE="win10" - else - GUEST_OS_TYPE="l26" - fi - - - - qm create "$VMID" -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} \ - -cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" -tags proxmenux \ - -net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -ostype "$GUEST_OS_TYPE" \ - -scsihw virtio-scsi-pci \ - $( [[ -n "$SERIAL_PORT" ]] && echo "-serial0 $SERIAL_PORT" ) >/dev/null 2>&1 - - msg_ok "$(translate "Base VM created with ID") $VMID" - - - - if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then - msg_info "$(translate "Configuring EFI disk")" - EFI_STORAGE=$(select_storage_target "EFI" "$VMID") - EFI_DISK_NAME="vm-${VMID}-disk-efivars" - - STORAGE_TYPE=$(pvesm status -storage "$EFI_STORAGE" | awk 'NR>1 {print $2}') - case "$STORAGE_TYPE" in - nfs | dir) - EFI_DISK_EXT=".raw" - EFI_DISK_REF="$VMID/" - ;; - *) - EFI_DISK_EXT="" - EFI_DISK_REF="" - ;; - esac - - EFI_KEYS="0" - [[ "$OS_TYPE" == "2" ]] && EFI_KEYS="1" - - if pvesm alloc "$EFI_STORAGE" "$VMID" "$EFI_DISK_NAME$EFI_DISK_EXT" 4M >/dev/null 2>&1; then - if qm set "$VMID" -efidisk0 "$EFI_STORAGE:${EFI_DISK_REF}$EFI_DISK_NAME$EFI_DISK_EXT,pre-enrolled-keys=$EFI_KEYS" >/dev/null 2>&1; then - msg_ok "$(translate "EFI disk created and configured on") $EFI_STORAGE" - else - msg_error "$(translate "Failed to configure EFI disk")" - fi - else - msg_error "$(translate "Failed to create EFI disk")" - fi - fi - - - - - - - if [[ "$OS_TYPE" == "2" ]]; then - msg_info "$(translate "Configuring TPM device")" - TPM_STORAGE=$(select_storage_target "TPM" "$VMID") - TPM_NAME="vm-${VMID}-tpmstate" - - STORAGE_TYPE=$(pvesm status -storage "$TPM_STORAGE" | awk 'NR>1 {print $2}') - case "$STORAGE_TYPE" in - nfs | dir) - TPM_EXT=".raw" - TPM_REF="$VMID/" - ;; - *) - TPM_EXT="" - TPM_REF="" - ;; - esac - - TPM_FULL_NAME="${TPM_NAME}${TPM_EXT}" - - if pvesm alloc "$TPM_STORAGE" "$VMID" "$TPM_FULL_NAME" 4M >/dev/null 2>&1; then - TPM_PATH="$TPM_STORAGE:${TPM_REF}${TPM_FULL_NAME},size=4M,version=v2.0" - if qm set "$VMID" -tpmstate0 "$TPM_PATH" >/dev/null 2>&1; then - msg_ok "$(translate "TPM device added to VM")" - else - msg_error "$(translate "Failed to configure TPM device in VM") → $TPM_PATH" - fi - else - msg_error "$(translate "Failed to create TPM state disk")" - fi - fi - - - - - - - - -# ========================================================== -# Create Diks -# ========================================================== - - -select_interface_type - - if [[ "$DISK_TYPE" == "virtual" && ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then - for i in "${!VIRTUAL_DISKS[@]}"; do - DISK_INDEX=$((i+1)) - IFS=':' read -r STORAGE SIZE <<< "${VIRTUAL_DISKS[$i]}" - DISK_NAME="vm-${VMID}-disk-${DISK_INDEX}" - SLOT_NAME="${INTERFACE_TYPE}${i}" - - STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}') - case "$STORAGE_TYPE" in - dir|nfs) - DISK_EXT=".raw" - DISK_REF="$VMID/" - ;; - *) - DISK_EXT="" - DISK_REF="" - ;; - esac - - if pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME$DISK_EXT" "$SIZE"G >/dev/null 2>&1; then - qm set "$VMID" -$SLOT_NAME "$STORAGE:${DISK_REF}${DISK_NAME}${DISK_EXT}${DISCARD_OPTS}" >/dev/null - msg_ok "$(translate "Virtual disk") $DISK_INDEX ${SIZE}GB - $STORAGE ($SLOT_NAME)" - DISK_INFO+="

Virtual Disk $DISK_INDEX: ${SIZE}GB ($STORAGE / $SLOT_NAME)

" - [[ -z "$BOOT_ORDER" ]] && BOOT_ORDER="$SLOT_NAME" - else - msg_error "$(translate "Failed to create disk") $DISK_INDEX" - fi - done - fi - - if [[ "$DISK_TYPE" == "passthrough" && ${#PASSTHROUGH_DISKS[@]} -gt 0 ]]; then - for i in "${!PASSTHROUGH_DISKS[@]}"; do - SLOT_NAME="${INTERFACE_TYPE}${i}" - DISK="${PASSTHROUGH_DISKS[$i]}" - MODEL=$(lsblk -ndo MODEL "$DISK") - SIZE=$(lsblk -ndo SIZE "$DISK") - qm set "$VMID" -$SLOT_NAME "$DISK${DISCARD_OPTS}" >/dev/null 2>&1 - msg_ok "$(translate "Passthrough disk assigned") ($DISK → $SLOT_NAME)" - DISK_INFO+="

Passthrough Disk $((i+1)): $DISK ($MODEL $SIZE)

" - [[ -z "$BOOT_ORDER" ]] && BOOT_ORDER="$SLOT_NAME" - done - fi - - - - - - if [[ -f "$ISO_PATH" ]]; then - mount_iso_to_vm "$VMID" "$ISO_PATH" "ide2" - fi - - - if [[ "$OS_TYPE" == "2" ]]; then - local VIRTIO_DIR="/var/lib/vz/template/iso" - local VIRTIO_SELECTED="" - local VIRTIO_DOWNLOAD_URL="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso" - - while true; do - VIRTIO_OPTION=$(whiptail --title "ProxMenux - VirtIO Drivers" --menu "$(translate "Select how to provide VirtIO drivers")" 15 70 2 \ - "1" "$(translate "Download latest VirtIO ISO automatically")" \ - "2" "$(translate "Use existing VirtIO ISO from storage")" 3>&1 1>&2 2>&3) - - [[ $? -ne 0 ]] && msg_warn "$(translate "VirtIO ISO selection cancelled.")" && break - - case "$VIRTIO_OPTION" in - 1) - - if [[ -f "$VIRTIO_DIR/virtio-win.iso" ]]; then - if whiptail --title "ProxMenux" --yesno "$(translate "A VirtIO ISO already exists. Do you want to overwrite it?")" 10 60; then - wget -q --show-progress -O "$VIRTIO_DIR/virtio-win.iso" "$VIRTIO_DOWNLOAD_URL" - if [[ -f "$VIRTIO_DIR/virtio-win.iso" ]]; then - msg_ok "$(translate "VirtIO driver ISO downloaded successfully.")" - else - msg_error "$(translate "Failed to download VirtIO driver ISO.")" - fi - fi - else - wget -q --show-progress -O "$VIRTIO_DIR/virtio-win.iso" "$VIRTIO_DOWNLOAD_URL" - if [[ -f "$VIRTIO_DIR/virtio-win.iso" ]]; then - msg_ok "$(translate "VirtIO driver ISO downloaded successfully.")" - else - msg_error "$(translate "Failed to download VirtIO driver ISO.")" - fi - fi - - VIRTIO_SELECTED="$VIRTIO_DIR/virtio-win.iso" - ;; - 2) - - VIRTIO_LIST=() - while read -r line; do - FILENAME=$(basename "$line") - SIZE=$(du -h "$line" | cut -f1) - VIRTIO_LIST+=("$FILENAME" "$SIZE") - done < <(find "$VIRTIO_DIR" -type f -iname "virtio*.iso" | sort) - - if [[ ${#VIRTIO_LIST[@]} -eq 0 ]]; then - msg_warn "$(translate "No VirtIO ISO found. Please download one.")" - continue - fi - - VIRTIO_FILE=$(whiptail --title "ProxMenux - VirtIO ISOs" --menu "$(translate "Select a VirtIO ISO to use:")" 20 70 10 "${VIRTIO_LIST[@]}" 3>&1 1>&2 2>&3) - - if [[ -n "$VIRTIO_FILE" ]]; then - VIRTIO_SELECTED="$VIRTIO_DIR/$VIRTIO_FILE" - else - msg_warn "$(translate "No VirtIO ISO selected. Please choose again.")" - continue - fi - ;; - esac - - if [[ -n "$VIRTIO_SELECTED" && -f "$VIRTIO_SELECTED" ]]; then - mount_iso_to_vm "$VMID" "$VIRTIO_SELECTED" "ide3" - else - msg_warn "$(translate "VirtIO ISO not found after selection.")" - fi - - break - done - fi - - - local BOOT_FINAL="$BOOT_ORDER" - [[ -f "$ISO_PATH" ]] && BOOT_FINAL="$BOOT_ORDER;ide2" - qm set "$VMID" -boot order="$BOOT_FINAL" >/dev/null - msg_ok "$(translate "Boot order set to") $BOOT_FINAL" - - - - - HTML_DESC="
- - - - - -
-ProxMenux Logo - -

$HN VM

-

Created with ProxMenux

-
- -

-Docs -Code -Ko-fi -

- -
-${DISK_INFO} -
-
" - -msg_info "$(translate "Setting VM description")" -if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then - msg_error "$(translate "Failed to set VM description")" -else - msg_ok "$(translate "VM description configured")" -fi - - - if [[ "$START_VM" == "yes" ]]; then - qm start "$VMID" - msg_ok "$(translate "VM started")" - fi - configure_guest_agent - msg_success "$(translate "VM creation completed")" - -if [[ "$OS_TYPE" == "2" ]]; then - echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}" - echo -e "${TAB}1. $(translate "Start the VM to begin Windows installation from the mounted ISO.")" - echo -e "${TAB}2. $(translate "When asked to select a disk, click Load Driver and load the VirtIO drivers.")" - echo -e "${TAB} $(translate "Required if using a VirtIO or SCSI disk.")" - echo -e "${TAB}3. $(translate "Also install the VirtIO network driver during setup to enable network access.")" - echo -e "${TAB}4. $(translate "Continue the Windows installation as usual.")" - echo -e "${TAB}5. $(translate "Once installed, open the VirtIO ISO and run the installer to complete driver setup.")" - echo -e "${TAB}6. $(translate "Reboot the VM to complete the driver installation.")" - echo -e -elif [[ "$OS_TYPE" == "3" ]]; then - echo -e "${TAB}${GN}$(translate "Recommended: Install the QEMU Guest Agent in the VM")${CL}" - echo -e "${TAB}$(translate "Run the following inside the VM:")" - echo -e "${TAB}${CY}apt install qemu-guest-agent -y && systemctl enable --now qemu-guest-agent${CL}" - echo -e -fi - - -msg_success "$(translate "Press Enter to return to the main menu...")" -read -r - -} diff --git a/scripts/vm/zimaos.sh b/scripts/vm/zimaos.sh index 318b4dd4..c4c2c380 100644 --- a/scripts/vm/zimaos.sh +++ b/scripts/vm/zimaos.sh @@ -22,14 +22,33 @@ # Configuration ============================================ -LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +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="$BASE_DIR/utils.sh" +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 VENV_PATH="/opt/googletrans-env" 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 load_language initialize_cache # ========================================================== @@ -44,6 +63,10 @@ NEXTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100") NAME="ZimaOS VM" IMAGES_DIR="/var/lib/vz/template/iso" ERROR_FLAG=false +WIZARD_ADD_GPU="no" +WIZARD_GPU_RESULT="not_requested" +VM_WIZARD_CAPTURE_FILE="" +VM_WIZARD_CAPTURE_ACTIVE=0 @@ -64,15 +87,99 @@ function exit_script() { function header_info() { show_proxmenux_logo - echo -e "${BL}╔═══════════════════════════════════════════════╗${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}║${YWB} ZimaOS VM Creator ${BL}║${CL}" - echo -e "${BL}║ ║${CL}" - echo -e "${BL}╚═══════════════════════════════════════════════╝${CL}" - echo -e + msg_title "ZimaOS VM Creator" } # ========================================================== +function start_vm_wizard_capture() { + [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]] && return 0 + VM_WIZARD_CAPTURE_FILE="/tmp/proxmenux_zima_vm_wizard_capture_$$.txt" + : >"$VM_WIZARD_CAPTURE_FILE" + exec 8>&1 + exec > >(tee -a "$VM_WIZARD_CAPTURE_FILE") + VM_WIZARD_CAPTURE_ACTIVE=1 +} + +function stop_vm_wizard_capture() { + if [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]]; then + exec 1>&8 + exec 8>&- + VM_WIZARD_CAPTURE_ACTIVE=0 + fi + if [[ -n "${VM_WIZARD_CAPTURE_FILE:-}" && -f "$VM_WIZARD_CAPTURE_FILE" ]]; then + rm -f "$VM_WIZARD_CAPTURE_FILE" + fi + VM_WIZARD_CAPTURE_FILE="" +} + +function replay_vm_wizard_capture() { + if [[ "${VM_WIZARD_CAPTURE_ACTIVE:-0}" -eq 1 ]]; then + stop_spinner + exec 1>&8 + exec 8>&- + VM_WIZARD_CAPTURE_ACTIVE=0 + fi + + if [[ -n "${VM_WIZARD_CAPTURE_FILE:-}" && -f "$VM_WIZARD_CAPTURE_FILE" ]]; then + show_proxmenux_logo + cat "$VM_WIZARD_CAPTURE_FILE" + rm -f "$VM_WIZARD_CAPTURE_FILE" + fi + VM_WIZARD_CAPTURE_FILE="" +} + +function has_usable_gpu_for_vm_passthrough() { + lspci -nn 2>/dev/null \ + | grep -iE "VGA compatible controller|3D controller|Display controller" \ + | grep -ivE "Ethernet|Network|Audio" \ + | grep -ivE "ASPEED|AST[0-9]{3,4}|Matrox|G200e|BMC" \ + | grep -q . +} + +function prompt_optional_gpu_passthrough() { + WIZARD_ADD_GPU="no" + if has_usable_gpu_for_vm_passthrough; then + if whiptail --backtitle "ProxMenuX" --title "$(translate "Optional GPU Passthrough")" \ + --yesno "$(translate "Do you want to configure GPU passthrough for this VM now?")\n\n$(translate "This will launch the GPU assistant after VM creation and may require a host reboot.")" 12 78 --defaultno; then + WIZARD_ADD_GPU="yes" + fi + else + msg_warn "$(translate "No compatible GPU detected for VM passthrough. Skipping GPU wizard option.")" + fi + export WIZARD_ADD_GPU +} + +function run_gpu_passthrough_wizard() { + [[ "${WIZARD_ADD_GPU:-no}" != "yes" ]] && return 0 + + local gpu_script="$LOCAL_SCRIPTS/gpu_tpu/add_gpu_vm.sh" + local local_gpu_script + local wizard_result_file="" + + if [[ ! -f "$gpu_script" ]]; then + local_gpu_script="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/gpu_tpu/add_gpu_vm.sh" + [[ -f "$local_gpu_script" ]] && gpu_script="$local_gpu_script" + fi + + if [[ ! -f "$gpu_script" ]]; then + msg_warn "$(translate "GPU passthrough assistant not found. You can run it later from Hardware Graphics.")" + WIZARD_GPU_RESULT="cancelled" + return 0 + fi + + msg_info2 "$(translate "Launching GPU passthrough assistant for VM") ${VMID}..." + wizard_result_file="/tmp/proxmenux_gpu_wizard_result_${VMID}_$$.txt" + : >"$wizard_result_file" + bash "$gpu_script" --vmid "$VMID" --wizard --result-file "$wizard_result_file" + + if [[ -s "$wizard_result_file" ]]; then + WIZARD_GPU_RESULT=$(head -n1 "$wizard_result_file" | tr -d '\r\n') + else + WIZARD_GPU_RESULT="cancelled" + fi + rm -f "$wizard_result_file" +} + @@ -85,12 +192,13 @@ function start_script() { if (whiptail --backtitle "ProxMenuX" --title "SETTINGS" --yesno "$(translate "Use Default Settings?")" --no-button Advanced 10 58); then header_info echo -e "${DEF}Using Default Settings${CL}" - default_settings + default_settings || return 1 else header_info echo -e "${CUS}Using Advanced Settings${CL}" - advanced_settings + advanced_settings || return 1 fi + return 0 } # ========================================================== @@ -137,7 +245,7 @@ function default_settings() { echo -e "${DEF}Creating a $NAME using the above default settings${CL}" sleep 1 - select_disk_type + select_disk_type || return 1 } # ========================================================== @@ -315,7 +423,10 @@ function advanced_settings() { "$(translate "Select the bus type for the disks:")" 15 70 4 \ "scsi" "$(translate "SCSI (recommended for Linux)")" ON \ "sata" "$(translate "SATA (standard - high compatibility)")" OFF \ - 3>&1 1>&2 2>&3) || exit 1 + 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "Disk interface selection cancelled.")" + return 1 + } case "$INTERFACE_TYPE" in "scsi"|"sata") @@ -331,7 +442,7 @@ function advanced_settings() { echo -e echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}" sleep 1 - select_disk_type + select_disk_type || return 1 else header_info sleep 1 @@ -348,285 +459,253 @@ function advanced_settings() { # ========================================================== # Select Disk # ========================================================== -function select_disk_type() { +VIRTUAL_DISKS=() +IMPORT_DISKS=() +CONTROLLER_NVME_PCIS=() +PASSTHROUGH_DISKS=() - DISK_TYPE=$(whiptail --backtitle "ProxMenuX" --title "DISK TYPE" --menu "$(translate "Choose disk type:")" 12 58 2 \ - "virtual" "$(translate "Create virtual disk")" \ - "passthrough" "$(translate "Use physical disk passthrough")" \ - --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) - - EXIT_STATUS=$? - - if [[ $EXIT_STATUS -ne 0 ]]; then - clear - header_info - msg_error "Operation cancelled by user. Returning to start script..." - sleep 2 - if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then - start_script - else - clear - exit - fi - fi - - if [[ "$DISK_TYPE" == "virtual" ]]; then - select_virtual_disk - else - select_passthrough_disk - fi +function _build_storage_plan_summary() { + local separator + local summary + separator="$(printf '%*s' 70 '' | tr ' ' '-')" + summary="$(translate "Current selection:")\n" + summary+=" - $(translate "Virtual disks"): ${#VIRTUAL_DISKS[@]}\n" + summary+=" - $(translate "Import disks"): ${#IMPORT_DISKS[@]}\n" + summary+=" - $(translate "Controllers + NVMe"): ${#CONTROLLER_NVME_PCIS[@]}\n" + summary+="${separator}\n\n" + echo -e "$summary" } -# ========================================================== +function select_disk_type() { + VIRTUAL_DISKS=() + IMPORT_DISKS=() + CONTROLLER_NVME_PCIS=() + while true; do + local choice + choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \ + "1" "$(translate "Add virtual disk")" \ + "2" "$(translate "Add import disk")" \ + "3" "$(translate "Add Controller or NVMe (PCI passthrough)")" \ + "r" "$(translate "Reset current storage selection")" \ + "d" "$(translate "[ Finish and continue ]")" \ + --ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "Storage plan selection cancelled.")" + return 1 + } - - - -# ========================================================== -# Select Virtual Disks -# ========================================================== -function select_virtual_disk() { - - VIRTUAL_DISKS=() - - # Loop to add multiple disks - local add_more_disks=true - while $add_more_disks; do - - msg_info "Detecting available storage volumes..." - - # Get list of available storage - STORAGE_MENU=() - while read -r line; do - TAG=$(echo $line | awk '{print $1}') - TYPE=$(echo $line | awk '{print $2}') - FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf( "%9sB", $6)}') - ITEM=$(printf "%-15s %-10s %-15s" "$TAG" "$TYPE" "$FREE") - STORAGE_MENU+=("$TAG" "$ITEM" "OFF") - done < <(pvesm status -content images | awk 'NR>1') - - # Check that storage is available - VALID=$(pvesm status -content images | awk 'NR>1') - if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." - sleep 2 - select_disk_type - fi - - - # Select storage - if [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then - STORAGE=${STORAGE_MENU[0]} - msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location." - else - - kill $SPINNER_PID > /dev/null - STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "Select Storage Volume")" --radiolist \ - "$(translate "Choose the storage volume for the virtual disk:\n")" 20 78 10 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) - - if [ $? -ne 0 ] || [ -z "$STORAGE" ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "No storage selected. At least one disk is required." - select_disk_type - else - add_more_disks=false + case "$choice" in + 1) + select_virtual_disk + ;; + 2) + select_import_disk + ;; + 3) + select_controller_nvme + ;; + r) + VIRTUAL_DISKS=() + IMPORT_DISKS=() + CONTROLLER_NVME_PCIS=() + ;; + d|done) + if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then continue fi - fi - - - fi - - # Request disk size - DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 64 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) - - if [ $? -ne 0 ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "Disk size not specified. At least one disk is required." - sleep 2 - select_disk_type - - else - add_more_disks=false - continue - fi - fi - - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="64" - fi - - # Store the configuration in the disk list - VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") - - - # Ask if you want to create another disk - if ! whiptail --backtitle "ProxMenuX" --title "$(translate "Add Another Disk")" \ - --yesno "$(translate "Do you want to add another virtual disk?")" 8 58; then - add_more_disks=false - fi + if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then + msg_ok "$(translate "Virtual Disks Created:")" + for i in "${!VIRTUAL_DISKS[@]}"; do + echo -e "${TAB}${BL}- $(translate "Disk") $((i+1)): ${VIRTUAL_DISKS[$i]}GB${CL}" + done + fi + if [[ ${#IMPORT_DISKS[@]} -gt 0 ]]; then + msg_ok "$(translate "Import Disks Selected:")" + for i in "${!IMPORT_DISKS[@]}"; do + local disk_info + disk_info=$(lsblk -ndo MODEL,SIZE "${IMPORT_DISKS[$i]}" 2>/dev/null | xargs) + echo -e "${TAB}${BL}- $(translate "Disk") $((i+1)): ${IMPORT_DISKS[$i]}${disk_info:+ ($disk_info)}${CL}" + done + fi + if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then + msg_ok "$(translate "Controllers + NVMe Selected:")" + for i in "${!CONTROLLER_NVME_PCIS[@]}"; do + local pci_info + pci_info=$(lspci -nn -s "${CONTROLLER_NVME_PCIS[$i]#0000:}" 2>/dev/null | sed 's/^[^ ]* //') + echo -e "${TAB}${BL}- $(translate "Controller") $((i+1)): ${CONTROLLER_NVME_PCIS[$i]}${pci_info:+ ($pci_info)}${CL}" + done + fi + PASSTHROUGH_DISKS=("${IMPORT_DISKS[@]}") + DISK_TYPE="mixed" + export DISK_TYPE VIRTUAL_DISKS IMPORT_DISKS CONTROLLER_NVME_PCIS PASSTHROUGH_DISKS + select_loader || return 1 + return 0 + ;; + esac done - - # Show summary of the created disks - if [ ${#VIRTUAL_DISKS[@]} -gt 0 ]; then - - msg_ok "Virtual Disks Created:" - for i in "${!VIRTUAL_DISKS[@]}"; do - echo -e "${TAB}${BL}- Disk $((i+1)): ${VIRTUAL_DISKS[$i]}GB${CL}" - done - fi - - - export VIRTUAL_DISKS - - - select_loader } -# ========================================================== +function select_virtual_disk() { + msg_info "Detecting available storage volumes..." + local STORAGE_MENU=() + local TAG TYPE FREE ITEM + while read -r line; do + TAG=$(echo $line | awk '{print $1}') + TYPE=$(echo $line | awk '{print $2}') + FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format "%.2f" | awk '{printf( "%9sB", $6)}') + ITEM=$(printf "%-15s %-10s %-15s" "$TAG" "$TYPE" "$FREE") + STORAGE_MENU+=("$TAG" "$ITEM" "OFF") + done < <(pvesm status -content images | awk 'NR>1') + local VALID + VALID=$(pvesm status -content images | awk 'NR>1') + if [[ -z "$VALID" ]]; then + msg_error "Unable to detect a valid storage location." + return 1 + fi + local STORAGE + if [[ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]]; then + STORAGE=${STORAGE_MENU[0]} + else + [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" >/dev/null 2>&1 + STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "Select Storage Volume")" --radiolist \ + "$(translate "Choose the storage volume for the virtual disk:\n")" 20 78 10 \ + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || return 0 + [[ -z "$STORAGE" ]] && return 0 + fi + local DISK_SIZE + stop_spinner + DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 64 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) || return 0 + [[ -z "$DISK_SIZE" ]] && DISK_SIZE="64" + VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}") +} -# ========================================================== -# Select Physical Disks -# ========================================================== -function select_passthrough_disk() { - +function select_import_disk() { msg_info "$(translate "Detecting available disks...")" + _refresh_host_storage_cache - FREE_DISKS=() - - USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}') - MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}') - - ZFS_DISKS="" - ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror') - - for entry in $ZFS_RAW; do - path="" - if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then - if [ -e "/dev/disk/by-id/$entry" ]; then - path=$(readlink -f "/dev/disk/by-id/$entry") - fi - elif [[ "$entry" == /dev/* ]]; then - path="$entry" - fi - - if [ -n "$path" ]; then - base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null) - if [ -n "$base_disk" ]; then - ZFS_DISKS+="/dev/$base_disk"$'\n' - fi - fi - done - - ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u) - LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u) - - RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u) - + local FREE_DISKS=() + local DISK INFO MODEL SIZE LABEL DESCRIPTION while read -r DISK; do - [[ "$DISK" =~ /dev/zd ]] && continue + [[ "$DISK" =~ /dev/zd ]] && continue + _disk_is_host_system_used "$DISK" && continue - INFO=($(lsblk -dn -o MODEL,SIZE "$DISK")) - MODEL="${INFO[@]::${#INFO[@]}-1}" - SIZE="${INFO[-1]}" - LABEL="" - SHOW_DISK=true - - IS_MOUNTED=false - IS_RAID=false - IS_ZFS=false - IS_LVM=false - - while read -r part fstype; do - [[ "$fstype" == "zfs_member" ]] && IS_ZFS=true - [[ "$fstype" == "linux_raid_member" ]] && IS_RAID=true - [[ "$fstype" == "LVM2_member" ]] && IS_LVM=true - if grep -q "/dev/$part" <<< "$MOUNTED_DISKS"; then - IS_MOUNTED=true - fi - done < <(lsblk -ln -o NAME,FSTYPE "$DISK" | tail -n +2) - - REAL_PATH=$(readlink -f "$DISK") - if echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then - IS_MOUNTED=true - fi - - USED_BY="" - REAL_PATH=$(readlink -f "$DISK") - CONFIG_DATA=$(cat /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null) - - if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then - USED_BY="⚠ $(translate "In use")" - else - for SYMLINK in /dev/disk/by-id/*; do - if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then - if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then - USED_BY="⚠ $(translate "In use")" - break - fi - fi - done - fi - - if $IS_RAID && grep -q "$DISK" <<< "$(cat /proc/mdstat)" && grep -q "active raid" /proc/mdstat; then - SHOW_DISK=false - fi - - if $IS_ZFS || $IS_MOUNTED || [[ "$ZFS_DISKS" == *"$DISK"* ]]; then - SHOW_DISK=false - fi - - if $SHOW_DISK; then - [[ -n "$USED_BY" ]] && LABEL+=" [$USED_BY]" - [[ "$IS_RAID" == true ]] && LABEL+=" ⚠ RAID" - [[ "$IS_LVM" == true ]] && LABEL+=" ⚠ LVM" - [[ "$IS_ZFS" == true ]] && LABEL+=" ⚠ ZFS" - DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") - FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") - fi + INFO=($(lsblk -dn -o MODEL,SIZE "$DISK")) + MODEL="${INFO[@]::${#INFO[@]}-1}" + SIZE="${INFO[-1]}" + LABEL="" + if _disk_used_in_guest_configs "$DISK"; then + LABEL+=" [⚠ $(translate "In use by VM/LXC config")]" + fi + DESCRIPTION=$(printf "%-30s %10s%s" "$MODEL" "$SIZE" "$LABEL") + if _array_contains "$DISK" "${IMPORT_DISKS[@]}"; then + FREE_DISKS+=("$DISK" "$DESCRIPTION" "ON") + else + FREE_DISKS+=("$DISK" "$DESCRIPTION" "OFF") + fi done < <(lsblk -dn -e 7,11 -o PATH) - - if [ "${#FREE_DISKS[@]}" -eq 0 ]; then - cleanup - whiptail --title "Error" --msgbox "$(translate "No disks available for this VM.")" 8 40 - select_disk_type - return + if [[ ${#FREE_DISKS[@]} -eq 0 ]]; then + whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70 + return 1 fi - MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1) - TOTAL_WIDTH=$((MAX_WIDTH + 20)) - [ $TOTAL_WIDTH -lt 50 ] && TOTAL_WIDTH=50 - cleanup - SELECTED_DISKS=$(whiptail --title "Select Disks" --checklist \ - "$(translate "Select the disks you want to use (use spacebar to select):")" 20 $TOTAL_WIDTH 10 \ - "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) + local selected + selected=$(whiptail --title "Select Import Disks" --checklist \ + "$(translate "Select the disks you want to import (use spacebar to toggle):")" 20 78 10 \ + "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3) || return 1 - if [ -z "$SELECTED_DISKS" ]; then - msg_error "Disk not specified. At least one disk is required." - sleep 2 - select_disk_type - return - fi - - - msg_ok "Disk passthrough selected:" - PASSTHROUGH_DISKS=() - for DISK in $(echo "$SELECTED_DISKS" | tr -d '"'); do - DISK_INFO=$(lsblk -ndo MODEL,SIZE "$DISK" | xargs) - echo -e "${TAB}${CL}${BL}- $DISK $DISK_INFO${GN}${CL}" - PASSTHROUGH_DISKS+=("$DISK") + IMPORT_DISKS=() + local item + for item in $(echo "$selected" | tr -d '"'); do + IMPORT_DISKS+=("$item") done + export IMPORT_DISKS +} - - select_loader +function select_controller_nvme() { + msg_info "$(translate "Detecting PCI storage controllers and NVMe devices...")" + _refresh_host_storage_cache + + local menu_items=() + local blocked_report="" + local safe_count=0 blocked_count=0 + local pci_path pci_full class_hex name controller_disks disk reason state controller_desc + + 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" || "${class_hex:0:2}" != "01" ]] && continue + + 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") + + reason="" + for disk in "${controller_disks[@]}"; do + if _disk_is_host_system_used "$disk"; then + reason+="${disk} (${DISK_USAGE_REASON}); " + elif _disk_used_in_guest_configs "$disk"; then + reason+="${disk} ($(translate "In use by VM/LXC config")); " + fi + done + + if [[ -n "$reason" ]]; then + blocked_count=$((blocked_count + 1)) + blocked_report+=" • ${pci_full} — ${name}\n $(translate "Blocked because protected/in-use disks are attached"): ${reason}\n" + continue + fi + + if [[ ${#controller_disks[@]} -gt 0 ]]; then + controller_desc="$(printf "%-48s [%s]" "$name" "$(IFS=,; echo "${controller_disks[*]}")")" + else + controller_desc="$(printf "%-48s [%s]" "$name" "$(translate "No attached disks detected")")" + fi + + if _array_contains "$pci_full" "${CONTROLLER_NVME_PCIS[@]}"; then + state="ON" + else + state="OFF" + fi + + 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 + whiptail --title "Controller + NVMe" --msgbox "$(translate "No safe controllers/NVMe devices are available for passthrough.")\n\n${blocked_report}" 20 90 + return 1 + fi + + if [[ $blocked_count -gt 0 ]]; then + whiptail --title "Controller + NVMe" --msgbox "$(translate "Some controllers were hidden because they have host system disks attached.")\n\n${blocked_report}" 20 90 + fi + + local selected + selected=$(whiptail --title "Controller + NVMe" --checklist \ + "$(translate "Select controllers/NVMe to passthrough (safe devices only):")" 20 90 10 \ + "${menu_items[@]}" 3>&1 1>&2 2>&3) || return 1 + + CONTROLLER_NVME_PCIS=() + local pci + for pci in $(echo "$selected" | tr -d '"'); do + CONTROLLER_NVME_PCIS+=("$pci") + done + export CONTROLLER_NVME_PCIS +} + +function select_passthrough_disk() { + select_import_disk } # ========================================================== @@ -866,23 +945,27 @@ function select_efi_storage() { VALID=$(pvesm status -content images | awk 'NR>1') if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location for EFI disk." + msg_error "Unable to detect a valid storage location for EFI disk." >&2 + return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} else - kill $SPINNER_PID > /dev/null + [[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" > /dev/null 2>&1 while [ -z "${STORAGE:+x}" ]; do STORAGE=$(whiptail --backtitle "ProxMenuX" --title "EFI Disk Storage" --radiolist \ "$(translate "Choose the storage volume for the EFI disk (4MB):\n\nUse Spacebar to select.")" \ 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "EFI storage selection cancelled.")" >&2 + return 1 + } done fi - + [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } # ========================================================== @@ -917,8 +1000,8 @@ function select_storage_volume() { VALID=$(pvesm status -content images | awk 'NR>1') if [ -z "$VALID" ]; then - msg_error "Unable to detect a valid storage location." - exit 1 + msg_error "Unable to detect a valid storage location." >&2 + return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} else @@ -926,10 +1009,13 @@ function select_storage_volume() { STORAGE=$(whiptail --backtitle "ProxMenuX" --title "Storage Pools" --radiolist \ "$(translate "Choose the storage volume for $purpose:")" \ 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit + "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || { + msg_warn "$(translate "Storage selection cancelled for $purpose.")" >&2 + return 1 + } done fi - + [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } @@ -944,7 +1030,7 @@ function create_vm() { # Create the VM qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \ - -name $HN -tags proxmenux -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \ + -name $HN -tags proxmenux,nas -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \ -serial0 socket msg_ok "Create a $NAME" @@ -954,7 +1040,10 @@ function create_vm() { if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then msg_info "Configuring EFI disk" - EFI_STORAGE=$(select_efi_storage $VMID) + if ! EFI_STORAGE=$(select_efi_storage $VMID); then + msg_error "EFI storage selection failed or was cancelled." + return 1 + fi EFI_DISK_NAME="vm-${VMID}-disk-efivars" # Determine storage type and extension @@ -1003,7 +1092,10 @@ function create_vm() { fi # Select storage volume for loader ======================= - LOADER_STORAGE=$(select_storage_volume $VMID "loader disk") + if ! LOADER_STORAGE=$(select_storage_volume $VMID "loader disk"); then + msg_error "Loader storage selection failed or was cancelled." + return 1 + fi #Run the command in the background and capture its PID qm importdisk $VMID ${LOADER_FILE} $LOADER_STORAGE > /tmp/import_log_$VMID.txt 2>&1 & @@ -1059,18 +1151,14 @@ function create_vm() { fi fi - if [ "$DISK_TYPE" = "virtual" ]; then - if [ ${#VIRTUAL_DISKS[@]} -eq 0 ]; then - msg_error "No virtual disks configured." - exit_script - fi - - DISK_INFO="" - CONSOLE_DISK_INFO="" + DISK_INFO="" + CONSOLE_DISK_INFO="" + DISK_SLOT_INDEX=0 + if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then for i in "${!VIRTUAL_DISKS[@]}"; do IFS=':' read -r STORAGE SIZE <<< "${VIRTUAL_DISKS[$i]}" - + STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}') case $STORAGE_TYPE in nfs | dir) @@ -1082,14 +1170,12 @@ function create_vm() { DISK_REF="" ;; esac - - DISK_NUM=$((i+1)) + + DISK_NUM=$((DISK_SLOT_INDEX+1)) DISK_NAME="vm-${VMID}-disk-${DISK_NUM}${DISK_EXT}" - INTERFACE_ID="${INTERFACE_TYPE}$i" - - # Create virtual disk + INTERFACE_ID="${INTERFACE_TYPE}${DISK_SLOT_INDEX}" + if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then - msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..." if ! qm set "$VMID" -$INTERFACE_ID "$STORAGE:$SIZE,format=raw$DISCARD_OPTS" >/dev/null 2>&1; then msg_error "Failed to assign disk $DISK_NUM ($INTERFACE_ID) on $STORAGE" @@ -1097,7 +1183,6 @@ function create_vm() { continue fi else - msg_info "Allocating virtual disk for $STORAGE_TYPE..." if ! pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME" "$SIZE"G >/dev/null 2>&1; then msg_error "Failed to allocate virtual disk $DISK_NUM" @@ -1112,16 +1197,86 @@ function create_vm() { fi msg_ok "Configured virtual disk as $INTERFACE_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}" - BOOT_ORDER_LIST+=("$INTERFACE_ID") - - # Add information to the description - DISK_INFO="${DISK_INFO}

Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} (${INTERFACE_TYPE}${i})

" + DISK_INFO="${DISK_INFO}

Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} (${INTERFACE_ID})

" CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} ($INTERFACE_ID)\n" + DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1)) done + fi + EFFECTIVE_IMPORT_DISKS=() + if [[ ${#IMPORT_DISKS[@]} -gt 0 ]]; then + EFFECTIVE_IMPORT_DISKS=("${IMPORT_DISKS[@]}") + elif [[ ${#PASSTHROUGH_DISKS[@]} -gt 0 ]]; then + EFFECTIVE_IMPORT_DISKS=("${PASSTHROUGH_DISKS[@]}") + fi + + if [[ ${#EFFECTIVE_IMPORT_DISKS[@]} -gt 0 ]]; then + for DISK in "${EFFECTIVE_IMPORT_DISKS[@]}"; do + INTERFACE_ID="${INTERFACE_TYPE}${DISK_SLOT_INDEX}" + MODEL=$(lsblk -ndo MODEL "$DISK" 2>/dev/null || echo "Unknown") + SIZE=$(lsblk -ndo SIZE "$DISK" 2>/dev/null || echo "Unknown") + + if qm set "$VMID" -$INTERFACE_ID "$DISK$DISCARD_OPTS" >/dev/null 2>&1; then + msg_ok "Configured import disk as $INTERFACE_ID: $DISK ($MODEL $SIZE)" + BOOT_ORDER_LIST+=("$INTERFACE_ID") + DISK_INFO="${DISK_INFO}

Import Disk $((DISK_SLOT_INDEX+1)): $DISK ($MODEL $SIZE) (${INTERFACE_ID})

" + CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Import Disk $((DISK_SLOT_INDEX+1)): $DISK ($MODEL $SIZE) ($INTERFACE_ID)\n" + DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1)) + else + msg_error "Failed to configure import disk $DISK as $INTERFACE_ID" + ERROR_FLAG=true + fi + done + fi + + if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then + if ! _vm_is_q35 "$VMID"; then + msg_error "$(translate "Controller + NVMe passthrough requires machine type q35. Skipping controller assignment.")" + ERROR_FLAG=true + else + HOSTPCI_INDEX=0 + if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then + HOSTPCI_INDEX=$(_pci_next_hostpci_index "$VMID" 2>/dev/null || echo 0) + else + while qm config "$VMID" | grep -q "^hostpci${HOSTPCI_INDEX}:"; do + HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1)) + done + fi + + for PCI_DEV in "${CONTROLLER_NVME_PCIS[@]}"; do + if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then + if _pci_function_assigned_to_vm "$PCI_DEV" "$VMID"; then + msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + continue + fi + else + local PCI_BDF + PCI_BDF="${PCI_DEV#0000:}" + if qm config "$VMID" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*(0000:)?${PCI_BDF}([,[:space:]]|$)"; then + msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})" + continue + fi + fi + + if qm set "$VMID" --hostpci${HOSTPCI_INDEX} "${PCI_DEV},pcie=1" >/dev/null 2>&1; then + msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}" + DISK_INFO="${DISK_INFO}

Controller/NVMe: ${PCI_DEV}

" + CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n" + HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1)) + else + msg_error "Failed to configure controller/NVMe: ${PCI_DEV}" + ERROR_FLAG=true + fi + done + fi + fi + + if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#EFFECTIVE_IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then + msg_error "No disks/controllers configured." + exit_script + fi - # HTML description HTML_DESC="
@@ -1147,7 +1302,7 @@ HTML_DESC="
${DISK_INFO}
" - + msg_info "Setting VM description" if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then msg_error "Failed to set VM description" @@ -1155,102 +1310,50 @@ ${DISK_INFO} fi msg_ok "Configured VM description" - - - elif [ "$DISK_TYPE" = "passthrough" ]; then - if [ ${#PASSTHROUGH_DISKS[@]} -eq 0 ]; then - msg_error "No passthrough disks configured." - exit_script - fi - - DISK_INFO="" - CONSOLE_DISK_INFO="" - - for i in "${!PASSTHROUGH_DISKS[@]}"; do - DISK="${PASSTHROUGH_DISKS[$i]}" - INTERFACE_ID="${INTERFACE_TYPE}$i" - - # Get disk information - MODEL=$(lsblk -ndo MODEL "$DISK" 2>/dev/null || echo "Unknown") - SIZE=$(lsblk -ndo SIZE "$DISK" 2>/dev/null || echo "Unknown") - - # Configure passthrough disk - if qm set "$VMID" -$INTERFACE_ID "$DISK$DISCARD_OPTS" >/dev/null 2>&1; then - msg_ok "Configured passthrough disk as $INTERFACE_ID: $DISK ($MODEL $SIZE)" - BOOT_ORDER_LIST+=("$INTERFACE_ID") - - # Add information to the description - DISK_INFO="${DISK_INFO}

Passthrough Disk $((i+1)): $DISK ($MODEL $SIZE) (${INTERFACE_TYPE}${i})

" - CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Passthrough Disk $((i+1)): $DISK ($MODEL $SIZE) ($INTERFACE_ID)\n" - else - msg_error "Failed to configure passthrough disk $DISK as $INTERFACE_ID" - ERROR_FLAG=true - fi - done - - - # HTML description -HTML_DESC="
-
- - - - -
-ProxMenux Logo - -

ZimaOS VM

-

Created with ProxMenuX

-

-

- -

-Docs -Code -Loader -Ko-fi -

- -
-${DISK_INFO} -
-
" - - - result=$(qm set $VMID -description "$HTML_DESC" 2>&1) - if [[ $? -eq 0 ]]; then - msg_ok "Configured VM description" - fi - - + if [ ${#BOOT_ORDER_LIST[@]} -gt 0 ]; then + BOOT_ORDER_LIST+=("ide0") + BOOT_ORDER_STRING=$(IFS=';'; echo "${BOOT_ORDER_LIST[*]}") + else + BOOT_ORDER_STRING="ide0" fi - - - - - if [ ${#BOOT_ORDER_LIST[@]} -gt 0 ]; then - BOOT_ORDER_LIST+=("ide0") # Add IDE installer as last boot option - BOOT_ORDER_STRING=$(IFS=';'; echo "${BOOT_ORDER_LIST[*]}") - - if qm set "$VMID" -boot order="$BOOT_ORDER_STRING" >/dev/null 2>&1; then - msg_ok "Boot order configured: $BOOT_ORDER_STRING" - else - msg_error "Failed to configure boot order" - ERROR_FLAG=true - fi + if qm set "$VMID" -boot order="$BOOT_ORDER_STRING" >/dev/null 2>&1; then + msg_ok "Boot order configured: $BOOT_ORDER_STRING" + else + msg_error "Failed to configure boot order" + ERROR_FLAG=true fi if [ "$ERROR_FLAG" = true ]; then msg_error "VM created with errors. Check configuration." else - msg_success "$(translate "Completed Successfully!")" + if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then + WIZARD_GPU_RESULT="cancelled" + run_gpu_passthrough_wizard + replay_vm_wizard_capture + fi + + if [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$WIZARD_GPU_RESULT" == "applied" ]]; then + msg_success "$(translate "Completed Successfully with GPU passthrough configured!")" + else + msg_success "$(translate "Completed Successfully!")" + if [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$WIZARD_GPU_RESULT" == "no_gpu" ]]; then + msg_warn "$(translate "GPU passthrough was skipped (no compatible GPU detected).")" + elif [[ "${WIZARD_ADD_GPU:-no}" == "yes" && "$WIZARD_GPU_RESULT" != "applied" ]]; then + msg_warn "$(translate "GPU passthrough was not applied.")" + fi + fi echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}" echo -e "${TAB}1. $(translate "Start the VM")" echo -e "${TAB}2. $(translate "Open the VM console and wait for the installer to boot")" echo -e "${TAB}3. $(translate "Complete the ZimaOS installation wizard")" + if [[ "$WIZARD_GPU_RESULT" == "applied" ]]; then + echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")" + echo -e "${TAB}• $(translate "First complete ZimaOS setup and verify remote access (web/SSH).")" + echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")" + fi echo -e @@ -1267,13 +1370,22 @@ sleep 1 if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then - start_script + start_vm_wizard_capture + if ! start_script; then + stop_vm_wizard_capture + msg_warn "$(translate "VM creation cancelled before disk planning.")" + exit 0 + fi + prompt_optional_gpu_passthrough + if [[ "${WIZARD_ADD_GPU:-no}" != "yes" ]]; then + stop_vm_wizard_capture + fi else clear exit fi - create_vm +stop_vm_wizard_capture # ==========================================================