#!/usr/bin/env bash # ========================================================== # ProxMenuX - Synology DSM VM 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: 13/03/2025 # ========================================================== # Description: # This script automates the creation and configuration of a Synology DSM # (DiskStation Manager) virtual machine (VM) in Proxmox VE. It simplifies the # setup process by allowing both default and advanced configuration options. # # The script automates the complete VM creation process, including loader # download, disk configuration, and VM boot setup. # # **Credits** # This script is an original idea but incorporates ideas and elements from # a similar script by user **tim104979** from the ProxmoxVE branch: # (https://raw.githubusercontent.com/tim104979/ProxmoxVE/refs/heads/main/vm/synology-vm.sh) # # Copyright (c) Proxmox VE Helper-Scripts Community # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE # # ========================================================== # Configuration ============================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)" LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts" LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$LOCAL_SCRIPTS/utils.sh" if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL" UTILS_FILE="$LOCAL_SCRIPTS/utils.sh" elif [[ ! -f "$UTILS_FILE" ]]; then UTILS_FILE="$BASE_DIR/utils.sh" fi 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 # ========================================================== GEN_MAC="02" for i in {1..5}; do BYTE=$(printf "%02X" $((RANDOM % 256))) GEN_MAC="${GEN_MAC}:${BYTE}" done 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 function exit_script() { clear if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then start_script else clear exit fi } # Define the header_info function at the beginning of the script function header_info() { clear show_proxmenux_logo 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" } # ========================================================== # start Script # ========================================================== 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 || return 1 else header_info echo -e "${CUS}Using Advanced Settings${CL}" advanced_settings || return 1 fi return 0 } # ========================================================== # ========================================================== # Default Settings # ========================================================== function default_settings() { VMID="$NEXTID" FORMAT="" MACHINE=" -machine q35" BIOS_TYPE=" -bios ovmf" DISK_CACHE="" HN="Synology-DSM" CPU_TYPE=" -cpu host" CORE_COUNT="2" RAM_SIZE="4096" BRG="vmbr0" MAC="$GEN_MAC" VLAN="" MTU="" SERIAL_PORT="socket" START_VM="no" echo -e " ${TAB}${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}" echo -e " ${TAB}${DGN}Using Machine Type: ${BGN}q35${CL}" echo -e " ${TAB}${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" echo -e " ${TAB}${DGN}Using Hostname: ${BGN}${HN}${CL}" echo -e " ${TAB}${DGN}Using CPU Model: ${BGN}Host${CL}" echo -e " ${TAB}${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}" echo -e " ${TAB}${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}" echo -e " ${TAB}${DGN}Using Bridge: ${BGN}${BRG}${CL}" echo -e " ${TAB}${DGN}Using MAC Address: ${BGN}${MAC}${CL}" echo -e " ${TAB}${DGN}Using VLAN: ${BGN}Default${CL}" echo -e " ${TAB}${DGN}Using Interface MTU Size: ${BGN}Default${CL}" echo -e " ${TAB}${DGN}Configuring Serial Port: ${BGN}${SERIAL_PORT}${CL}" echo -e " ${TAB}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}" echo -e echo -e "${DEF}Creating a $NAME using the above default settings${CL}" sleep 1 select_disk_type || return 1 } # ========================================================== # ========================================================== # advanced Settings # ========================================================== function advanced_settings() { # VM ID Selection while true; do if VMID=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Virtual Machine ID")" 8 58 $NEXTID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z "$VMID" ]; then VMID="$NEXTID" fi if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then echo -e "${CROSS}${RD} ID $VMID is already in use${CL}" sleep 1 continue fi echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}" break else exit_script fi done # Machine Type Selection if MACH=$(whiptail --backtitle "ProxMenuX" --title "$(translate "MACHINE TYPE")" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \ "q35" "Machine q35" ON \ "i440fx" "Machine i440fx" OFF \ 3>&1 1>&2 2>&3); then if [ $MACH = q35 ]; then echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" FORMAT="" MACHINE=" -machine q35" else echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}" FORMAT=",efitype=4m" MACHINE="" fi else exit_script fi # BIOS Type Selection if BIOS=$(whiptail --backtitle "ProxMenuX" --title "$(translate "BIOS TYPE")" --radiolist --cancel-button Exit-Script "Choose BIOS Type" 10 58 2 \ "ovmf" "UEFI (OVMF)" ON \ "seabios" "SeaBIOS (Legacy)" OFF \ 3>&1 1>&2 2>&3); then if [ "$BIOS" = "seabios" ]; then echo -e "${DGN}Using BIOS Type: ${BGN}SeaBIOS${CL}" BIOS_TYPE=" -bios seabios" else echo -e "${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}" BIOS_TYPE=" -bios ovmf" fi else exit_script fi # Hostname Selection if VM_NAME=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Hostname")" 8 58 Synology-DSM --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $VM_NAME ]; then HN="Synology-DSM" echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" else HN=$(echo ${VM_NAME,,} | tr -d ' ') echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" fi else exit_script fi # CPU Type Selection if CPU_TYPE1=$(whiptail --backtitle "ProxMenuX" --title "$(translate "CPU MODEL")" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \ "1" "Host" ON \ "0" "KVM64" OFF \ 3>&1 1>&2 2>&3); then if [ $CPU_TYPE1 = "1" ]; then echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}" CPU_TYPE=" -cpu host" else echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}" CPU_TYPE="" fi else exit_script fi # Core Count Selection if CORE_COUNT=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate CPU Cores")" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $CORE_COUNT ]; then CORE_COUNT="2" echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" else echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" fi else exit_script fi # RAM Size Selection if RAM_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate RAM in MiB")" 8 58 4096 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $RAM_SIZE ]; then RAM_SIZE="4096" echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" else echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" fi else exit_script fi # Bridge Selection if BRG=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Bridge")" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $BRG ]; then BRG="vmbr0" echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" else echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" fi else exit_script fi # MAC Address Selection if MAC1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a MAC Address")" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $MAC1 ]; then MAC="$GEN_MAC" echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}" else MAC="$MAC1" echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}" fi else exit_script fi # VLAN Selection if VLAN1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Vlan(leave blank for default)")" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $VLAN1 ]; then VLAN1="Default" VLAN="" echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" else VLAN=",tag=$VLAN1" echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" fi else exit_script fi # MTU Selection if MTU1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Interface MTU Size (leave blank for default)")" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [ -z $MTU1 ]; then MTU1="Default" MTU="" echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" else MTU=",mtu=$MTU1" echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" fi else exit_script fi # Confirmation if (whiptail --backtitle "ProxMenuX" --title "$(translate "ADVANCED SETTINGS COMPLETE")" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then echo -e echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}" sleep 1 select_disk_type || return 1 else header_info sleep 1 echo -e "${CUS}Using Advanced Settings${CL}" advanced_settings fi } # ========================================================== # ========================================================== # Select Disk # ========================================================== VIRTUAL_DISKS=() IMPORT_DISKS=() CONTROLLER_NVME_PCIS=() PASSTHROUGH_DISKS=() 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 } 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 select_loader || return 1 return 0 ;; esac done } 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}") } function select_import_disk() { msg_info "$(translate "Detecting available disks...")" _refresh_host_storage_cache local FREE_DISKS=() local DISK INFO MODEL SIZE LABEL DESCRIPTION while read -r DISK; do [[ "$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="" 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 whiptail --title "Error" --msgbox "$(translate "No importable disks available. System disks and protected disks are hidden.")" 9 70 return 1 fi 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 IMPORT_DISKS=() local item for item in $(echo "$selected" | tr -d '"'); do IMPORT_DISKS+=("$item") done export IMPORT_DISKS } 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 } # ========================================================== # ========================================================== # Select Loader # ========================================================== function select_loader() { # Ensure the images directory exists if [ ! -d "$IMAGES_DIR" ]; then msg_info "Creating images directory" mkdir -p "$IMAGES_DIR" chmod 755 "$IMAGES_DIR" msg_ok "Images directory created: $IMAGES_DIR" fi # Create the loader selection menu LOADER_OPTION=$(whiptail --backtitle "ProxMenuX" --title "SELECT LOADER" --menu "$(translate "Choose a loader for Synology DSM:")" 15 70 4 \ "1" "AuxXxilium Arc Loader" \ "2" "RedPill Loader (RROrg - RR)" \ "3" "TinyCore RedPill Loader (PeterSuh-Q3 M-shell)" \ "4" "Custom Loader Image (from $IMAGES_DIR)" \ 3>&1 1>&2 2>&3) if [ -z "$LOADER_OPTION" ]; then exit_script fi case $LOADER_OPTION in 1) LOADER_TYPE="arc" LOADER_NAME="AuxXxilium Arc" LOADER_URL="https://github.com/AuxXxilium/arc/" echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" download_loader ;; 2) LOADER_TYPE="redpill" LOADER_NAME="RedPill RR" LOADER_URL="https://github.com/RROrg/rr/" echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" download_loader ;; 3) LOADER_TYPE="tinycore" LOADER_NAME="TinyCore RedPill M-shell" LOADER_URL="https://github.com/PeterSuh-Q3/tinycore-redpill/" echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" download_loader ;; 4) LOADER_TYPE="custom" LOADER_NAME="Custom Image" LOADER_URL="https://xpenology.com/forum/" echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}" select_custom_image ;; esac } function select_custom_image() { # Check if there are any images in the directory IMAGES=$(find "$IMAGES_DIR" -type f -name "*.img" -o -name "*.iso" -o -name "*.qcow2" -o -name "*.vmdk" | sort) if [ -z "$IMAGES" ]; then whiptail --title "$(translate "No Images Found")" --msgbox "No compatible images found in $IMAGES_DIR\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again." 15 70 select_loader fi # Create an array of image options for whiptail IMAGE_OPTIONS=() while read -r img; do filename=$(basename "$img") filesize=$(du -h "$img" | cut -f1) IMAGE_OPTIONS+=("$img" "$filesize") done <<< "$IMAGES" # Let the user select an image LOADER_FILE=$(whiptail --backtitle "ProxMenuX" --title "SELECT CUSTOM IMAGE" --menu "$(translate "Choose a custom image:")" 20 70 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3) if [ -z "$LOADER_FILE" ]; then msg_error "No custom image selected" exit_script fi echo -e "${DGN}${TAB}Using Custom Image: ${BGN}$(basename "$LOADER_FILE")${CL}" FILE=$(basename "$LOADER_FILE") } # ========================================================== # ========================================================== # Download Loader # ========================================================== function download_loader() { echo -e "${DGN}${TAB}Retrieving the URL for the ${BGN}$LOADER_NAME loader${CL}" if [[ "$LOADER_TYPE" == "arc" || "$LOADER_TYPE" == "redpill" ]] && ! command -v unzip &> /dev/null; then msg_info "Installing unzip..." apt-get update -qq && apt-get install -y unzip -qq >/dev/null 2>&1 if ! command -v unzip &> /dev/null; then msg_error "Failed to install unzip" sleep 2 return 1 fi msg_ok "Installed unzip successfully." fi case $LOADER_TYPE in arc) curl -s https://api.github.com/repos/AuxXxilium/arc/releases/latest \ | grep "browser_download_url.*\.img\.zip" \ | cut -d '"' -f 4 \ | xargs wget -q --show-progress -O "$IMAGES_DIR/arc.img.zip" if [ -f "$IMAGES_DIR/arc.img.zip" ]; then cd "$IMAGES_DIR" unzip -q arc.img.zip rm arc.img.zip FILE="arc.img" LOADER_FILE="$IMAGES_DIR/$FILE" cd - > /dev/null else msg_error "Failed to download $LOADER_NAME loader" sleep 1 select_loader fi ;; redpill) curl -s https://api.github.com/repos/RROrg/rr/releases/latest \ | grep "browser_download_url.*\.img\.zip" \ | cut -d '"' -f 4 \ | xargs wget -q --show-progress -O "$IMAGES_DIR/rr.img.zip" if [ -f "$IMAGES_DIR/rr.img.zip" ]; then cd "$IMAGES_DIR" msg_info "Unzipping $LOADER_NAME loader. Please wait..." unzip -qo rr.img.zip msg_ok "Unzipped $LOADER_NAME loader successfully." rm -f rr.img.zip FILE="rr.img" LOADER_FILE="$IMAGES_DIR/$FILE" cd - > /dev/null fi ;; tinycore) curl -s https://api.github.com/repos/PeterSuh-Q3/tinycore-redpill/releases/latest \ | grep "browser_download_url.*tinycore-redpill.v.*img.gz" \ | cut -d '"' -f 4 \ | xargs wget -q --show-progress -O "$IMAGES_DIR/tinycore.img.gz" if [ -f "$IMAGES_DIR/tinycore.img.gz" ]; then cd "$IMAGES_DIR" msg_info "Unzipping $LOADER_NAME loader. Please wait..." gunzip -f tinycore.img.gz 2> /dev/null msg_ok "Unzipped $LOADER_NAME loader successfully." FILE="tinycore.img" LOADER_FILE="$IMAGES_DIR/$FILE" cd - > /dev/null else msg_error "Failed to download $LOADER_NAME loader" sleep 1 select_loader fi ;; esac msg_ok "Downloaded ${CL}${BL}${FILE}${CL} to ${IMAGES_DIR}" } # ======================================================= # ========================================================== # Select UEFI Storage # ========================================================== function select_efi_storage() { local vmid=$1 local STORAGE="" 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)}') ITEM=" Type: $TYPE Free: $FREE" OFFSET=2 if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) fi 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 for EFI disk." >&2 return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} else [[ -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) || { msg_warn "$(translate "EFI storage selection cancelled.")" >&2 return 1 } done fi [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } # ========================================================== # ========================================================== # Select Storage Loader # ========================================================== function select_storage_volume() { local vmid=$1 local purpose=$2 local STORAGE="" 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)}') ITEM=" Type: $TYPE Free: $FREE" OFFSET=2 if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) fi 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." >&2 return 1 elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then STORAGE=${STORAGE_MENU[0]} else while [ -z "${STORAGE:+x}" ]; do 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) || { msg_warn "$(translate "Storage selection cancelled for $purpose.")" >&2 return 1 } done fi [[ -z "$STORAGE" ]] && return 1 echo "$STORAGE" } # ========================================================== # Create VM # ========================================================== 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,nas -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \ -serial0 socket msg_ok "Create a $NAME" # Check if UEFI (OVMF) is being used =================== if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then msg_info "Configuring EFI disk" 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 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 STORAGE_TYPE=$(pvesm status -storage "$EFI_STORAGE" | awk 'NR>1 {print $2}') EFI_DISK_ID="efidisk0" if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:4,efitype=4m,format=raw,pre-enrolled-keys=0" >/dev/null 2>&1; then msg_ok "EFI disk created (raw) and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" else msg_error "Failed to configure EFI disk" ERROR_FLAG=true fi else EFI_DISK_NAME="vm-${VMID}-disk-efivars" EFI_DISK_EXT="" EFI_DISK_REF="" if pvesm alloc "$EFI_STORAGE" "$VMID" "$EFI_DISK_NAME" 4M >/dev/null 2>&1; then if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:${EFI_DISK_NAME},pre-enrolled-keys=0" >/dev/null 2>&1; then msg_ok "EFI disk created and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}" else msg_error "Failed to configure EFI disk" ERROR_FLAG=true fi else msg_error "Failed to create EFI disk" ERROR_FLAG=true fi fi fi # ========================================================== # Select storage volume for loader ======================= 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 & import_pid=$! # Show a simple progress indicator echo -n "Importing loader disk: " while kill -0 $import_pid 2>/dev/null; do echo -n "." sleep 2.5 done wait $import_pid rm -f /tmp/import_log_$VMID.txt IMPORTED_DISK=$(qm config $VMID | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f1) # If the disk was not imported correctly, show an error message but continue if [ -z "$IMPORTED_DISK" ]; then msg_error "Loader import failed. No disk detected." ERROR_FLAG=true else msg_ok "Loader imported successfully to ${CL}${BL}$LOADER_STORAGE${GN}${CL}" fi STORAGE_TYPE=$(pvesm status -storage "$LOADER_STORAGE" | awk 'NR>1 {print $2}') if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then UNUSED_LINE=$(qm config "$VMID" | grep -E '^unused[0-9]+:') IMPORTED_ID=$(echo "$UNUSED_LINE" | cut -d: -f1) IMPORTED_REF=$(echo "$UNUSED_LINE" | cut -d: -f2- | xargs) if [[ -n "$IMPORTED_REF" && -n "$IMPORTED_ID" ]]; then if qm set "$VMID" -ide0 "$IMPORTED_REF" >/dev/null 2>&1; then msg_ok "Configured loader disk as ide0" qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1 else msg_error "Failed to assign loader disk to ide0" ERROR_FLAG=true fi else msg_error "Loader import failed. No disk detected in config." ERROR_FLAG=true fi else DISK_NAME="vm-${VMID}-disk-0" if qm set "$VMID" -ide0 "$LOADER_STORAGE:${DISK_NAME}" >/dev/null 2>&1; then msg_ok "Configured loader disk as ide0" else msg_error "Failed to assign loader disk" ERROR_FLAG=true fi fi result=$(qm set "$VMID" -boot order=ide0 2>&1) if [[ $? -eq 0 ]]; then msg_ok "Loader configured as boot device." else ERROR_FLAG=true fi # ========================================================== 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) DISK_EXT=".raw" DISK_REF="$VMID/" ;; *) DISK_EXT="" DISK_REF="" ;; esac DISK_NUM=$((DISK_SLOT_INDEX+1)) DISK_NAME="vm-${VMID}-disk-${DISK_NUM}${DISK_EXT}" SATA_ID="sata${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" -$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}" 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 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 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 loader to boot")" 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...")" #read -r fi } # ========================================================== # ========================================================== # Main execution # ========================================================== header_info #echo -e "\n Loading..." sleep 1 # Start script if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then 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 create_vm stop_vm_wizard_capture # ==========================================================