2025-05-01 23:35:19 +02:00
#!/usr/bin/env bash
# ==========================================================
2025-05-07 23:50:20 +02:00
# ProxMenuX - Virtual Machine Creator Script
2025-05-01 23:35:19 +02:00
# ==========================================================
2025-05-07 23:50:20 +02:00
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
2026-01-19 17:15:00 +01:00
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
2025-05-07 23:50:20 +02:00
# 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.
2025-05-01 23:35:19 +02:00
# ==========================================================
2026-04-05 11:24:08 +02:00
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
LOCAL_SCRIPTS_LOCAL = " $( cd " $SCRIPT_DIR /.." && pwd ) "
LOCAL_SCRIPTS_DEFAULT = "/usr/local/share/proxmenux/scripts"
2025-05-01 23:35:19 +02:00
BASE_DIR = "/usr/local/share/proxmenux"
2026-04-05 11:24:08 +02:00
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
2025-05-01 23:35:19 +02:00
VENV_PATH = "/opt/googletrans-env"
if [[ -f " $UTILS_FILE " ]] ; then
source " $UTILS_FILE "
fi
2026-04-05 11:24:08 +02:00
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
2025-05-01 23:35:19 +02:00
load_language
initialize_cache
2026-04-05 11:24:08 +02:00
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 "
}
2025-05-01 23:35:19 +02:00
function select_disk_type() {
2026-04-05 11:24:08 +02:00
VIRTUAL_DISKS =()
IMPORT_DISKS =()
CONTROLLER_NVME_PCIS =()
2025-05-01 23:35:19 +02:00
2026-04-05 11:24:08 +02:00
while true; do
local choice
choice = $( whiptail --backtitle "ProxMenux" --title "STORAGE PLAN" --menu " $( _build_storage_plan_summary) " 18 78 5 \
2026-04-12 20:32:34 +02:00
"a" " $( translate "Add virtual disk" ) " \
"b" " $( translate "Add import disk" ) " \
"c" " $( translate "Add Controller or NVMe (PCI passthrough)" ) " \
2026-04-05 11:24:08 +02:00
"r" " $( translate "Reset current storage selection" ) " \
2026-04-12 20:32:34 +02:00
"d" " $( translate "──── [ Finish and continue ] ────" ) " \
2026-04-05 11:24:08 +02:00
--ok-button "Select" --cancel-button "Cancel" 3>& 1 1>& 2 2>& 3) || return 1
2025-05-01 23:35:19 +02:00
2026-04-05 11:24:08 +02:00
case " $choice " in
2026-04-12 20:32:34 +02:00
a)
2026-04-05 11:24:08 +02:00
select_virtual_disk
;;
2026-04-12 20:32:34 +02:00
b)
2026-04-05 11:24:08 +02:00
select_import_disk
;;
2026-04-12 20:32:34 +02:00
c)
2026-04-05 11:24:08 +02:00
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
2025-05-01 23:35:19 +02:00
}
# ==========================================================
# Select Virtual Disks
# ==========================================================
function select_virtual_disk() {
msg_info "Detecting available storage volumes..."
2026-04-05 11:24:08 +02:00
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' )
2025-05-07 21:42:50 +02:00
2026-04-05 11:24:08 +02:00
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
2025-05-01 23:35:19 +02:00
fi
2026-04-05 11:24:08 +02:00
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
2026-04-06 17:16:26 +02:00
stop_spinner
2026-04-05 11:24:08 +02:00
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 } " )
2025-05-01 23:35:19 +02:00
export VIRTUAL_DISKS
}
# ==========================================================
# ==========================================================
2026-04-05 11:24:08 +02:00
# Select Import Disks
2025-05-01 23:35:19 +02:00
# ==========================================================
2026-04-05 11:24:08 +02:00
function select_import_disk() {
2025-05-01 23:35:19 +02:00
msg_info " $( translate "Detecting available disks..." ) "
2026-04-05 11:24:08 +02:00
_refresh_host_storage_cache
local FREE_DISKS =()
local DISK INFO MODEL SIZE LABEL DESCRIPTION
2025-05-01 23:35:19 +02:00
while read -r DISK; do
2026-04-05 11:24:08 +02:00
[[ " $DISK " = ~ /dev/zd ]] && continue
if _disk_is_host_system_used " $DISK " ; then
continue
fi
2025-05-01 23:35:19 +02:00
2026-04-05 11:24:08 +02:00
INFO =( $( lsblk -dn -o MODEL,SIZE " $DISK " ) )
MODEL = " ${ INFO [@]:: ${# INFO [@] } -1 } "
SIZE = " ${ INFO [-1] } "
LABEL = ""
2025-05-01 23:35:19 +02:00
2026-04-05 11:24:08 +02:00
if _disk_used_in_guest_configs " $DISK " ; then
LABEL += " [⚠ $( translate "In use by VM/LXC config" ) ]"
fi
2025-05-01 23:35:19 +02:00
2026-04-05 11:24:08 +02:00
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
2025-05-01 23:35:19 +02:00
done < <( lsblk -dn -e 7,11 -o PATH)
2026-04-05 11:24:08 +02:00
if [[ " ${# FREE_DISKS [@] } " -eq 0 ]] ; then
2026-04-06 17:16:26 +02:00
stop_spinner
2026-04-05 11:24:08 +02:00
whiptail --title "Error" --msgbox " $( translate "No importable disks available. System disks and protected disks are hidden." ) " 9 70
return 1
2025-05-01 23:35:19 +02:00
fi
2026-04-05 11:24:08 +02:00
local MAX_WIDTH TOTAL_WIDTH SELECTED_DISKS
2025-05-01 23:35:19 +02:00
MAX_WIDTH = $( printf "%s\n" " ${ FREE_DISKS [@] } " | awk '{print length}' | sort -nr | head -n1)
2026-04-05 11:24:08 +02:00
TOTAL_WIDTH = $(( MAX_WIDTH + 20 ))
[[ $TOTAL_WIDTH -lt 50 ]] && TOTAL_WIDTH = 50
2025-05-01 23:35:19 +02:00
cleanup
2026-04-05 11:24:08 +02:00
SELECTED_DISKS = $( whiptail --title "Select Import Disks" --checklist \
" $( translate "Select the disks you want to import (use spacebar to toggle):" ) " 20 $TOTAL_WIDTH 10 \
2025-05-01 23:35:19 +02:00
" ${ FREE_DISKS [@] } " 3>& 1 1>& 2 2>& 3)
2026-04-05 11:24:08 +02:00
[[ $? -ne 0 ]] && return 1
2025-05-01 23:35:19 +02:00
2026-04-05 11:24:08 +02:00
IMPORT_DISKS =()
local DISK_INFO
2025-05-01 23:35:19 +02:00
for DISK in $( echo " $SELECTED_DISKS " | tr -d '"' ) ; do
2026-04-05 11:24:08 +02:00
_array_contains " $DISK " " ${ IMPORT_DISKS [@] } " || IMPORT_DISKS +=( " $DISK " )
2025-05-01 23:35:19 +02:00
done
2026-04-05 11:24:08 +02:00
if [[ ${# IMPORT_DISKS [@] } -eq 0 ]] ; then
msg_warn " $( translate "No import disks selected for now." ) "
return 0
fi
export IMPORT_DISKS
return 0
2025-05-01 23:35:19 +02:00
}
2026-04-05 11:24:08 +02:00
function select_passthrough_disk() {
select_import_disk
}
function select_controller_nvme() {
2026-04-06 17:16:26 +02:00
local VM_STORAGE_IOMMU_REBOOT_POLICY = "defer"
if declare -F _vm_storage_ensure_iommu_or_offer >/dev/null 2>& 1; then
if ! _vm_storage_ensure_iommu_or_offer; then
return 1
fi
elif declare -F _pci_is_iommu_active >/dev/null 2>& 1; then
if ! _pci_is_iommu_active; then
whiptail --title "Controller + NVMe" --msgbox \
" $( translate "IOMMU is not active on this host." ) \n\n $( translate "Controller/NVMe passthrough requires IOMMU enabled in BIOS/UEFI and kernel." ) \n\n $( translate "Enable IOMMU, reboot the host, and try again." ) " \
14 90
return 1
fi
fi
2026-04-05 11:24:08 +02:00
msg_info " $( translate "Detecting PCI storage controllers and NVMe devices..." ) "
_refresh_host_storage_cache
local menu_items =()
local blocked_report = ""
2026-04-06 13:39:07 +02:00
local pci_path pci_full class_hex name controller_disks controller_desc disk safe_count blocked_count state slot_base hidden_target_count
2026-04-05 11:24:08 +02:00
safe_count = 0
blocked_count = 0
2026-04-06 13:39:07 +02:00
hidden_target_count = 0
local target_vmid = " ${ VMID :- } "
2026-04-05 11:24:08 +02:00
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
2026-04-06 13:39:07 +02:00
slot_base = $( _pci_slot_base " $pci_full " )
# If target VM already has this slot assigned, hide it.
if [[ -n " $target_vmid " ]] && _vm_has_pci_slot " $target_vmid " " $slot_base " ; then
hidden_target_count = $(( hidden_target_count + 1 ))
continue
fi
2026-04-05 11:24:08 +02:00
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 " )
2026-04-06 13:39:07 +02:00
local -a blocked_reasons =()
2026-04-05 11:24:08 +02:00
for disk in " ${ controller_disks [@] } " ; do
if _disk_is_host_system_used " $disk " ; then
2026-04-06 13:39:07 +02:00
blocked_reasons +=( " ${ disk } ( ${ DISK_USAGE_REASON } )" )
2026-04-05 11:24:08 +02:00
elif _disk_used_in_guest_configs " $disk " ; then
2026-04-06 13:39:07 +02:00
blocked_reasons +=( " ${ disk } ( $( translate "In use by VM/LXC config" ) )" )
2026-04-05 11:24:08 +02:00
fi
done
2026-04-06 13:39:07 +02:00
if [[ ${# blocked_reasons [@] } -gt 0 ]] ; then
2026-04-05 11:24:08 +02:00
blocked_count = $(( blocked_count + 1 ))
2026-04-06 21:12:13 +02:00
blocked_report += " • ${ pci_full } — $( _shorten_text " $name " 56) \n"
2026-04-05 11:24:08 +02:00
continue
fi
2026-04-06 13:39:07 +02:00
local short_name
short_name = $( _shorten_text " $name " 42)
local assigned_suffix = ""
if [[ -n " $( _pci_assigned_vm_ids " $pci_full " " $target_vmid " 2>/dev/null | head -1) " ]] ; then
assigned_suffix = " | $( translate "Assigned to VM" ) "
2026-04-05 11:24:08 +02:00
fi
2026-04-06 21:12:13 +02:00
controller_desc = " ${ short_name }${ assigned_suffix } "
2026-04-06 13:39:07 +02:00
2026-04-05 11:24:08 +02:00
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
2026-04-06 13:39:07 +02:00
if [[ " $hidden_target_count " -gt 0 && " $blocked_count " -eq 0 ]] ; then
msg = " $( translate "All detected controllers/NVMe are already present in the selected VM." ) \n\n $( translate "No additional device needs to be added." ) "
else
2026-04-06 21:12:13 +02:00
msg = " $( translate "No available Controllers/NVMe devices were found." ) \n\n"
2026-04-06 13:39:07 +02:00
fi
2026-04-05 11:24:08 +02:00
if [[ $blocked_count -gt 0 ]] ; then
2026-04-06 21:12:13 +02:00
msg += " $( translate "Hidden for safety" ) :\n ${ blocked_report } "
2026-04-05 11:24:08 +02:00
fi
2026-04-06 22:04:38 +02:00
whiptail --title "Controller + NVMe" --msgbox " $msg " 18 84
2026-04-05 11:24:08 +02:00
return 1
fi
local selected
selected = $( whiptail --title "Controller + NVMe" --checklist \
2026-04-06 21:12:13 +02:00
" $( translate "Select available Controllers/NVMe to add:" ) " 20 96 10 \
2026-04-05 11:24:08 +02:00
" ${ 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
2026-04-06 17:16:26 +02:00
if declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>& 1; then
local vm_name_for_notice = " ${ HN :- } "
if ! _vm_storage_confirm_controller_passthrough_risk " ${ VMID :- } " " $vm_name_for_notice " "Controller + NVMe" ; then
CONTROLLER_NVME_PCIS =()
return 1
fi
fi
2026-04-05 11:24:08 +02:00
export CONTROLLER_NVME_PCIS
return 0
}
# ==========================================================