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.3
# Last Updated: 07/04/2026
2025-04-09 10:12:04 +02:00
# ==========================================================
# Description:
# This script allows users to assign physical disks to existing
# Proxmox containers (CTs) through an interactive menu.
# - Detects the system disk and excludes it from selection.
# - Lists all available CTs for the user to choose from.
# - Identifies and displays unassigned physical disks.
# - Allows the user to select multiple disks and attach them to a CT.
# - Configures the selected disks for the CT and verifies the assignment.
2025-07-30 18:43:54 +02:00
# - Uses persistent device paths to avoid issues with device order changes.
2025-04-09 10:12:04 +02:00
# ==========================================================
# 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
2025-06-28 21:43:48 +02:00
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-07-30 18:43:54 +02:00
2025-06-28 21:43:48 +02:00
# ==========================================================
2025-04-09 10:12:04 +02:00
2026-04-12 20:32:34 +02:00
# Returns the most stable /dev/disk/by-id symlink for a device.
# Prefers ata-/scsi-/nvme- > wwn- > other by-id > by-path > raw path.
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
2025-07-30 18:43:54 +02:00
fi
done
2026-04-12 20:32:34 +02:00
if [ [ -n " $best " ] ] ; then
echo " $best "
2025-07-30 18:43:54 +02:00
return 0
fi
2026-04-12 20:32:34 +02:00
for link in /dev/disk/by-path/*; do
[ [ -e " $link " ] ] || continue
[ [ " $( readlink -f " $link " 2>/dev/null) " = = " $real_path " ] ] || continue
echo " $link "
return 0
2025-07-30 18:43:54 +02:00
done
2026-04-12 20:32:34 +02:00
msg_warn " $( translate "No persistent path found for" ) $disk — $( translate "using direct path (not guaranteed to survive reboots)." ) "
echo " $disk "
2025-07-30 18:43:54 +02:00
}
2026-04-12 20:32:34 +02:00
install_fs_tools_in_ct( ) {
local ctid = " $1 "
local pkg = " $2 "
if pct exec " $ctid " -- sh -c "[ -f /etc/alpine-release ]" ; then
pct exec " $ctid " -- sh -c " apk update >/dev/null 2>&1 && apk add --no-progress $pkg >/dev/null 2>&1 "
elif pct exec " $ctid " -- sh -c "grep -qi 'arch' /etc/os-release 2>/dev/null" ; then
pct exec " $ctid " -- sh -c " pacman -Sy --noconfirm $pkg >/dev/null 2>&1 "
elif pct exec " $ctid " -- sh -c "grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null" ; then
pct exec " $ctid " -- sh -c " apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq $pkg >/dev/null 2>&1 "
else
return 1
2025-07-30 18:43:54 +02:00
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
# Suggest an unused mount point inside the CT for the given disk.
# Reads the CT config to collect already-used mp= paths, then returns
# the first free candidate: /mnt/disk_<devname>, /mnt/disk_<devname>_2, ...
_get_suggested_mount_point( ) {
local ctid = " $1 "
local disk = " $2 "
local devname
devname = $( basename " $disk " )
local base = " /mnt/disk_ ${ devname } "
local used_mps
used_mps = $( pct config " $ctid " 2>/dev/null | grep '^mp[0-9]*:' | \
grep -oP 'mp=\K[^,]+' | sort)
if ! grep -qxF " $base " <<< " $used_mps " ; then
echo " $base " ; return
fi
local n = 2
while grep -qxF " ${ base } _ ${ n } " <<< " $used_mps " ; do
( ( n++) )
done
echo " ${ base } _ ${ n } "
}
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
}
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
}
2025-06-28 21:43:48 +02:00
2025-07-30 18:43:54 +02:00
CT_LIST = $( pct list | awk 'NR>1 {print $1, $3}' )
2025-04-09 10:12:04 +02:00
if [ -z " $CT_LIST " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No CTs 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 # CT_LIST is intentionally word-split into dialog menu pairs
CTID = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Select CT for destination disk" ) " \
--menu " $( translate "Select the CT to which you want to add disks:" ) " $UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
$CT_LIST \
2>& 1 >/dev/tty)
2025-04-09 10:12:04 +02:00
if [ -z " $CTID " ] ; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No CT was selected." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
CTID = $( echo " $CTID " | tr -d '"' )
CT_STATUS = $( pct status " $CTID " | awk '{print $2}' )
2026-04-12 20:32:34 +02:00
CT_RUNNING = false
[ [ " $CT_STATUS " = = "running" ] ] && CT_RUNNING = true
# ── Check for unprivileged container — also a dialog, stays before show_proxmenux_logo ──
CONF_FILE = " /etc/pve/lxc/ $CTID .conf "
CONVERT_PRIVILEGED = false
if grep -q '^unprivileged: 1' " $CONF_FILE " ; then
if dialog --backtitle " $BACKTITLE " \
--title " $( translate "Privileged Container" ) " \
--yesno " \n\n $( translate "The selected container is unprivileged. A privileged container is required for direct device passthrough." ) \\n\\n $( translate "Do you want to convert it to a privileged container now?" ) " $UI_YESNO_H $UI_YESNO_W ; then
CONVERT_PRIVILEGED = true
else
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Aborted" ) " \
--msgbox " $( translate "Operation cancelled. Cannot continue with an unprivileged container." ) " $UI_MSG_H $UI_MSG_W
2025-04-09 10:12:04 +02:00
exit 1
fi
fi
2026-04-12 20:32:34 +02:00
# ── TERMINAL PHASE 1 ──────────────────────────────────────────────────────────
show_proxmenux_logo
msg_title " $( translate "Import Disk to LXC" ) "
msg_ok " $( translate " CT $CTID selected successfully. " ) "
if [ " $CONVERT_PRIVILEGED " = true ] ; then
show_proxmenux_logo
msg_title " $( translate "Import Disk to LXC" ) "
CURRENT_CT_STATUS = $( pct status " $CTID " | awk '{print $2}' )
if [ " $CURRENT_CT_STATUS " = = "running" ] ; then
2025-04-09 10:12:04 +02:00
msg_info " $( translate "Stopping container" ) $CTID ... "
2025-04-09 11:32:12 +02:00
pct shutdown " $CTID " & >/dev/null
2025-04-09 10:12:04 +02:00
for i in { 1..10} ; do
sleep 1
2026-04-12 20:32:34 +02:00
[ " $( pct status " $CTID " | awk '{print $2}' ) " != "running" ] && break
2025-04-09 10:12:04 +02:00
done
if [ " $( pct status " $CTID " | awk '{print $2}' ) " = = "running" ] ; then
msg_error " $( translate "Failed to stop the container." ) "
exit 1
fi
msg_ok " $( translate "Container stopped." ) "
2026-04-12 20:32:34 +02:00
fi
cp " $CONF_FILE " " $CONF_FILE .bak "
sed -i '/^unprivileged: 1/d' " $CONF_FILE "
echo "unprivileged: 0" >> " $CONF_FILE "
msg_ok " $( translate "Container successfully converted to privileged." ) "
if [ " $CT_RUNNING " = true ] ; then
2025-04-09 10:12:04 +02:00
msg_info " $( translate "Starting container" ) $CTID ... "
2025-04-09 11:32:12 +02:00
pct start " $CTID " & >/dev/null
2025-04-09 10:12:04 +02:00
sleep 2
if [ " $( pct status " $CTID " | awk '{print $2}' ) " != "running" ] ; then
msg_error " $( translate "Failed to start the container." ) "
2026-04-12 20:32:34 +02:00
CT_RUNNING = false
else
msg_ok " $( translate "Container started successfully." ) "
2025-04-09 10:12:04 +02:00
fi
fi
fi
##########################################
msg_info " $( translate "Detecting available disks..." ) "
2026-04-12 20:32:34 +02:00
_refresh_host_storage_cache
# Read this CT's current config for the "already assigned to this CT" check
CT_CONFIG = $( pct config " $CTID " 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
2025-07-30 18:43:54 +02:00
2025-04-09 10:12:04 +02:00
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)
2025-07-30 18:43:54 +02:00
2025-04-09 10:12:04 +02:00
REAL_PATH = $( readlink -f " $DISK " )
if echo " $LVM_DEVICES " | grep -qFx " $REAL_PATH " ; then
IS_MOUNTED = true
fi
2025-07-30 18:43:54 +02:00
2025-04-09 10:12:04 +02:00
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
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
if $IS_RAID && grep -q " $DISK " <<< " $( cat /proc/mdstat) " ; then
if grep -q "active raid" /proc/mdstat; then
SHOW_DISK = false
fi
fi
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
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 " $CT_CONFIG " " $DISK " ; then
2025-04-09 10:12:04 +02:00
SHOW_DISK = false
fi
2025-07-30 18:43:54 +02:00
2025-04-09 10:12:04 +02:00
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"
2025-07-30 18:43:54 +02:00
2025-04-09 10:12:04 +02:00
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 CT." ) " $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
######################################################
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 " $( 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
show_proxmenux_logo
msg_title " $( translate "Import Disk to LXC" ) "
msg_ok " $( translate " CT $CTID selected successfully. " ) "
msg_info " $( translate "Analyzing selected disks..." ) "
2025-04-09 10:12:04 +02:00
2026-04-12 20:32:34 +02:00
# ── DIALOG PHASE: collect config for each disk ────────────────────────────────
declare -a DISK_LIST = ( )
declare -a DISK_DESCRIPTIONS = ( )
declare -a DISK_MOUNT_POINTS = ( )
declare -a DISK_SKIP_FORMATS = ( )
declare -a DISK_FORMAT_TYPES = ( )
declare -a DISK_NEEDS_PARTITION = ( )
declare -a DISK_PARTITIONS = ( )
declare -a DISK_ASSIGNED_TOS = ( )
declare -a DISK_CURRENT_FSes = ( )
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 " )
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
ASSIGNED_TO = ""
RUNNING_CTS = ""
RUNNING_VMS = ""
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
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 "
CT_STATUS = $( pct status " $CT_ID " | awk '{print $2}' )
2026-04-12 20:32:34 +02:00
[ [ " $CT_STATUS " = = "running" ] ] && RUNNING_CTS += " CT $CT_ID $CT_NAME \n "
2025-04-09 10:12:04 +02:00
fi
done < <( pct list | awk 'NR>1 {print $1, $3}' )
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
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 "
VM_STATUS = $( qm status " $VM_ID " | awk '{print $2}' )
2026-04-12 20:32:34 +02:00
[ [ " $VM_STATUS " = = "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}' )
2026-04-12 20:32:34 +02:00
stop_spinner
2025-04-09 10:12:04 +02:00
if [ -n " $RUNNING_CTS " ] || [ -n " $RUNNING_VMS " ] ; 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_CTS $RUNNING_VMS \\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
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
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
2025-04-09 10:12:04 +02:00
fi
fi
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
if lsblk " $DISK " | grep -q "raid" || grep -q " ${ DISK ##*/ } " /proc/mdstat; then
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "RAID Detected" ) " \
--msgbox " \n $( translate "The disk" ) $DISK_INFO $( translate "appears to be part of a" ) RAID. $( translate "For security reasons, the system cannot format it." ) \\n\\n $( translate "If you are sure you want to use it, please remove the" ) RAID metadata $( translate "or format it manually using external tools." ) \\n\\n $( translate "After that, run this script again to add it." ) " $UI_RESULT_H $UI_RESULT_W
2025-04-09 10:12:04 +02:00
continue
fi
2026-04-12 20:32:34 +02:00
# 1. Detect current partition/FS state
2025-04-09 10:12:04 +02:00
PARTITION = $( lsblk -rno NAME " $DISK " | awk -v disk = " $( basename " $DISK " ) " '$1 != disk {print $1; exit}' )
SKIP_FORMAT = false
2026-04-12 20:32:34 +02:00
NEEDS_PARTITION = false
2025-04-09 10:12:04 +02:00
if [ -n " $PARTITION " ] ; then
PARTITION = " /dev/ $PARTITION "
CURRENT_FS = $( lsblk -no FSTYPE " $PARTITION " | xargs)
else
CURRENT_FS = $( lsblk -no FSTYPE " $DISK " | xargs)
2026-04-12 20:32:34 +02:00
PARTITION = " $DISK "
fi
# 2. Ask what to do with this disk
if [ -n " $CURRENT_FS " ] ; then
# Disk already has a filesystem — offer use-as-is or reformat
DISK_ACTION = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Disk Setup" ) " \
--menu " $( translate "Disk" ) $DISK_INFO \n $( translate "Detected filesystem:" ) $CURRENT_FS \n\n $( translate "What do you want to do?" ) " \
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
"use" " $( translate "Use as-is — keep data and filesystem" ) " \
"format" " $( translate "Format — erase and create new filesystem" ) " \
2>& 1 >/dev/tty)
[ -z " $DISK_ACTION " ] && continue
else
DISK_ACTION = "format"
fi
FORMAT_TYPE = ""
if [ " $DISK_ACTION " = "use" ] ; then
SKIP_FORMAT = true
FORMAT_TYPE = " $CURRENT_FS "
# PARTITION already set correctly by the detection block above — do not modify
else
# 3. Ask desired filesystem for format
FORMAT_TYPE = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Select Filesystem" ) " \
--menu " $( translate "Select the filesystem for" ) $DISK_INFO : " \
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
"ext4" " $( translate "ext4 — recommended, most compatible" ) " \
"xfs" " $( translate "xfs — better for large files" ) " \
"btrfs" " $( translate "btrfs — snapshots and compression" ) " \
2>& 1 >/dev/tty)
[ -z " $FORMAT_TYPE " ] && continue
# Check if already the right FS — otherwise need partition + format
if [ " $CURRENT_FS " = " $FORMAT_TYPE " ] ; then
2025-04-09 10:12:04 +02:00
SKIP_FORMAT = true
2026-04-12 20:32:34 +02:00
elif [ -z " $CURRENT_FS " ] && [ " $PARTITION " = " $DISK " ] ; then
NEEDS_PARTITION = true
PARTITION = ""
fi
# 4. Warn if data will be erased
if [ " $SKIP_FORMAT " != true ] ; then
if ! dialog --backtitle " $BACKTITLE " \
--title " $( translate "WARNING" ) " \
--yesno " \n $( translate "WARNING: This will FORMAT the disk" ) $DISK_INFO $( translate "as" ) $FORMAT_TYPE .\\n\\n $( translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!" ) \\n\\n $( translate "Are you sure?" ) " \
$UI_YESNO_H $UI_YESNO_W ; then
2025-04-09 10:12:04 +02:00
continue
fi
fi
fi
2026-04-12 20:32:34 +02:00
# 5. Ask mount point — suggest unique path based on device name
SUGGESTED_MP = $( _get_suggested_mount_point " $CTID " " $DISK " )
MOUNT_POINT = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Mount Point" ) " \
--inputbox " $( translate "Enter the mount point inside the CT for" ) $DISK_INFO : " \
$UI_MSG_H $UI_MSG_W " $SUGGESTED_MP " \
2>& 1 >/dev/tty)
if [ -z " $MOUNT_POINT " ] ; then
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Error" ) " \
--msgbox " $( translate "No mount point was specified." ) " $UI_MSG_H $UI_MSG_W
continue
fi
DISK_LIST += ( " $DISK " )
DISK_DESCRIPTIONS += ( " $DISK_INFO " )
DISK_MOUNT_POINTS += ( " $MOUNT_POINT " )
DISK_SKIP_FORMATS += ( " $SKIP_FORMAT " )
DISK_FORMAT_TYPES += ( " $FORMAT_TYPE " )
DISK_NEEDS_PARTITION += ( " $NEEDS_PARTITION " )
DISK_PARTITIONS += ( " $PARTITION " )
DISK_ASSIGNED_TOS += ( " $ASSIGNED_TO " )
DISK_CURRENT_FSes += ( " $CURRENT_FS " )
done
if [ " ${# DISK_LIST [@] } " -eq 0 ] ; then
show_proxmenux_logo
msg_title " $( translate "Import Disk to LXC" ) "
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 LXC" ) "
msg_ok " $( translate " CT $CTID 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
echo ""
DISKS_ADDED = 0
for i in " ${ !DISK_LIST[@] } " ; do
DISK = " ${ DISK_LIST [ $i ] } "
MOUNT_POINT = " ${ DISK_MOUNT_POINTS [ $i ] } "
SKIP_FORMAT = " ${ DISK_SKIP_FORMATS [ $i ] } "
FORMAT_TYPE = " ${ DISK_FORMAT_TYPES [ $i ] } "
NEEDS_PARTITION = " ${ DISK_NEEDS_PARTITION [ $i ] } "
PARTITION = " ${ DISK_PARTITIONS [ $i ] } "
ASSIGNED_TO = " ${ DISK_ASSIGNED_TOS [ $i ] } "
CURRENT_FS = " ${ DISK_CURRENT_FSes [ $i ] } "
DISK_INFO = $( get_disk_info " $DISK " )
echo ""
msg_ok " $( translate "Disk:" ) $DISK → $MOUNT_POINT "
if [ " $NEEDS_PARTITION " = true ] ; then
msg_info " $( translate "Creating partition table and partition..." ) "
if ! parted -s " $DISK " mklabel gpt mkpart primary 0% 100% >/dev/null 2>& 1; then
msg_error " $( translate "Failed to create partition table on disk" ) $DISK_INFO . "
continue
fi
sleep 2
partprobe " $DISK " 2>/dev/null || true
udevadm settle 2>/dev/null || true
# Wait up to 5 s for by-id symlinks to be created by udev
for _i in { 1..5} ; do
for _p in /dev/disk/by-id/*; do
[ [ " $( readlink -f " $_p " 2>/dev/null) " = = " $DISK " * ] ] && break 2
done
sleep 1
done
PARTITION = $( lsblk -rno NAME " $DISK " | awk -v disk = " $( basename " $DISK " ) " '$1 != disk {print $1; exit}' )
if [ -n " $PARTITION " ] ; then
PARTITION = " /dev/ $PARTITION "
msg_ok " $( translate "Partition created:" ) $PARTITION "
2025-04-09 10:12:04 +02:00
else
2026-04-12 20:32:34 +02:00
msg_error " $( translate "Failed to detect partition on disk" ) $DISK_INFO . "
continue
2025-04-09 10:12:04 +02:00
fi
fi
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
if [ " $SKIP_FORMAT " != true ] ; then
2026-04-12 20:32:34 +02:00
msg_info " $( translate "Formatting partition" ) $PARTITION $( translate "with" ) $FORMAT_TYPE ... "
if ! case " $FORMAT_TYPE " in
"ext4" ) mkfs.ext4 -F " $PARTITION " >/dev/null 2>& 1 ; ;
"xfs" ) mkfs.xfs -f " $PARTITION " >/dev/null 2>& 1 ; ;
"btrfs" ) mkfs.btrfs -f " $PARTITION " >/dev/null 2>& 1 ; ;
esac ; then
msg_error " $( translate "Failed to format partition" ) $PARTITION $( translate "with" ) $FORMAT_TYPE . "
2025-04-09 10:12:04 +02:00
continue
fi
2026-04-12 20:32:34 +02:00
msg_ok " $( translate "Partition" ) $PARTITION $( translate "successfully formatted with" ) $FORMAT_TYPE . "
partprobe " $DISK " >/dev/null 2>& 1 || true
sleep 2
else
msg_ok " $( translate "Disk already has" ) $FORMAT_TYPE $( translate "filesystem. Skipping format." ) "
2025-04-09 10:12:04 +02:00
fi
2026-04-12 20:32:34 +02:00
2025-04-09 10:12:04 +02:00
INDEX = 0
while pct config " $CTID " | grep -q " mp ${ INDEX } : " ; do
( ( INDEX++) )
done
2026-04-12 20:32:34 +02:00
2025-06-28 21:43:48 +02:00
FS_PKG = ""
FS_BIN = ""
2026-04-12 20:32:34 +02:00
[ [ " $FORMAT_TYPE " = = "xfs" ] ] && FS_PKG = "xfsprogs" && FS_BIN = "xfs_repair"
[ [ " $FORMAT_TYPE " = = "btrfs" ] ] && FS_PKG = "btrfs-progs" && FS_BIN = "btrfsck"
2025-06-28 21:43:48 +02:00
if [ [ -n " $FS_PKG " && -n " $FS_BIN " ] ] ; then
2026-04-12 20:32:34 +02:00
if [ " $CT_RUNNING " = true ] ; then
if ! pct exec " $CTID " -- sh -c " command -v $FS_BIN >/dev/null 2>&1 " ; then
msg_info " $( translate " Installing required tools for $FORMAT_TYPE in CT $CTID ... " ) "
if install_fs_tools_in_ct " $CTID " " $FS_PKG " ; then
msg_ok " $( translate " Required tools for $FORMAT_TYPE installed in CT $CTID . " ) "
else
msg_warn " $( translate "Could not install" ) $FS_PKG $( translate "automatically. Install it manually inside the container." ) "
fi
fi
else
# CT is stopped — ask via whiptail (terminal-safe, no dialog on top of output)
if whiptail --backtitle " $BACKTITLE " \
--title " $( translate "Filesystem Tools Required" ) " \
--yesno " $( translate "The filesystem" ) $FORMAT_TYPE $( translate "requires the package" ) $FS_PKG $( translate "installed inside CT" ) $CTID .\n\n $( translate "The container is currently stopped. Do you want to start it now to install the package?" ) \n\n $( translate "If you choose No, install" ) $FS_PKG $( translate "manually inside the container before starting it." ) " \
$UI_YESNO_H $UI_YESNO_W ; then
msg_info " $( translate "Starting CT" ) $CTID ... "
pct start " $CTID " & >/dev/null
sleep 2
if [ " $( pct status " $CTID " | awk '{print $2}' ) " != "running" ] ; then
msg_error " $( translate "Failed to start CT" ) $CTID . $( translate "Install" ) $FS_PKG $( translate "manually inside the container." ) "
else
msg_ok " $( translate "CT" ) $CTID $( translate "started." ) "
CT_RUNNING = true
msg_info " $( translate "Installing" ) $FS_PKG $( translate "in CT" ) $CTID ... "
if install_fs_tools_in_ct " $CTID " " $FS_PKG " ; then
msg_ok " $FS_PKG $( translate "installed in CT" ) $CTID . "
else
msg_warn " $( translate "Could not install" ) $FS_PKG $( translate "automatically. Install it manually inside the container." ) "
fi
fi
else
msg_warn " $( translate "Manual install required inside CT" ) $CTID : "
echo -e " ${ DGN } ${ TAB } Debian/Ubuntu: ${ CL } ${ BL } apt-get install -y $FS_PKG ${ CL } "
echo -e " ${ DGN } ${ TAB } Arch: ${ CL } ${ BL } pacman -S --noconfirm $FS_PKG ${ CL } "
echo -e " ${ DGN } ${ TAB } Alpine: ${ CL } ${ BL } apk add $FS_PKG ${ CL } "
echo
2025-06-28 21:43:48 +02:00
fi
fi
fi
2026-04-12 20:32:34 +02:00
PERSISTENT_PARTITION = $( get_preferred_disk_path " $PARTITION " )
msg_info " $( translate "Applying passthrough to CT" ) $CTID ... "
if [ " $FORMAT_TYPE " = = "xfs" ] ; then
2025-07-30 18:43:54 +02:00
RESULT = $( pct set " $CTID " -mp${ INDEX } " $PERSISTENT_PARTITION ,mp= $MOUNT_POINT ,backup=0,ro=0 " 2>& 1)
2025-06-28 21:43:48 +02:00
else
2025-07-30 18:43:54 +02:00
RESULT = $( pct set " $CTID " -mp${ INDEX } " $PERSISTENT_PARTITION ,mp= $MOUNT_POINT ,backup=0,ro=0,acl=1 " 2>& 1)
2025-06-28 21:43:48 +02:00
fi
2026-04-12 20:32:34 +02:00
SET_STATUS = $?
if [ $SET_STATUS -eq 0 ] ; then
msg_ok " $( translate "Disk assigned at" ) $MOUNT_POINT $( translate "using" ) $PERSISTENT_PARTITION "
[ [ -n " $ASSIGNED_TO " ] ] && msg_warn " $( translate "WARNING: This disk is also assigned to:" ) $( echo -e " $ASSIGNED_TO " | tr '\n' ' ' ) "
# Verify disk is accessible inside the CT
if [ " $CT_RUNNING " = true ] ; then
msg_info " $( translate "Verifying disk accessibility in CT" ) $CTID ... "
sleep 1
if pct exec " $CTID " -- sh -c " mountpoint -q ' $MOUNT_POINT ' || [ -d ' $MOUNT_POINT ' ] " 2>/dev/null; then
msg_ok " $( translate "Disk verified and accessible inside CT at" ) $MOUNT_POINT "
fi
2025-04-09 10:12:04 +02:00
fi
2026-04-12 20:32:34 +02:00
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 disk" ) $DISK_INFO $( translate "to CT" ) $CTID . $( translate "Error:" ) $RESULT "
2025-04-09 10:12:04 +02:00
fi
2025-06-28 21:43:48 +02:00
done
2025-04-09 10:12:04 +02:00
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 CT" ) $CTID . "
else
msg_warn " $( translate "No disks were added." ) "
2025-04-09 10:12:04 +02:00
fi
2025-04-15 20:11:09 +02:00
msg_success " $( translate "Press Enter to return to menu..." ) "
read -r
2025-04-09 10:12:04 +02:00
exit 0