2025-08-21 18:00:58 +02:00
#!/usr/bin/env bash
# ==========================================================
# ProxMenuX - ZimaOS VM Creator Script
# ==========================================================
# 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-08-21 18:00:58 +02:00
# Version : 1.0
# Last Updated: 21/08/2025
# ==========================================================
# Description:
# This script automates the creation and configuration of a ZimaOS
# 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.
#
#
# ==========================================================
# Configuration ============================================
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"
LOCAL_SCRIPTS = " $LOCAL_SCRIPTS_DEFAULT "
2025-08-21 18:00:58 +02:00
BASE_DIR = "/usr/local/share/proxmenux"
2026-04-05 11:24:08 +02:00
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-08-21 18:00:58 +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
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
2025-08-21 18:00:58 +02:00
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 = "ZimaOS VM"
IMAGES_DIR = "/var/lib/vz/template/iso"
ERROR_FLAG = false
2026-04-05 11:24:08 +02:00
WIZARD_ADD_GPU = "no"
WIZARD_GPU_RESULT = "not_requested"
VM_WIZARD_CAPTURE_FILE = ""
VM_WIZARD_CAPTURE_ACTIVE = 0
2025-08-21 18:00:58 +02:00
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( ) {
show_proxmenux_logo
2026-04-05 11:24:08 +02:00
msg_title "ZimaOS VM Creator"
2025-08-21 18:00:58 +02:00
}
# ==========================================================
2026-04-05 11:24:08 +02:00
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 "
}
2025-08-21 18:00:58 +02:00
# ==========================================================
# 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 } "
2026-04-05 11:24:08 +02:00
default_settings || return 1
2025-08-21 18:00:58 +02:00
else
header_info
echo -e " ${ CUS } Using Advanced Settings ${ CL } "
2026-04-05 11:24:08 +02:00
advanced_settings || return 1
2025-08-21 18:00:58 +02:00
fi
2026-04-05 11:24:08 +02:00
return 0
2025-08-21 18:00:58 +02:00
}
# ==========================================================
# ==========================================================
# Default Settings
# ==========================================================
function default_settings( ) {
VMID = " $NEXTID "
FORMAT = ""
MACHINE = " -machine q35"
BIOS_TYPE = " -bios ovmf"
DISK_CACHE = ""
2025-08-21 18:27:30 +02:00
HN = "ZimaOS"
2025-08-21 18:00:58 +02:00
CPU_TYPE = " -cpu host"
CORE_COUNT = "2"
RAM_SIZE = "4096"
BRG = "vmbr0"
MAC = " $GEN_MAC "
VLAN = ""
MTU = ""
SERIAL_PORT = "socket"
START_VM = "no"
2025-08-21 18:59:54 +02:00
INTERFACE_TYPE = "scsi"
DISCARD_OPTS = ",discard=on,ssd=on"
2025-08-21 18:00:58 +02:00
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 } "
2025-08-21 18:59:54 +02:00
echo -e " ${ TAB } ${ DGN } Disk Interface Type: ${ BGN } ${ INTERFACE_TYPE } ${ CL } "
2025-08-21 18:00:58 +02:00
echo -e
echo -e " ${ DEF } Creating a $NAME using the above default settings ${ CL } "
sleep 1
2026-04-05 11:24:08 +02:00
select_disk_type || return 1
2025-08-21 18:00:58 +02:00
}
# ==========================================================
# ==========================================================
# 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
2025-08-21 18:31:32 +02:00
if VM_NAME = $( whiptail --backtitle "ProxMenuX" --inputbox " $( translate "Set Hostname" ) " 8 58 ZimaOS --title "HOSTNAME" --cancel-button Exit-Script 3>& 1 1>& 2 2>& 3) ; then
2025-08-21 18:00:58 +02:00
if [ -z $VM_NAME ] ; then
2025-08-21 18:30:43 +02:00
HN = "ZimaOS"
2025-08-21 18:00:58 +02:00
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
2025-08-21 18:59:54 +02:00
# 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)" ) " ON \
"sata" " $( translate "SATA (standard - high compatibility)" ) " OFF \
2026-04-05 11:24:08 +02:00
3>& 1 1>& 2 2>& 3) || {
msg_warn " $( translate "Disk interface selection cancelled." ) "
return 1
}
2025-08-21 18:59:54 +02:00
case " $INTERFACE_TYPE " in
"scsi" | "sata" )
DISCARD_OPTS = ",discard=on,ssd=on"
; ;
esac
msg_ok " $( translate "Disk interface selected:" ) $INTERFACE_TYPE "
2025-08-21 18:00:58 +02:00
# 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
2026-04-05 11:24:08 +02:00
select_disk_type || return 1
2025-08-21 18:00:58 +02:00
else
header_info
sleep 1
echo -e " ${ CUS } Using Advanced Settings ${ CL } "
advanced_settings
fi
}
# ==========================================================
# ==========================================================
# Select Disk
# ==========================================================
2026-04-05 11:24:08 +02:00
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 "
2025-08-21 18:00:58 +02:00
}
2026-04-05 11:24:08 +02:00
function select_disk_type( ) {
VIRTUAL_DISKS = ( )
IMPORT_DISKS = ( )
CONTROLLER_NVME_PCIS = ( )
2025-08-21 18:00:58 +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 \
"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
2025-08-21 18:00:58 +02:00
continue
fi
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
done
}
2026-04-05 11:24:08 +02:00
function select_virtual_disk( ) {
msg_info "Detecting available storage volumes..."
2025-08-21 18:00:58 +02:00
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-08-21 18:00:58 +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
fi
2025-08-21 18:00:58 +02:00
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) || return 0
[ [ -z " $STORAGE " ] ] && return 0
fi
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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 } " )
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
}
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
function select_import_disk( ) {
2025-08-21 18:00:58 +02:00
msg_info " $( translate "Detecting available disks..." ) "
2026-04-05 11:24:08 +02:00
_refresh_host_storage_cache
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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)
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
IMPORT_DISKS = ( )
local item
for item in $( echo " $selected " | tr -d '"' ) ; do
IMPORT_DISKS += ( " $item " )
2025-08-21 18:00:58 +02:00
done
2026-04-05 11:24:08 +02:00
export IMPORT_DISKS
}
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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" ) ); "
2025-08-21 18:00:58 +02:00
fi
2026-04-05 11:24:08 +02:00
done
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +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
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
menu_items += ( " $pci_full " " $controller_desc " " $state " )
safe_count = $(( safe_count + 1 ))
done < <( ls -d /sys/bus/pci/devices/* 2>/dev/null | sort)
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
fi
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
fi
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
CONTROLLER_NVME_PCIS = ( )
local pci
for pci in $( echo " $selected " | tr -d '"' ) ; do
CONTROLLER_NVME_PCIS += ( " $pci " )
2025-08-21 18:00:58 +02:00
done
2026-04-05 11:24:08 +02:00
export CONTROLLER_NVME_PCIS
}
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
function select_passthrough_disk( ) {
select_import_disk
2025-08-21 18:00:58 +02:00
}
# ==========================================================
# ==========================================================
# 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 for ZimaOS
LOADER_OPTION = $( whiptail --backtitle "ProxMenuX" --title "SELECT ZIMAOS IMAGE" --menu " $( translate "Choose ZimaOS image option:" ) " 15 70 4 \
"1" "Download Latest ZimaOS Release" \
"2" "Use Existing ZimaOS Image" \
3>& 1 1>& 2 2>& 3)
if [ -z " $LOADER_OPTION " ] ; then
exit_script
fi
case $LOADER_OPTION in
1)
LOADER_TYPE = "latest"
LOADER_NAME = "ZimaOS Latest"
LOADER_URL = "https://github.com/IceWhaleTech/ZimaOS/"
echo -e " ${ DGN } ${ TAB } Selected: ${ BGN } $LOADER_NAME ${ CL } "
download_loader
; ;
2)
LOADER_TYPE = "existing"
LOADER_NAME = "ZimaOS"
LOADER_URL = "https://github.com/IceWhaleTech/ZimaOS/"
echo -e " ${ DGN } ${ TAB } Selected: ${ 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 ZimaOS IMAGE" --menu " $( translate "Choose a ZimaOS image:" ) " 20 70 10 " ${ IMAGE_OPTIONS [@] } " 3>& 1 1>& 2 2>& 3)
if [ -z " $LOADER_FILE " ] ; then
msg_error "No ZimaOS image selected"
exit_script
fi
echo -e " ${ DGN } ${ TAB } Using ZimaOS Image: ${ BGN } $( basename " $LOADER_FILE " ) ${ CL } "
FILE = $( basename " $LOADER_FILE " )
}
2025-10-18 16:11:28 +02:00
# ==========================================================
function download_loader_( ) {
2025-08-21 18:00:58 +02:00
echo -e " ${ DGN } ${ TAB } Retrieving ZimaOS image ${ CL } "
case $LOADER_TYPE in
latest)
# Get the latest release download URL
DOWNLOAD_URL = $( curl -s https://api.github.com/repos/IceWhaleTech/ZimaOS/releases/latest \
| grep "browser_download_url.*\.img" \
| cut -d '"' -f 4 \
| head -1)
if [ -z " $DOWNLOAD_URL " ] ; then
msg_error "Failed to get latest ZimaOS release URL"
sleep 2
select_loader
return
fi
# Extract filename from URL
FILE = $( basename " $DOWNLOAD_URL " )
LOADER_FILE = " $IMAGES_DIR / $FILE "
# Check if file already exists
if [ -f " $LOADER_FILE " ] ; then
if whiptail --backtitle "ProxMenuX" --title "FILE EXISTS" --yesno " $( translate " ZimaOS image already exists: $FILE \n\nDo you want to re-download it? " ) " 10 70; then
rm -f " $LOADER_FILE "
else
echo -e " ${ DGN } ${ TAB } Using existing file: ${ BGN } $FILE ${ CL } "
return
fi
fi
if wget -q --show-progress -O " $LOADER_FILE " " $DOWNLOAD_URL " ; then
msg_ok " Downloaded ZimaOS image successfully: $FILE "
else
msg_error "Failed to download ZimaOS image"
rm -f " $LOADER_FILE "
sleep 2
select_loader
return
fi
; ;
esac
# Verify the downloaded file
if [ -f " $LOADER_FILE " ] && [ -s " $LOADER_FILE " ] ; then
echo -e " ${ DGN } ${ TAB } ZimaOS Image: ${ BGN } $FILE ${ CL } "
echo -e " ${ DGN } ${ TAB } File Size: ${ BGN } $( du -h " $LOADER_FILE " | cut -f1) ${ CL } "
else
msg_error "Downloaded file is empty or corrupted"
rm -f " $LOADER_FILE "
sleep 2
select_loader
fi
}
2025-10-18 16:11:28 +02:00
function download_loader( ) {
echo -e " ${ DGN } ${ TAB } Retrieving ZimaOS image ${ CL } "
API_URL = "https://api.github.com/repos/IceWhaleTech/ZimaOS/releases/latest"
DOWNLOAD_URL = $( curl -fsSL " $API_URL " \
| grep -Eo '"browser_download_url":\s*"[^"]*zimaos-x86_64-[^"]*_installer\.img"' \
| head -1 \
| cut -d '"' -f 4)
2025-10-18 16:16:48 +02:00
# if [[ -z "$DOWNLOAD_URL" ]]; then
# TAG=$(curl -fsSL "$API_URL" | grep -Eo '"tag_name":\s*"[^"]+"' | cut -d '"' -f 4 | head -1)
# if [[ -n "$TAG" ]]; then
# DOWNLOAD_URL="https://github.com/IceWhaleTech/ZimaOS/releases/download/${TAG}/zimaos-x86_64-${TAG}_installer.img"
# fi
# fi
2025-10-18 16:11:28 +02:00
if [ [ -z " $DOWNLOAD_URL " ] ] ; then
msg_error "Failed to get latest ZimaOS release URL"
sleep 2; select_loader; return
fi
FILE = " $( basename " $DOWNLOAD_URL " ) "
LOADER_FILE = " $IMAGES_DIR / $FILE "
if [ [ -f " $LOADER_FILE " ] ] ; then
if whiptail --backtitle "ProxMenuX" --title "FILE EXISTS" \
--yesno " $( translate " ZimaOS image already exists: $FILE \n\nDo you want to re-download it? " ) " 10 70; then
rm -f " $LOADER_FILE "
else
echo -e " ${ DGN } ${ TAB } Using existing file: ${ BGN } $FILE ${ CL } "
echo -e " ${ DGN } ${ TAB } ZimaOS Image: ${ BGN } $FILE ${ CL } "
echo -e " ${ DGN } ${ TAB } File Size: ${ BGN } $( du -h " $LOADER_FILE " | cut -f1) ${ CL } "
return
fi
fi
if ! curl -fL --retry 3 --retry-delay 2 --retry-connrefused \
--progress-bar -o " $LOADER_FILE " " $DOWNLOAD_URL " ; then
msg_error "Failed to download ZimaOS image"
rm -f " $LOADER_FILE "
sleep 2; select_loader; return
fi
if [ [ ! -s " $LOADER_FILE " ] ] || [ [ $( stat -c%s " $LOADER_FILE " ) -lt $(( 100 * 1024 * 1024 )) ] ] ; then
msg_error "Downloaded file is empty or unexpectedly small"
rm -f " $LOADER_FILE "
sleep 2; select_loader; return
fi
msg_ok " Downloaded ZimaOS image successfully: $FILE "
echo -e " ${ DGN } ${ TAB } ZimaOS Image: ${ BGN } $FILE ${ CL } "
echo -e " ${ DGN } ${ TAB } File Size: ${ BGN } $( du -h " $LOADER_FILE " | cut -f1) ${ CL } "
}
2025-08-21 18:00:58 +02:00
# ==========================================================
# ==========================================================
# 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
2026-04-05 11:24:08 +02:00
msg_error "Unable to detect a valid storage location for EFI disk." >& 2
return 1
2025-08-21 18:00:58 +02:00
elif [ $(( ${# STORAGE_MENU [@] } / 3 )) -eq 1 ] ; then
STORAGE = ${ STORAGE_MENU [0] }
else
2026-04-05 11:24:08 +02:00
[ [ -n " ${ SPINNER_PID :- } " ] ] && kill " $SPINNER_PID " > /dev/null 2>& 1
2025-08-21 18:00:58 +02:00
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 \
2026-04-05 11:24:08 +02:00
" ${ STORAGE_MENU [@] } " 3>& 1 1>& 2 2>& 3) || {
msg_warn " $( translate "EFI storage selection cancelled." ) " >& 2
return 1
}
2025-08-21 18:00:58 +02:00
done
fi
2026-04-05 11:24:08 +02:00
[ [ -z " $STORAGE " ] ] && return 1
2025-08-21 18:00:58 +02:00
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
2026-04-05 11:24:08 +02:00
msg_error "Unable to detect a valid storage location." >& 2
return 1
2025-08-21 18:00:58 +02:00
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 \
2025-09-01 12:20:45 +02:00
" $( translate " Choose the storage volume for $purpose : " ) " \
2025-08-21 18:00:58 +02:00
16 $(( $MSG_MAX_LENGTH + 23 )) 6 \
2026-04-05 11:24:08 +02:00
" ${ STORAGE_MENU [@] } " 3>& 1 1>& 2 2>& 3) || {
msg_warn " $( translate " Storage selection cancelled for $purpose . " ) " >& 2
return 1
}
2025-08-21 18:00:58 +02:00
done
fi
2026-04-05 11:24:08 +02:00
[ [ -z " $STORAGE " ] ] && return 1
2025-08-21 18:00:58 +02:00
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 \
2026-04-05 11:24:08 +02:00
-name $HN -tags proxmenux,nas -net0 virtio,bridge= $BRG ,macaddr= $MAC $VLAN $MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \
2025-08-21 18:00:58 +02:00
-serial0 socket
msg_ok " Create a $NAME "
2025-08-21 19:36:28 +02:00
BOOT_ORDER_LIST = ( ) # Array to store boot order for all disks
2025-08-21 18:00:58 +02:00
2025-08-21 19:36:28 +02:00
# Check if UEFI (OVMF) is being used ===================
2025-08-21 18:00:58 +02:00
if [ [ " $BIOS_TYPE " = = *"ovmf" * ] ] ; then
msg_info "Configuring EFI disk"
2026-04-05 11:24:08 +02:00
if ! EFI_STORAGE = $( select_efi_storage $VMID ) ; then
msg_error "EFI storage selection failed or was cancelled."
return 1
fi
2025-08-21 18:00:58 +02:00
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 =======================
2026-04-05 11:24:08 +02:00
if ! LOADER_STORAGE = $( select_storage_volume $VMID "loader disk" ) ; then
msg_error "Loader storage selection failed or was cancelled."
return 1
fi
2025-08-21 18:00:58 +02:00
#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"
else
2025-08-21 19:36:28 +02:00
msg_error "Failed to assign loader disk"
2025-08-21 18:00:58 +02:00
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
2026-04-05 11:24:08 +02:00
DISK_INFO = ""
CONSOLE_DISK_INFO = ""
DISK_SLOT_INDEX = 0
2025-08-21 19:36:28 +02:00
2026-04-05 11:24:08 +02:00
if [ [ ${# VIRTUAL_DISKS [@] } -gt 0 ] ] ; then
2025-08-21 19:36:28 +02:00
for i in " ${ !VIRTUAL_DISKS[@] } " ; do
IFS = ':' read -r STORAGE SIZE <<< " ${ VIRTUAL_DISKS [ $i ] } "
2026-04-05 11:24:08 +02:00
2025-08-21 19:36:28 +02:00
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
2026-04-05 11:24:08 +02:00
DISK_NUM = $(( DISK_SLOT_INDEX+1))
2025-08-21 19:36:28 +02:00
DISK_NAME = " vm- ${ VMID } -disk- ${ DISK_NUM } ${ DISK_EXT } "
2026-04-05 11:24:08 +02:00
INTERFACE_ID = " ${ INTERFACE_TYPE } ${ DISK_SLOT_INDEX } "
2025-08-21 19:36:28 +02:00
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 "
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 " -$INTERFACE_ID " $STORAGE : ${ DISK_REF } $DISK_NAME $DISCARD_OPTS " >/dev/null 2>& 1; then
msg_error " Failed to configure virtual disk as $INTERFACE_ID "
ERROR_FLAG = true
continue
fi
fi
2025-08-21 18:00:58 +02:00
2025-08-21 19:36:28 +02:00
msg_ok " Configured virtual disk as $INTERFACE_ID , ${ SIZE } GB on ${ CL } ${ BL } $STORAGE ${ CL } ${ GN } "
BOOT_ORDER_LIST += ( " $INTERFACE_ID " )
2026-04-05 11:24:08 +02:00
DISK_INFO = " ${ DISK_INFO } <p>Virtual Disk $DISK_NUM : ${ SIZE } GB on ${ STORAGE } ( ${ INTERFACE_ID } )</p> "
2025-08-21 19:36:28 +02:00
CONSOLE_DISK_INFO = " ${ CONSOLE_DISK_INFO } - Virtual Disk $DISK_NUM : ${ SIZE } GB on ${ STORAGE } ( $INTERFACE_ID )\n "
2026-04-05 11:24:08 +02:00
DISK_SLOT_INDEX = $(( DISK_SLOT_INDEX + 1 ))
2025-08-21 19:36:28 +02:00
done
2025-08-21 18:00:58 +02:00
fi
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
if [ [ ${# EFFECTIVE_IMPORT_DISKS [@] } -gt 0 ] ] ; then
for DISK in " ${ EFFECTIVE_IMPORT_DISKS [@] } " ; do
INTERFACE_ID = " ${ INTERFACE_TYPE } ${ DISK_SLOT_INDEX } "
2025-08-21 19:36:28 +02:00
MODEL = $( lsblk -ndo MODEL " $DISK " 2>/dev/null || echo "Unknown" )
SIZE = $( lsblk -ndo SIZE " $DISK " 2>/dev/null || echo "Unknown" )
2026-04-05 11:24:08 +02:00
2025-08-21 19:36:28 +02:00
if qm set " $VMID " -$INTERFACE_ID " $DISK $DISCARD_OPTS " >/dev/null 2>& 1; then
2026-04-05 11:24:08 +02:00
msg_ok " Configured import disk as $INTERFACE_ID : $DISK ( $MODEL $SIZE ) "
2025-08-21 19:36:28 +02:00
BOOT_ORDER_LIST += ( " $INTERFACE_ID " )
2026-04-05 11:24:08 +02:00
DISK_INFO = " ${ DISK_INFO } <p>Import Disk $(( DISK_SLOT_INDEX+1)) : $DISK ( $MODEL $SIZE ) ( ${ INTERFACE_ID } )</p> "
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 ))
2025-08-21 19:36:28 +02:00
else
2026-04-05 11:24:08 +02:00
msg_error " Failed to configure import disk $DISK as $INTERFACE_ID "
2025-08-21 19:36:28 +02:00
ERROR_FLAG = true
fi
done
2026-04-05 11:24:08 +02:00
fi
2025-08-21 18:00:58 +02:00
2026-04-05 11:24:08 +02:00
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 } <p>Controller/NVMe: ${ PCI_DEV } </p> "
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
2025-08-21 18:00:58 +02:00
HTML_DESC = " <div align='center'>
<table style = 'width: 100%; border-collapse: collapse;' >
<tr>
<td style = 'width: 100px; vertical-align: middle;' >
<img src = 'https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logo_desc.png' alt = 'ProxMenux Logo' style = 'height: 100px;' >
</td>
<td style = 'vertical-align: middle;' >
<h1 style = 'margin: 0;' >ZimaOS VM</h1>
<p style = 'margin: 0;' >Created with ProxMenuX</p>
2026-04-05 11:24:08 +02:00
<p style = 'margin: 0;' >$LOADER_NAME </p>
2025-08-21 18:00:58 +02:00
</td>
</tr>
</table>
<p>
<a href = 'https://macrimi.github.io/ProxMenux/' target = '_blank' ><img src = 'https://img.shields.io/badge/📚_Docs-blue' alt = 'Docs' ></a>
<a href = 'https://raw.githubusercontent.com/MacRimi/ProxMenux/refs/heads/main/scripts/vm/zimaos.sh' target = '_blank' ><img src = 'https://img.shields.io/badge/💻_Code-green' alt = 'Code' ></a>
<a href = '$LOADER_URL' target = '_blank' ><img src = 'https://img.shields.io/badge/📦_Installer-orange' alt = 'Loader' ></a>
<a href = 'https://ko-fi.com/macrimi' target = '_blank' ><img src = 'https://img.shields.io/badge/☕_Ko--fi-red' alt = 'Ko-fi' ></a>
</p>
<div>
${ DISK_INFO }
</div>
</div>"
2026-04-05 11:24:08 +02:00
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
2025-08-21 19:36:28 +02:00
fi
2026-04-05 11:24:08 +02:00
msg_ok "Configured VM description"
2025-08-21 19:36:28 +02:00
if [ ${# BOOT_ORDER_LIST [@] } -gt 0 ] ; then
2026-04-05 11:24:08 +02:00
BOOT_ORDER_LIST += ( "ide0" )
2025-08-21 19:36:28 +02:00
BOOT_ORDER_STRING = $( IFS = ';' ; echo " ${ BOOT_ORDER_LIST [*] } " )
2026-04-05 11:24:08 +02:00
else
BOOT_ORDER_STRING = "ide0"
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
2025-08-21 19:36:28 +02:00
fi
2025-08-21 19:39:50 +02:00
if [ " $ERROR_FLAG " = true ] ; then
msg_error "VM created with errors. Check configuration."
else
2026-04-05 11:24:08 +02:00
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
2025-08-21 19:39:50 +02:00
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" ) "
2026-04-05 11:24:08 +02:00
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
2025-08-21 19:39:50 +02:00
echo -e
fi
2025-08-21 18:00:58 +02:00
}
# ==========================================================
header_info
sleep 1
if whiptail --backtitle "ProxMenuX" --title " $NAME " --yesno " $( translate " This will create a New $NAME . Proceed? " ) " 10 58; then
2026-04-05 11:24:08 +02:00
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
2025-08-21 18:00:58 +02:00
else
clear
exit
fi
create_vm
2026-04-05 11:24:08 +02:00
stop_vm_wizard_capture
2025-08-21 18:00:58 +02:00
# ==========================================================