2025-04-09 10:12:04 +02:00
#!/bin/bash
# ==========================================================
2025-09-10 18:47:55 +02:00
# ProxMenux - A menu-driven script for Proxmox VE management
2025-04-09 10:12:04 +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)
2026-04-12 20:32:34 +02:00
# Version : 1.2
# Last Updated: 12/04/2026
2025-04-09 10:12:04 +02:00
# ==========================================================
# Description:
# This script allows users to assign physical disks to existing
# Proxmox virtual machines (VMs) through an interactive menu.
# - Detects the system disk and excludes it from selection.
# - Lists all available VMs for the user to choose from.
# - Identifies and displays unassigned physical disks.
# - Allows the user to select multiple disks and attach them to a VM.
# - Supports interface types: SATA, SCSI, VirtIO, and IDE.
# - Ensures that disks are not already assigned to active VMs.
# - Warns about disk sharing between multiple VMs to avoid data corruption.
# - Configures the selected disks for the VM and verifies the assignment.
2026-04-12 20:32:34 +02:00
# - Prefers persistent /dev/disk/by-id paths for assignment when available.
2025-04-09 10:12:04 +02:00
#
# The goal of this script is to simplify the process of assigning
# physical disks to Proxmox VMs, reducing manual configurations
# and preventing potential errors.
# ==========================================================
# Configuration ============================================
2026-04-12 20:32:34 +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-04-09 10:12:04 +02:00
BASE_DIR = "/usr/local/share/proxmenux"
2026-04-12 20:32:34 +02:00
UTILS_FILE = " $LOCAL_SCRIPTS /utils.sh "
2025-04-09 10:12:04 +02:00
2026-04-12 20:32:34 +02:00
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
# shellcheck source=/dev/null
2025-04-09 10:12:04 +02:00
if [ [ -f " $UTILS_FILE " ] ] ; then
source " $UTILS_FILE "
fi
2026-04-12 20:32:34 +02:00
# shellcheck source=/dev/null
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
BACKTITLE = "ProxMenux"
UI_MENU_H = 20
UI_MENU_W = 84
UI_MENU_LIST_H = 10
UI_SHORT_MENU_H = 16
UI_SHORT_MENU_W = 72
UI_SHORT_MENU_LIST_H = 6
UI_MSG_H = 10
UI_MSG_W = 72
UI_YESNO_H = 18
UI_YESNO_W = 86
UI_RESULT_H = 18
UI_RESULT_W = 86
2025-04-09 10:12:04 +02:00
load_language
initialize_cache
2026-04-12 20:32:34 +02:00
if ! command -v pveversion >/dev/null 2>& 1; then
dialog --backtitle " $BACKTITLE " --title " $( translate "Error" ) " \
--msgbox " $( translate "This script must be run on a Proxmox host." ) " 8 60
exit 1
fi
# ==========================================================
2025-04-09 10:12:04 +02:00
get_disk_info( ) {
local disk = $1
2026-04-12 20:32:34 +02:00
local model size
model = $( lsblk -dn -o MODEL " $disk " | xargs)
size = $( lsblk -dn -o SIZE " $disk " | xargs)
[ [ -z " $model " ] ] && model = "Unknown"
printf '%s\t%s\n' " $model " " $size "
2025-04-09 10:12:04 +02:00
}
2026-04-12 20:32:34 +02:00
get_all_disk_paths( ) {
local disk = " $1 "
local real_path
real_path = $( readlink -f " $disk " 2>/dev/null)
[ [ -n " $disk " ] ] && echo " $disk "
[ [ -n " $real_path " ] ] && echo " $real_path "
local link
for link in /dev/disk/by-id/* /dev/disk/by-path/*; do
[ [ -e " $link " ] ] || continue
[ [ " $( readlink -f " $link " 2>/dev/null) " = = " $real_path " ] ] || continue
echo " $link "
done | sort -u
}
get_preferred_disk_path( ) {
local disk = " $1 "
local real_path
real_path = $( readlink -f " $disk " 2>/dev/null)
[ [ -z " $real_path " ] ] && { echo " $disk " ; return 0; }
local best = "" best_score = 99999
local link name score
for link in /dev/disk/by-id/*; do
[ [ -e " $link " ] ] || continue
[ [ " $( readlink -f " $link " 2>/dev/null) " = = " $real_path " ] ] || continue
name = $( basename " $link " )
[ [ " $name " = = *-part* ] ] && continue
case " $name " in
ata-*| scsi-*| nvme-*) score = 100 ; ;
wwn-*) score = 200 ; ;
*) score = 300 ; ;
esac
score = $(( score + ${# name } ))
if ( ( score < best_score ) ) ; then
best = " $link "
best_score = $score
fi
done
if [ [ -n " $best " ] ] ; then
echo " $best "
else
echo " $disk "
fi
}
disk_referenced_in_config( ) {
local config_text = " $1 "
local disk = " $2 "
local alias
while read -r alias; do
[ [ -z " $alias " ] ] && continue
if grep -Fq " $alias " <<< " $config_text " ; then
return 0
fi
done < <( get_all_disk_paths " $disk " )
return 1
}
# ── DIALOG PHASE ──────────────────────────────────────────────────────────────
2025-04-09 10:12:04 +02:00
VM_LIST = $( qm list | awk 'NR>1 {print $1, $2}' )
if [ -z " $VM_LIST " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No VMs available in the system." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
2026-04-12 20:32:34 +02:00
# shellcheck disable=SC2086
VMID = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Select VM" ) " \
--menu " $( translate "Select the VM to which you want to add disks:" ) " $UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
$VM_LIST \
2>& 1 >/dev/tty)
2025-04-09 10:12:04 +02:00
if [ -z " $VMID " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No VM was selected." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
VMID = $( echo " $VMID " | tr -d '"' )
VM_STATUS = $( qm status " $VMID " | awk '{print $2}' )
if [ " $VM_STATUS " = = "running" ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Warning" ) " \
--msgbox " $( translate "The VM is powered on. Turn it off before adding disks." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
2026-04-12 20:32:34 +02:00
# ── TERMINAL PHASE 1: detect disks ────────────────────────────────────────────
show_proxmenux_logo
msg_title " $( translate "Import Disk to VM" ) "
msg_ok " $( translate " VM $VMID selected successfully. " ) "
2025-04-09 10:12:04 +02:00
msg_info " $( translate "Detecting available disks..." ) "
2026-04-12 20:32:34 +02:00
_refresh_host_storage_cache
VM_CONFIG = $( qm config " $VMID " 2>/dev/null | grep -vE '^\s*#|^description:' )
2025-04-09 10:12:04 +02:00
FREE_DISKS = ( )
while read -r DISK; do
[ [ " $DISK " = ~ /dev/zd ] ] && continue
2026-04-12 20:32:34 +02:00
IFS = $'\t' read -r MODEL SIZE < <( get_disk_info " $DISK " )
2025-04-09 10:12:04 +02:00
LABEL = ""
SHOW_DISK = true
IS_MOUNTED = false
IS_RAID = false
IS_ZFS = false
IS_LVM = false
while read -r part fstype; do
[ [ " $fstype " = = "zfs_member" ] ] && IS_ZFS = true
[ [ " $fstype " = = "linux_raid_member" ] ] && IS_RAID = true
[ [ " $fstype " = = "LVM2_member" ] ] && IS_LVM = true
if grep -q " /dev/ $part " <<< " $MOUNTED_DISKS " ; then
IS_MOUNTED = true
fi
done < <( lsblk -ln -o NAME,FSTYPE " $DISK " | tail -n +2)
REAL_PATH = $( readlink -f " $DISK " )
if echo " $LVM_DEVICES " | grep -qFx " $REAL_PATH " ; then
IS_MOUNTED = true
fi
USED_BY = ""
2026-04-12 20:32:34 +02:00
if _disk_used_in_guest_configs " $DISK " ; then
2025-04-09 10:12:04 +02:00
USED_BY = " ⚠ $( translate "In use" ) "
fi
if $IS_RAID && grep -q " $DISK " <<< " $( cat /proc/mdstat) " ; then
if grep -q "active raid" /proc/mdstat; then
SHOW_DISK = false
fi
fi
if $IS_ZFS ; then
SHOW_DISK = false
fi
2026-04-12 20:32:34 +02:00
# Catch whole-disk ZFS vdevs with no partitions (e.g. bare NVMe ZFS)
# The tail -n +2 trick misses them; ZFS_DISKS from _refresh_host_storage_cache covers them.
if [ [ -n " $ZFS_DISKS " ] ] && \
{ grep -qFx " $DISK " <<< " $ZFS_DISKS " || \
{ [ [ -n " $REAL_PATH " ] ] && grep -qFx " $REAL_PATH " <<< " $ZFS_DISKS " ; } ; } ; then
SHOW_DISK = false
fi
2025-04-09 10:12:04 +02:00
if $IS_MOUNTED ; then
SHOW_DISK = false
fi
2026-04-12 20:32:34 +02:00
if disk_referenced_in_config " $VM_CONFIG " " $DISK " ; then
2025-04-09 10:12:04 +02:00
SHOW_DISK = false
fi
if $SHOW_DISK ; then
[ [ -n " $USED_BY " ] ] && LABEL += " [ $USED_BY ] "
[ [ " $IS_RAID " = = true ] ] && LABEL += " ⚠ RAID"
[ [ " $IS_LVM " = = true ] ] && LABEL += " ⚠ LVM"
[ [ " $IS_ZFS " = = true ] ] && LABEL += " ⚠ ZFS"
DESCRIPTION = $( printf "%-30s %10s%s" " $MODEL " " $SIZE " " $LABEL " )
FREE_DISKS += ( " $DISK " " $DESCRIPTION " "OFF" )
fi
done < <( lsblk -dn -e 7,11 -o PATH)
if [ " ${# FREE_DISKS [@] } " -eq 0 ] ; then
2026-04-12 20:32:34 +02:00
stop_spinner
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No disks available for this VM." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
2026-04-12 20:32:34 +02:00
stop_spinner
2025-04-09 10:12:04 +02:00
msg_ok " $( translate "Available disks detected." ) "
2026-04-12 20:32:34 +02:00
# ── DIALOG PHASE: select disks + interface ────────────────────────────────────
2025-04-09 10:12:04 +02:00
MAX_WIDTH = $( printf "%s\n" " ${ FREE_DISKS [@] } " | awk '{print length}' | sort -nr | head -n1)
TOTAL_WIDTH = $(( MAX_WIDTH + 20 ))
2026-04-12 20:32:34 +02:00
if [ $TOTAL_WIDTH -lt $UI_MENU_W ] ; then
TOTAL_WIDTH = $UI_MENU_W
fi
if [ $TOTAL_WIDTH -gt 116 ] ; then
TOTAL_WIDTH = 116
2025-04-09 10:12:04 +02:00
fi
2026-04-12 20:32:34 +02:00
SELECTED = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Select Disks" ) " \
--checklist " \n $( translate "Select the disks you want to add:" ) " $UI_MENU_H $TOTAL_WIDTH $UI_MENU_LIST_H \
" ${ FREE_DISKS [@] } " \
2>& 1 >/dev/tty)
2025-04-09 10:12:04 +02:00
if [ -z " $SELECTED " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No disks were selected." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
2026-04-12 20:32:34 +02:00
INTERFACE = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Interface Type" ) " \
--menu " $( translate "Select the interface type for all disks:" ) " $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
"sata" " $( translate "Add as SATA" ) " \
"scsi" " $( translate "Add as SCSI" ) " \
2025-04-09 10:12:04 +02:00
"virtio" " $( translate "Add as VirtIO" ) " \
2026-04-12 20:32:34 +02:00
"ide" " $( translate "Add as IDE" ) " \
2>& 1 >/dev/tty)
2025-04-09 10:12:04 +02:00
if [ -z " $INTERFACE " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No interface type was selected for the disks." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
2026-04-12 20:32:34 +02:00
# ── DIALOG PHASE: per-disk pre-check ──────────────────────────────────────────
declare -a DISK_LIST = ( )
declare -a DISK_DESCRIPTIONS = ( )
declare -a DISK_ASSIGNED_TOS = ( )
declare -a NVME_SKIPPED = ( )
2025-04-09 10:12:04 +02:00
for DISK in $SELECTED ; do
2026-04-12 20:32:34 +02:00
DISK = " ${ DISK // \" / } "
2025-04-09 10:12:04 +02:00
DISK_INFO = $( get_disk_info " $DISK " )
ASSIGNED_TO = ""
RUNNING_VMS = ""
RUNNING_CTS = ""
while read -r VM_ID VM_NAME; do
2026-04-12 20:32:34 +02:00
VM_CONFIG_RAW = $( qm config " $VM_ID " 2>/dev/null)
if [ [ " $VM_ID " = ~ ^[ 0-9] +$ ] ] && disk_referenced_in_config " $VM_CONFIG_RAW " " $DISK " ; then
2025-04-09 10:12:04 +02:00
ASSIGNED_TO += " VM $VM_ID $VM_NAME \n "
2026-04-12 20:32:34 +02:00
VM_STATUS_CHK = $( qm status " $VM_ID " | awk '{print $2}' )
[ [ " $VM_STATUS_CHK " = = "running" ] ] && RUNNING_VMS += " VM $VM_ID $VM_NAME \n "
2025-04-09 10:12:04 +02:00
fi
done < <( qm list | awk 'NR>1 {print $1, $2}' )
while read -r CT_ID CT_NAME; do
2026-04-12 20:32:34 +02:00
CT_CONFIG_RAW = $( pct config " $CT_ID " 2>/dev/null)
if [ [ " $CT_ID " = ~ ^[ 0-9] +$ ] ] && disk_referenced_in_config " $CT_CONFIG_RAW " " $DISK " ; then
2025-04-09 10:12:04 +02:00
ASSIGNED_TO += " CT $CT_ID $CT_NAME \n "
2026-04-12 20:32:34 +02:00
CT_STATUS_CHK = $( pct status " $CT_ID " | awk '{print $2}' )
[ [ " $CT_STATUS_CHK " = = "running" ] ] && RUNNING_CTS += " CT $CT_ID $CT_NAME \n "
2025-04-09 10:12:04 +02:00
fi
2026-04-12 20:32:34 +02:00
done < <( pct list | awk 'NR>1 {print $1, $3}' )
2025-04-09 10:12:04 +02:00
if [ -n " $RUNNING_VMS " ] || [ -n " $RUNNING_CTS " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Disk In Use" ) " \
--msgbox " $( translate "The disk" ) $DISK_INFO $( translate "is in use by the following running VM(s) or CT(s):" ) \\n $RUNNING_VMS $RUNNING_CTS \\n\\n $( translate "Stop them first and run this script again." ) " $UI_RESULT_H $UI_RESULT_W
2025-04-09 10:12:04 +02:00
continue
fi
if [ -n " $ASSIGNED_TO " ] ; then
2026-04-12 20:32:34 +02:00
if ! dialog --backtitle " $BACKTITLE " \
--title " $( translate "Disk Already Assigned" ) " \
--yesno " \n\n $( translate "The disk" ) $DISK_INFO $( translate "is already assigned to the following VM(s) or CT(s):" ) \\n $ASSIGNED_TO \\n\\n $( translate "Do you want to continue anyway?" ) " $UI_YESNO_H $UI_YESNO_W ; then
continue
fi
fi
# NVMe: suggest PCIe passthrough for better performance
if [ [ " $DISK " = ~ /dev/nvme ] ] || \
[ [ " $( lsblk -dn -o TRAN " $DISK " 2>/dev/null | xargs) " = = "nvme" ] ] ; then
NVME_CHOICE = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "NVMe Disk Detected" ) " \
--default-item "disk" \
--menu " \n $( translate "Adding this NVMe as a PCIe device (via 'Add Controller or NVMe PCIe to VM') gives better performance." ) \n\n $( translate "How do you want to add it?" ) " \
$UI_YESNO_H $UI_YESNO_W 2 \
"disk" " $( translate "Add as disk (standard)" ) " \
"pci" " $( translate "Skip — I will add it as PCIe device" ) " \
2>& 1 >/dev/tty)
if [ [ " $NVME_CHOICE " = = "pci" ] ] ; then
NVME_SKIPPED += ( " $DISK " )
continue
2025-04-09 10:12:04 +02:00
fi
fi
2026-04-12 20:32:34 +02:00
DISK_LIST += ( " $DISK " )
DISK_DESCRIPTIONS += ( " $DISK_INFO " )
DISK_ASSIGNED_TOS += ( " $ASSIGNED_TO " )
done
if [ " ${# DISK_LIST [@] } " -eq 0 ] ; then
show_proxmenux_logo
msg_title " $( translate "Import Disk to VM" ) "
msg_warn " $( translate "No disks were configured for processing." ) "
echo ""
msg_success " $( translate "Press Enter to return to menu..." ) "
read -r
exit 0
fi
# ── TERMINAL PHASE: execute all disk operations ───────────────────────────────
show_proxmenux_logo
msg_title " $( translate "Import Disk to VM" ) "
msg_ok " $( translate " VM $VMID selected successfully. " ) "
msg_ok " $( translate "Disks to process:" ) ${# DISK_LIST [@] } "
for i in " ${ !DISK_LIST[@] } " ; do
IFS = $'\t' read -r _desc_model _desc_size <<< " ${ DISK_DESCRIPTIONS [ $i ] } "
echo -e " ${ TAB } ${ BL } ${ DISK_LIST [ $i ] } $_desc_model $_desc_size ${ CL } "
done
if [ [ ${# NVME_SKIPPED [@] } -gt 0 ] ] ; then
echo ""
msg_warn " $( translate "NVMe skipped (to add as PCIe use 'Add Controller or NVMe PCIe to VM'):" ) "
for _nvme in " ${ NVME_SKIPPED [@] } " ; do
echo -e " ${ TAB } ${ BL } ${ _nvme } ${ CL } "
done
fi
echo ""
msg_ok " $( translate "Interface type:" ) $INTERFACE "
echo ""
DISKS_ADDED = 0
for i in " ${ !DISK_LIST[@] } " ; do
DISK = " ${ DISK_LIST [ $i ] } "
ASSIGNED_TO = " ${ DISK_ASSIGNED_TOS [ $i ] } "
IFS = $'\t' read -r _model _size <<< " ${ DISK_DESCRIPTIONS [ $i ] } "
2025-04-09 10:12:04 +02:00
INDEX = 0
while qm config " $VMID " | grep -q " ${ INTERFACE } ${ INDEX } " ; do
( ( INDEX++) )
done
2026-04-12 20:32:34 +02:00
ASSIGN_PATH = $( get_preferred_disk_path " $DISK " )
msg_info " $( translate "Adding" ) $_model $_size $( translate "as" ) ${ INTERFACE } ${ INDEX } ... "
if RESULT = $( qm set " $VMID " " - ${ INTERFACE } ${ INDEX } " " $ASSIGN_PATH " 2>& 1) ; then
msg_ok " $( translate "Disk added as" ) ${ INTERFACE } ${ INDEX } $( translate "using" ) $ASSIGN_PATH "
[ [ -n " $ASSIGNED_TO " ] ] && msg_warn " $( translate "WARNING: This disk is also assigned to:" ) $( echo -e " $ASSIGNED_TO " | tr '\n' ' ' ) "
2025-04-09 10:12:04 +02:00
( ( DISKS_ADDED++) )
else
2026-04-12 20:32:34 +02:00
msg_error " $( translate "Could not add" ) $_model $_size : $RESULT "
2025-04-09 10:12:04 +02:00
fi
done
2026-04-12 20:32:34 +02:00
echo ""
if [ " $DISKS_ADDED " -gt 0 ] ; then
msg_ok " $( translate "Completed." ) $DISKS_ADDED $( translate "disk(s) added to VM" ) $VMID . "
else
msg_warn " $( translate "No disks were added." ) "
2025-04-09 10:12:04 +02:00
fi
2026-04-12 20:32:34 +02:00
msg_success " $( translate "Press Enter to return to menu..." ) "
read -r
2025-04-09 10:12:04 +02:00
exit 0