Update scripts

This commit is contained in:
MacRimi
2026-04-12 20:32:34 +02:00
parent 4fa4bbb08b
commit 4843fae0eb
47 changed files with 8313 additions and 3014 deletions

View File

@@ -14,6 +14,7 @@ LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
BASE_DIR="/usr/local/share/proxmenux"
TOOLS_JSON="$BASE_DIR/installed_tools.json"
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
@@ -51,22 +52,45 @@ SELECTED_VMID=""
SELECTED_VM_NAME=""
declare -a SELECTED_CONTROLLER_PCIS=()
IOMMU_PENDING_REBOOT=0
IOMMU_ALREADY_ACTIVE=0
NEED_HOOK_SYNC=false
WIZARD_CONFLICT_POLICY=""
WIZARD_CONFLICT_SCOPE=""
set_title() {
show_proxmenux_logo
msg_title "$(translate "Add Controller or NVMe PCIe to VM")"
}
ensure_tools_json() {
[[ -f "$TOOLS_JSON" ]] || echo "{}" > "$TOOLS_JSON"
}
register_tool() {
local tool="$1"
local state="$2"
command -v jq >/dev/null 2>&1 || return 0
ensure_tools_json
jq --arg t "$tool" --argjson v "$state" \
'.[$t]=$v' "$TOOLS_JSON" > "$TOOLS_JSON.tmp" \
&& mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
}
register_vfio_iommu_tool() {
register_tool "vfio_iommu" true || true
}
enable_iommu_cmdline() {
local silent="${1:-}"
local cpu_vendor iommu_param
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
if [[ "$cpu_vendor" == "GenuineIntel" ]]; then
iommu_param="intel_iommu=on"
msg_info "$(translate "Intel CPU detected")"
[[ "$silent" != "silent" ]] && msg_info "$(translate "Intel CPU detected")"
elif [[ "$cpu_vendor" == "AuthenticAMD" ]]; then
iommu_param="amd_iommu=on"
msg_info "$(translate "AMD CPU detected")"
[[ "$silent" != "silent" ]] && msg_info "$(translate "AMD CPU detected")"
else
msg_error "$(translate "Unknown CPU vendor. Cannot determine IOMMU parameter.")"
return 1
@@ -76,22 +100,22 @@ enable_iommu_cmdline() {
local grub_file="/etc/default/grub"
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
if ! grep -q "$iommu_param" "$cmdline_file"; then
if ! grep -q "$iommu_param" "$cmdline_file" || ! grep -q "iommu=pt" "$cmdline_file"; then
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
proxmox-boot-tool refresh >/dev/null 2>&1 || true
msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to /etc/kernel/cmdline")"
else
msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in /etc/kernel/cmdline")"
fi
elif [[ -f "$grub_file" ]]; then
if ! grep -q "$iommu_param" "$grub_file"; then
if ! grep -q "$iommu_param" "$grub_file" || ! grep -q "iommu=pt" "$grub_file"; then
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
update-grub >/dev/null 2>&1 || true
msg_ok "$(translate "IOMMU parameters added to GRUB")"
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU parameters added to GRUB")"
else
msg_ok "$(translate "IOMMU already configured in GRUB")"
[[ "$silent" != "silent" ]] && msg_ok "$(translate "IOMMU already configured in GRUB")"
fi
else
msg_error "$(translate "Neither /etc/kernel/cmdline nor /etc/default/grub found.")"
@@ -101,24 +125,29 @@ enable_iommu_cmdline() {
check_iommu_or_offer_enable() {
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
register_vfio_iommu_tool
return 0
fi
if grep -qE 'intel_iommu=on|amd_iommu=on' /etc/kernel/cmdline 2>/dev/null || \
grep -qE 'intel_iommu=on|amd_iommu=on' /etc/default/grub 2>/dev/null; then
IOMMU_PENDING_REBOOT=1
msg_warn "$(translate "IOMMU is configured for next boot, but not active yet.")"
msg_info2 "$(translate "Controller/NVMe assignment can continue now and will be effective after reboot.")"
register_vfio_iommu_tool
return 0
fi
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
IOMMU_ALREADY_ACTIVE=1
register_vfio_iommu_tool
return 0
fi
if grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null && \
[[ -d /sys/kernel/iommu_groups ]] && \
[[ -n "$(ls /sys/kernel/iommu_groups/ 2>/dev/null)" ]]; then
IOMMU_ALREADY_ACTIVE=1
register_vfio_iommu_tool
return 0
fi
@@ -133,13 +162,11 @@ check_iommu_or_offer_enable() {
--title "$(translate "IOMMU Required")" \
--yesno "$msg" 15 74
local response=$?
clear
[[ $response -ne 0 ]] && return 1
set_title
msg_title "$(translate "Enabling IOMMU")"
echo
if ! enable_iommu_cmdline; then
echo
msg_error "$(translate "Failed to configure IOMMU automatically.")"
@@ -148,37 +175,36 @@ check_iommu_or_offer_enable() {
return 1
fi
echo
msg_success "$(translate "IOMMU configured. Reboot required before using Controller/NVMe passthrough.")"
echo
if whiptail --title "$(translate "Reboot Required")" \
--yesno "$(translate "Do you want to reboot now?")" 10 64; then
msg_warn "$(translate "Rebooting the system...")"
reboot
else
IOMMU_PENDING_REBOOT=1
msg_warn "$(translate "Reboot postponed by user.")"
msg_info2 "$(translate "You can continue assigning Controller/NVMe now, but reboot the host before starting the VM.")"
msg_success "$(translate "Press Enter to continue...")"
read -r
fi
register_vfio_iommu_tool
IOMMU_PENDING_REBOOT=1
return 0
}
select_target_vm() {
local -a vm_menu=()
local line vmid vmname vmstatus vm_machine status_label
local max_name_len=0 padded_name
while IFS= read -r line; do
vmid=$(awk '{print $1}' <<< "$line")
vmname=$(awk '{print $2}' <<< "$line")
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue
[[ ${#vmname} -gt $max_name_len ]] && max_name_len=${#vmname}
done < <(qm list 2>/dev/null)
while IFS= read -r line; do
vmid=$(awk '{print $1}' <<< "$line")
vmname=$(awk '{print $2}' <<< "$line")
vmstatus=$(awk '{print $3}' <<< "$line")
[[ -z "$vmid" || "$vmid" == "VMID" ]] && continue
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]] || continue
vm_machine=$(qm config "$vmid" 2>/dev/null | awk -F': ' '/^machine:/ {print $2}')
[[ -z "$vm_machine" ]] && vm_machine="unknown"
status_label="${vmstatus}, ${vm_machine}"
vm_menu+=("$vmid" "${vmname} [${status_label}]")
printf -v padded_name "%-${max_name_len}s" "$vmname"
vm_menu+=("$vmid" "${padded_name} [${status_label}]")
done < <(qm list 2>/dev/null)
if [[ ${#vm_menu[@]} -eq 0 ]]; then
dialog --backtitle "ProxMenux" \
@@ -221,6 +247,10 @@ validate_vm_requirements() {
}
select_controller_nvme() {
# Show progress during potentially slow PCIe + disk detection
set_title
msg_info "$(translate "Analyzing system for available PCIe storage devices...")"
_refresh_host_storage_cache
local -a menu_items=()
@@ -251,12 +281,19 @@ select_controller_nvme() {
_array_contains "$disk" "${controller_disks[@]}" || controller_disks+=("$disk")
done < <(_controller_block_devices "$pci_full")
# blocked_reasons: system disk OR disk in RUNNING guest → hide controller
# warn_reasons: disk in STOPPED guest only → show with ⚠ but allow selection
local -a blocked_reasons=()
local -a warn_reasons=()
for disk in "${controller_disks[@]}"; do
if _disk_is_host_system_used "$disk"; then
blocked_reasons+=("${disk} (${DISK_USAGE_REASON})")
elif _disk_used_in_guest_configs "$disk"; then
blocked_reasons+=("${disk} ($(translate "In use by VM/LXC config"))")
if _disk_used_in_running_guest "$disk"; then
blocked_reasons+=("${disk} ($(translate "In use by running VM/LXC — stop it first"))")
else
warn_reasons+=("$disk")
fi
fi
done
@@ -266,21 +303,30 @@ select_controller_nvme() {
continue
fi
local short_name
short_name=$(_shorten_text "$name" 42)
local short_name display_name
display_name=$(_pci_storage_display_name "$pci_full")
short_name=$(_shorten_text "$display_name" 56)
local assigned_suffix=""
if [[ -n "$(_pci_assigned_vm_ids "$pci_full" "$SELECTED_VMID" 2>/dev/null | head -1)" ]]; then
assigned_suffix=" | $(translate "Assigned to VM")"
fi
controller_desc="${short_name}${assigned_suffix}"
# Warn if some disks are referenced in stopped VM/CT configs
local warn_suffix=""
if [[ ${#warn_reasons[@]} -gt 0 ]]; then
warn_suffix=" ⚠"
fi
controller_desc="${short_name}${assigned_suffix}${warn_suffix}"
state="off"
menu_items+=("$pci_full" "$controller_desc" "$state")
safe_count=$((safe_count + 1))
done < <(ls -d /sys/bus/pci/devices/* 2>/dev/null | sort)
stop_spinner
if [[ "$safe_count" -eq 0 ]]; then
local msg
if [[ "$hidden_target_count" -gt 0 && "$blocked_count" -eq 0 ]]; then
@@ -318,29 +364,100 @@ select_controller_nvme() {
return 1
fi
if declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>&1; then
if ! _vm_storage_confirm_controller_passthrough_risk "$SELECTED_VMID" "$SELECTED_VM_NAME" "$(translate "Controller + NVMe")"; then
return 1
fi
fi
return 0
}
confirm_summary() {
local msg
msg="\n$(translate "The following devices will be added to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\n\n"
local pci info
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
info=$(lspci -nn -s "${pci#0000:}" 2>/dev/null | sed 's/^[^ ]* //')
msg+=" - ${pci}${info:+ (${info})}\n"
done
msg+="\n$(translate "Do you want to continue?")"
_prompt_raw_disk_conflict_policy() {
local disk="$1"
shift
local -a guest_ids=("$@")
local msg gid gtype gid_num gname gstatus
dialog --backtitle "ProxMenux" --colors \
msg="$(translate "Disk") ${disk} $(translate "is referenced in the following stopped VM(s)/CT(s):")\\n\\n"
for gid in "${guest_ids[@]}"; do
gtype="${gid%%:*}"; gid_num="${gid##*:}"
if [[ "$gtype" == "VM" ]]; then
gname=$(_vm_name_by_id "$gid_num")
gstatus=$(qm status "$gid_num" 2>/dev/null | awk '{print $2}')
msg+=" - VM $gid_num ($gname) [${gstatus}]\\n"
else
gname=$(pct config "$gid_num" 2>/dev/null | awk '/^hostname:/ {print $2}')
[[ -z "$gname" ]] && gname="CT-$gid_num"
gstatus=$(pct status "$gid_num" 2>/dev/null | awk '{print $2}')
msg+=" - CT $gid_num ($gname) [${gstatus}]\\n"
fi
done
msg+="\\n$(translate "Choose action:")"
local choice
choice=$(dialog --backtitle "ProxMenux" \
--title "$(translate "Disk Reference Conflict")" \
--menu "$msg" 22 84 3 \
"1" "$(translate "Disable onboot on affected VM(s)/CT(s)")" \
"2" "$(translate "Remove disk references from affected VM(s)/CT(s) config")" \
"3" "$(translate "Skip — leave as-is")" \
2>&1 >/dev/tty) || { echo "skip"; return; }
case "$choice" in
1) echo "disable_onboot" ;;
2) echo "remove_refs" ;;
*) echo "skip" ;;
esac
}
confirm_summary() {
# ── Risk detection ─────────────────────────────────────────────────────────
local reinforce_limited_firmware="no"
local bios_date bios_year current_year bios_age cpu_model risk_detail=""
bios_date=$(cat /sys/class/dmi/id/bios_date 2>/dev/null)
bios_year=$(echo "$bios_date" | grep -oE '[0-9]{4}' | tail -n1)
current_year=$(date +%Y 2>/dev/null)
if [[ -n "$bios_year" && -n "$current_year" ]]; then
bios_age=$(( current_year - bios_year ))
if (( bios_age >= 7 )); then
reinforce_limited_firmware="yes"
risk_detail="$(translate "BIOS from") ${bios_year} (${bios_age} $(translate "years old")) — $(translate "older firmware may increase passthrough instability")"
fi
fi
cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | xargs)
if echo "$cpu_model" | grep -qiE 'J4[0-9]{3}|J3[0-9]{3}|N4[0-9]{3}|N3[0-9]{3}|Apollo Lake'; then
reinforce_limited_firmware="yes"
[[ -z "$risk_detail" ]] && risk_detail="$(translate "Low-power CPU platform"): ${cpu_model}"
fi
# ── Build unified message ──────────────────────────────────────────────────
local msg pci display_name
msg="\n"
# Devices to add
msg+="\Zb$(translate "Devices to add to VM") ${SELECTED_VMID} (${SELECTED_VM_NAME}):\Zn\n"
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
display_name=$(_pci_storage_display_name "$pci")
msg+=" \Zb•\Zn ${pci} ${display_name}\n"
done
msg+="\n"
# Compatibility notice (always shown)
msg+="\Zb\Z4⚠ $(translate "Controller/NVMe passthrough — compatibility notice")\Zn\n\n"
msg+="$(translate "Not all platforms support Controller/NVMe passthrough reliably.")\n"
msg+="$(translate "On some systems, when starting the VM the host may slow down for several minutes until it stabilizes, or freeze completely.")\n"
# Detected risk (only when applicable)
if [[ "$reinforce_limited_firmware" == "yes" && -n "$risk_detail" ]]; then
msg+="\n\Z1$(translate "Detected risk factor"): ${risk_detail}\Zn\n"
fi
msg+="\n$(translate "If the host freezes, remove hostpci entries from") /etc/pve/qemu-server/${SELECTED_VMID}.conf\n"
msg+="\n\Zb$(translate "Do you want to continue?")\Zn"
local height=22
[[ "$reinforce_limited_firmware" == "yes" ]] && height=25
if ! dialog --backtitle "ProxMenux" --colors \
--title "$(translate "Confirm Controller + NVMe Assignment")" \
--yesno "$msg" 18 90
[[ $? -ne 0 ]] && return 1
--yesno "$msg" $height 90; then
return 1
fi
return 0
}
@@ -349,7 +466,7 @@ prompt_controller_conflict_policy() {
shift
local -a source_vms=("$@")
local msg vmid vm_name st ob
msg="$(translate "Selected device is already assigned to other VM(s):")\n\n"
msg="\n$(translate "Selected device is already assigned to other VM(s):")\n\n"
for vmid in "${source_vms[@]}"; do
vm_name=$(_vm_name_by_id "$vmid")
st="stopped"; _vm_status_is_running "$vmid" && st="running"
@@ -359,11 +476,13 @@ prompt_controller_conflict_policy() {
msg+="\n$(translate "Choose action for this controller/NVMe:")"
local choice
choice=$(whiptail --title "$(translate "Controller/NVMe Conflict Policy")" --menu "$msg" 22 96 10 \
choice=$(dialog --backtitle "ProxMenux" \
--title "$(translate "Controller/NVMe Conflict Policy")" \
--menu "$msg" 20 80 10 \
"1" "$(translate "Keep in source VM(s) + disable onboot + add to target VM")" \
"2" "$(translate "Move to target VM (remove from source VM config)")" \
"3" "$(translate "Skip this device")" \
3>&1 1>&2 2>&3) || { echo "skip"; return; }
2>&1 >/dev/tty) || { echo "skip"; return; }
case "$choice" in
1) echo "keep_disable_onboot" ;;
@@ -372,17 +491,143 @@ prompt_controller_conflict_policy() {
esac
}
# ── DIALOG PHASE: resolve all conflicts before terminal ───────────────────────
resolve_disk_conflicts() {
local -a new_pci_list=()
local pci vmid action slot_base scope_key has_running
# ── hostpci conflicts: controller already assigned to another VM ──────────
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
local -a source_vms=()
mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null)
if [[ ${#source_vms[@]} -eq 0 ]]; then
new_pci_list+=("$pci")
continue
fi
has_running=false
for vmid in "${source_vms[@]}"; do
if _vm_status_is_running "$vmid"; then
has_running=true
dialog --backtitle "ProxMenux" \
--title "$(translate "Device In Use")" \
--msgbox "\n$(translate "Controller") $pci $(translate "is in use by running VM") $vmid.\n\n$(translate "Stop it first and run this option again.")" \
10 72
break
fi
done
$has_running && continue
scope_key=$(printf '%s,' "${source_vms[@]}")
if [[ -n "$WIZARD_CONFLICT_POLICY" && "$WIZARD_CONFLICT_SCOPE" == "$scope_key" ]]; then
action="$WIZARD_CONFLICT_POLICY"
else
action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}")
WIZARD_CONFLICT_POLICY="$action"
WIZARD_CONFLICT_SCOPE="$scope_key"
fi
case "$action" in
keep_disable_onboot)
for vmid in "${source_vms[@]}"; do
_vm_onboot_is_enabled "$vmid" && qm set "$vmid" -onboot 0 >/dev/null 2>&1
done
NEED_HOOK_SYNC=true
new_pci_list+=("$pci")
;;
move_remove_source)
slot_base=$(_pci_slot_base "$pci")
for vmid in "${source_vms[@]}"; do
_remove_pci_slot_from_vm_config "$vmid" "$slot_base"
done
new_pci_list+=("$pci")
;;
*) ;; # skip — do not add to new_pci_list
esac
done
SELECTED_CONTROLLER_PCIS=("${new_pci_list[@]}")
if [[ ${#SELECTED_CONTROLLER_PCIS[@]} -eq 0 ]]; then
dialog --backtitle "ProxMenux" \
--title "$(translate "Controller + NVMe")" \
--msgbox "\n$(translate "No controllers remaining after conflict resolution.")" 8 64
return 1
fi
# ── Raw disk passthrough conflicts ───────────────────────────────────────
local raw_disk_policy="" raw_disk_scope=""
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
local -a cdisks=()
while IFS= read -r disk; do
[[ -z "$disk" ]] && continue
_array_contains "$disk" "${cdisks[@]}" || cdisks+=("$disk")
done < <(_controller_block_devices "$pci")
for disk in "${cdisks[@]}"; do
_disk_used_in_guest_configs "$disk" || continue
_disk_used_in_running_guest "$disk" && continue
local -a guest_ids=()
mapfile -t guest_ids < <(_disk_guest_ids "$disk")
[[ ${#guest_ids[@]} -eq 0 ]] && continue
local gscope gaction
gscope=$(printf '%s,' "${guest_ids[@]}")
if [[ -n "$raw_disk_policy" && "$raw_disk_scope" == "$gscope" ]]; then
gaction="$raw_disk_policy"
else
gaction=$(_prompt_raw_disk_conflict_policy "$disk" "${guest_ids[@]}")
raw_disk_policy="$gaction"
raw_disk_scope="$gscope"
fi
local gid gtype gid_num slot
case "$gaction" in
disable_onboot)
for gid in "${guest_ids[@]}"; do
gtype="${gid%%:*}"; gid_num="${gid##*:}"
if [[ "$gtype" == "VM" ]]; then
_vm_onboot_is_enabled "$gid_num" && qm set "$gid_num" -onboot 0 >/dev/null 2>&1
else
grep -qE '^onboot:\s*1' "/etc/pve/lxc/$gid_num.conf" 2>/dev/null && \
pct set "$gid_num" -onboot 0 >/dev/null 2>&1
fi
done
;;
remove_refs)
for gid in "${guest_ids[@]}"; do
gtype="${gid%%:*}"; gid_num="${gid##*:}"
if [[ "$gtype" == "VM" ]]; then
while IFS= read -r slot; do
[[ -z "$slot" ]] && continue
qm set "$gid_num" -delete "$slot" >/dev/null 2>&1
done < <(_find_disk_slots_in_vm "$gid_num" "$disk")
else
while IFS= read -r slot; do
[[ -z "$slot" ]] && continue
pct set "$gid_num" -delete "$slot" >/dev/null 2>&1
done < <(_find_disk_slots_in_ct "$gid_num" "$disk")
fi
done
;;
esac
done
done
return 0
}
apply_assignment() {
: >"$LOG_FILE"
set_title
echo
msg_info "$(translate "Applying Controller/NVMe passthrough to VM") ${SELECTED_VMID}..."
msg_ok "$(translate "Target VM validated") (${SELECTED_VM_NAME} / ${SELECTED_VMID})"
msg_ok "$(translate "Selected devices"): ${#SELECTED_CONTROLLER_PCIS[@]}"
local hostpci_idx=0
msg_info "$(translate "Calculating next available hostpci slot...")"
if declare -F _pci_next_hostpci_index >/dev/null 2>&1; then
hostpci_idx=$(_pci_next_hostpci_index "$SELECTED_VMID" 2>/dev/null || echo 0)
else
@@ -392,10 +637,8 @@ apply_assignment() {
hostpci_idx=$((hostpci_idx + 1))
done
fi
msg_ok "$(translate "Next available hostpci slot"): hostpci${hostpci_idx}"
local pci bdf assigned_count=0
local need_hook_sync=false
for pci in "${SELECTED_CONTROLLER_PCIS[@]}"; do
bdf="${pci#0000:}"
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
@@ -408,50 +651,11 @@ apply_assignment() {
continue
fi
local -a source_vms=()
mapfile -t source_vms < <(_pci_assigned_vm_ids "$pci" "$SELECTED_VMID" 2>/dev/null)
if [[ ${#source_vms[@]} -gt 0 ]]; then
local has_running=false vmid action slot_base
for vmid in "${source_vms[@]}"; do
if _vm_status_is_running "$vmid"; then
has_running=true
msg_warn "$(translate "Controller/NVMe is in use by running VM") ${vmid} ($(translate "stop source VM first"))"
fi
done
if $has_running; then
continue
fi
action=$(prompt_controller_conflict_policy "$pci" "${source_vms[@]}")
case "$action" in
keep_disable_onboot)
for vmid in "${source_vms[@]}"; do
if _vm_onboot_is_enabled "$vmid"; then
if qm set "$vmid" -onboot 0 >>"$LOG_FILE" 2>&1; then
msg_warn "$(translate "Start on boot disabled for VM") ${vmid}"
fi
fi
done
need_hook_sync=true
;;
move_remove_source)
slot_base=$(_pci_slot_base "$pci")
for vmid in "${source_vms[@]}"; do
if _remove_pci_slot_from_vm_config "$vmid" "$slot_base"; then
msg_ok "$(translate "Controller/NVMe removed from source VM") ${vmid} (${pci})"
fi
done
;;
*)
msg_info2 "$(translate "Skipped device"): ${pci}"
continue
;;
esac
fi
if qm set "$SELECTED_VMID" --hostpci${hostpci_idx} "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx} -> ${pci})"
local display_name
display_name=$(_pci_storage_display_name "$pci")
msg_info "$(translate "Adding") ${display_name} (${pci}) → hostpci${hostpci_idx}..."
if qm set "$SELECTED_VMID" "--hostpci${hostpci_idx}" "${pci},pcie=1" >>"$LOG_FILE" 2>&1; then
msg_ok "$(translate "Controller/NVMe assigned") (hostpci${hostpci_idx}${pci})"
assigned_count=$((assigned_count + 1))
hostpci_idx=$((hostpci_idx + 1))
else
@@ -459,33 +663,50 @@ apply_assignment() {
fi
done
if $need_hook_sync && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
if $NEED_HOOK_SYNC && declare -F sync_proxmenux_gpu_guard_hooks >/dev/null 2>&1; then
ensure_proxmenux_gpu_guard_hookscript
sync_proxmenux_gpu_guard_hooks
msg_ok "$(translate "VM hook guard synced for shared controller/NVMe protection")"
fi
echo
echo ""
echo -e "${TAB}${BL}Log: ${LOG_FILE}${CL}"
if [[ "$assigned_count" -gt 0 ]]; then
msg_success "$(translate "Completed. Controller/NVMe passthrough configured for VM") ${SELECTED_VMID}."
if [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
msg_warn "$(translate "IOMMU was configured during this run. Reboot the host before starting the VM.")"
fi
msg_ok "$(translate "Completed.") $assigned_count $(translate "device(s) added to VM") ${SELECTED_VMID}."
else
msg_warn "$(translate "No new Controller/NVMe entries were added.")"
fi
if [[ "${IOMMU_ALREADY_ACTIVE:-0}" == "1" ]]; then
msg_ok "$(translate "IOMMU is enabled on the system")"
elif [[ "${IOMMU_PENDING_REBOOT:-0}" == "1" ]]; then
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
echo ""
if whiptail --title "$(translate "Reboot Required")" \
--yesno "\n$(translate "IOMMU has been enabled on this system. A reboot is required to apply the changes. Reboot now?")" 11 64; then
msg_success "$(translate "Press Enter to continue...")"
read -r
msg_warn "$(translate "Rebooting the system...")"
reboot
else
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
fi
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
main() {
select_target_vm || exit 0
export WIZARD_CONFLICT_POLICY
export WIZARD_CONFLICT_SCOPE
select_target_vm || exit 0
validate_vm_requirements || exit 0
select_controller_nvme || exit 0
confirm_summary || exit 0
clear
select_controller_nvme || exit 0
resolve_disk_conflicts || exit 0
confirm_summary || exit 0
apply_assignment
}

View File

@@ -6,8 +6,8 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Version : 1.2
# Last Updated: 12/04/2026
# ==========================================================
# Description:
# This script allows users to assign physical disks to existing
@@ -20,6 +20,7 @@
# - 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.
# - Prefers persistent /dev/disk/by-id paths for assignment when available.
#
# The goal of this script is to simplify the process of assigning
# physical disks to Proxmox VMs, reducing manual configurations
@@ -28,134 +29,181 @@
# Configuration ============================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
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
# shellcheck source=/dev/null
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# 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
load_language
initialize_cache
# ==========================================================
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
# ==========================================================
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
echo "$MODEL" "$SIZE"
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"
}
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 ──────────────────────────────────────────────────────────────
VM_LIST=$(qm list | awk 'NR>1 {print $1, $2}')
if [ -z "$VM_LIST" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No VMs available in the system.")" 8 40
dialog --backtitle "$BACKTITLE" \
--title "$(translate "Error")" \
--msgbox "$(translate "No VMs available in the system.")" $UI_MSG_H $UI_MSG_W
exit 1
fi
VMID=$(whiptail --title "$(translate "Select VM")" --menu "$(translate "Select the VM to which you want to add disks:")" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
# 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)
if [ -z "$VMID" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No VM was selected.")" 8 40
dialog --backtitle "$BACKTITLE" \
--title "$(translate "Error")" \
--msgbox "$(translate "No VM was selected.")" $UI_MSG_H $UI_MSG_W
exit 1
fi
VMID=$(echo "$VMID" | tr -d '"')
clear
show_proxmenux_logo
echo -e
msg_title "$(translate "Import Disk to VM")"
echo -e
msg_ok "$(translate "VM selected successfully.")"
VM_STATUS=$(qm status "$VMID" | awk '{print $2}')
if [ "$VM_STATUS" == "running" ]; then
whiptail --title "$(translate "Warning")" --msgbox "$(translate "The VM is powered on. Turn it off before adding disks.")" 12 60
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
exit 1
fi
##########################################
# ── TERMINAL PHASE 1: detect disks ────────────────────────────────────────────
show_proxmenux_logo
msg_title "$(translate "Import Disk to VM")"
msg_ok "$(translate "VM $VMID selected successfully.")"
msg_info "$(translate "Detecting available disks...")"
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
ZFS_DISKS=""
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
for entry in $ZFS_RAW; do
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
if [ -e "/dev/disk/by-id/$entry" ]; then
path=$(readlink -f "/dev/disk/by-id/$entry")
fi
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
ZFS_DISKS+="/dev/$base_disk"$'\n'
fi
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
is_disk_in_use() {
local disk="$1"
while read -r part fstype; do
case "$fstype" in
zfs_member|linux_raid_member)
return 0 ;;
esac
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
return 0
fi
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
return 0
fi
return 1
}
_refresh_host_storage_cache
VM_CONFIG=$(qm config "$VMID" 2>/dev/null | grep -vE '^\s*#|^description:')
FREE_DISKS=()
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
IFS=$'\t' read -r MODEL SIZE < <(get_disk_info "$DISK")
LABEL=""
SHOW_DISK=true
IS_MOUNTED=false
IS_RAID=false
IS_ZFS=false
@@ -175,46 +223,34 @@ while read -r DISK; do
IS_MOUNTED=true
fi
USED_BY=""
REAL_PATH=$(readlink -f "$DISK")
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
if _disk_used_in_guest_configs "$DISK"; then
USED_BY="$(translate "In use")"
else
for SYMLINK in /dev/disk/by-id/*; do
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
break
fi
fi
done
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
# 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
if $IS_MOUNTED; then
SHOW_DISK=false
fi
if qm config "$VMID" | grep -vE '^\s*#|^description:' | grep -q "$DISK"; then
if disk_referenced_in_config "$VM_CONFIG" "$DISK"; then
SHOW_DISK=false
fi
@@ -229,144 +265,188 @@ while read -r DISK; do
fi
done < <(lsblk -dn -e 7,11 -o PATH)
if [ "${#FREE_DISKS[@]}" -eq 0 ]; then
cleanup
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks available for this VM.")" 8 40
clear
stop_spinner
dialog --backtitle "$BACKTITLE" \
--title "$(translate "Error")" \
--msgbox "$(translate "No disks available for this VM.")" $UI_MSG_H $UI_MSG_W
exit 1
fi
stop_spinner
msg_ok "$(translate "Available disks detected.")"
######################################################
# ── DIALOG PHASE: select disks + interface ────────────────────────────────────
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
TOTAL_WIDTH=$((MAX_WIDTH + 20))
if [ $TOTAL_WIDTH -lt 50 ]; then
TOTAL_WIDTH=50
if [ $TOTAL_WIDTH -lt $UI_MENU_W ]; then
TOTAL_WIDTH=$UI_MENU_W
fi
if [ $TOTAL_WIDTH -gt 116 ]; then
TOTAL_WIDTH=116
fi
SELECTED=$(whiptail --title "$(translate "Select Disks")" --checklist \
"$(translate "Select the disks you want to add:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3)
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)
if [ -z "$SELECTED" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disks were selected.")" 10 64
clear
dialog --backtitle "$BACKTITLE" \
--title "$(translate "Error")" \
--msgbox "$(translate "No disks were selected.")" $UI_MSG_H $UI_MSG_W
exit 1
fi
msg_ok "$(translate "Disks selected successfully.")"
INTERFACE=$(whiptail --title "$(translate "Interface Type")" --menu "$(translate "Select the interface type for all disks:")" 15 40 4 \
"sata" "$(translate "Add as SATA")" \
"scsi" "$(translate "Add as SCSI")" \
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")" \
"virtio" "$(translate "Add as VirtIO")" \
"ide" "$(translate "Add as IDE")" 3>&1 1>&2 2>&3)
"ide" "$(translate "Add as IDE")" \
2>&1 >/dev/tty)
if [ -z "$INTERFACE" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No interface type was selected for the disks.")" 8 40
clear
dialog --backtitle "$BACKTITLE" \
--title "$(translate "Error")" \
--msgbox "$(translate "No interface type was selected for the disks.")" $UI_MSG_H $UI_MSG_W
exit 1
fi
msg_ok "$(translate "Interface type selected: $INTERFACE")"
DISKS_ADDED=0
ERROR_MESSAGES=""
SUCCESS_MESSAGES=""
msg_info "$(translate "Processing selected disks...")"
# ── DIALOG PHASE: per-disk pre-check ──────────────────────────────────────────
declare -a DISK_LIST=()
declare -a DISK_DESCRIPTIONS=()
declare -a DISK_ASSIGNED_TOS=()
declare -a NVME_SKIPPED=()
for DISK in $SELECTED; do
DISK=$(echo "$DISK" | tr -d '"')
DISK="${DISK//\"/}"
DISK_INFO=$(get_disk_info "$DISK")
ASSIGNED_TO=""
RUNNING_VMS=""
RUNNING_CTS=""
while read -r VM_ID VM_NAME; do
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && qm config "$VM_ID" | grep -q "$DISK"; then
VM_CONFIG_RAW=$(qm config "$VM_ID" 2>/dev/null)
if [[ "$VM_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$VM_CONFIG_RAW" "$DISK"; then
ASSIGNED_TO+="VM $VM_ID $VM_NAME\n"
VM_STATUS=$(qm status "$VM_ID" | awk '{print $2}')
if [ "$VM_STATUS" == "running" ]; then
RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
fi
VM_STATUS_CHK=$(qm status "$VM_ID" | awk '{print $2}')
[[ "$VM_STATUS_CHK" == "running" ]] && RUNNING_VMS+="VM $VM_ID $VM_NAME\n"
fi
done < <(qm list | awk 'NR>1 {print $1, $2}')
while read -r CT_ID CT_NAME; do
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && pct config "$CT_ID" | grep -q "$DISK"; then
CT_CONFIG_RAW=$(pct config "$CT_ID" 2>/dev/null)
if [[ "$CT_ID" =~ ^[0-9]+$ ]] && disk_referenced_in_config "$CT_CONFIG_RAW" "$DISK"; then
ASSIGNED_TO+="CT $CT_ID $CT_NAME\n"
CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}')
if [ "$CT_STATUS" == "running" ]; then
RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
fi
CT_STATUS_CHK=$(pct status "$CT_ID" | awk '{print $2}')
[[ "$CT_STATUS_CHK" == "running" ]] && RUNNING_CTS+="CT $CT_ID $CT_NAME\n"
fi
done < <(pct list | awk 'NR>1 {print $1, $2}')
done < <(pct list | awk 'NR>1 {print $1, $3}')
if [ -n "$RUNNING_VMS" ] || [ -n "$RUNNING_CTS" ]; then
ERROR_MESSAGES+="$(translate "The disk") $DISK_INFO $(translate "is currently in use by the following running VM(s) or CT(s):")\\n$RUNNING_VMS$RUNNING_CTS\\n\\n$(translate "You cannot add this disk while the VM or CT is running.")\\n$(translate "Please shut it down first and run this script again to add the disk.")\\n\\n"
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
continue
fi
if [ -n "$ASSIGNED_TO" ]; then
cleanup
whiptail --title "$(translate "Disk Already Assigned")" --yesno "$(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?")" 15 70
if [ $? -ne 0 ]; then
sleep 1
exec "$0"
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
fi
fi
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]}"
INDEX=0
while qm config "$VMID" | grep -q "${INTERFACE}${INDEX}"; do
((INDEX++))
done
RESULT=$(qm set "$VMID" -${INTERFACE}${INDEX} "$DISK" 2>&1)
if [ $? -eq 0 ]; then
MESSAGE="$(translate "The disk") $DISK_INFO $(translate "has been successfully added to VM") $VMID."
if [ -n "$ASSIGNED_TO" ]; then
MESSAGE+="\\n\\n$(translate "WARNING: This disk is also assigned to the following VM(s):")\\n$ASSIGNED_TO"
MESSAGE+="\\n$(translate "Make sure not to start VMs that share this disk at the same time to avoid data corruption.")"
fi
SUCCESS_MESSAGES+="$MESSAGE\\n\\n"
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' ' ')"
((DISKS_ADDED++))
else
ERROR_MESSAGES+="$(translate "Could not add disk") $DISK_INFO $(translate "to VM") $VMID.\\n$(translate "Error:") $RESULT\\n\\n"
msg_error "$(translate "Could not add") $_model $_size: $RESULT"
fi
done
msg_ok "$(translate "Disk processing completed.")"
if [ -n "$SUCCESS_MESSAGES" ]; then
MSG_LINES=$(echo "$SUCCESS_MESSAGES" | wc -l)
whiptail --title "$(translate "Successful Operations")" --msgbox "$SUCCESS_MESSAGES" 16 70
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.")"
fi
if [ -n "$ERROR_MESSAGES" ]; then
whiptail --title "$(translate "Warnings and Errors")" --msgbox "$ERROR_MESSAGES" 16 70
fi
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,183 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Disk and Storage Manager Manual CLI Guide
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : GPL-3.0
# Version : 1.0
# Last Updated: 07/04/2026
# ==========================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR/.." && pwd)"
LOCAL_SCRIPTS_DEFAULT="/usr/local/share/proxmenux/scripts"
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_DEFAULT"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
if [[ -f "$LOCAL_SCRIPTS_LOCAL/utils.sh" ]]; then
LOCAL_SCRIPTS="$LOCAL_SCRIPTS_LOCAL"
UTILS_FILE="$LOCAL_SCRIPTS/utils.sh"
elif [[ ! -f "$UTILS_FILE" ]]; then
UTILS_FILE="$BASE_DIR/utils.sh"
fi
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
GREEN=$'\033[0;32m'
NC=$'\033[0m'
_cl() {
local num="$1" disp="$2" desc="$3"
local pad=$((47 - ${#disp}))
[[ $pad -lt 1 ]] && pad=1
local spaces
spaces=$(printf '%*s' "$pad" '')
printf " %2d) %s%s%s%s - %s\n" "$num" "$GREEN" "$disp" "$NC" "$spaces" "$desc"
}
while true; do
clear
show_proxmenux_logo
msg_title "$(translate "Disk and Storage Manager - Manual CLI Guide")"
echo -e "${TAB}${YW}$(translate 'Inspection commands run directly. Template commands [T] require parameter substitution.')${CL}"
echo
_cl 1 "lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" "$(translate 'Inspect disks before any action')"
_cl 2 "ls -lh /dev/disk/by-id/" "$(translate 'Identify persistent disk paths')"
_cl 3 "qm list && pct list" "$(translate 'List VM/CT IDs to operate on')"
_cl 4 "qm config <vmid> | grep 'sata|scsi|hostpci'" "$(translate 'Check VM disk/PCI slots')"
_cl 5 "pvesm status -content images" "$(translate 'List storages valid for image import')"
_cl 6 "qm importdisk <vmid> <image_path> <storage>" "[T] $(translate 'Import disk image to VM')"
_cl 7 "qm set <vmid> --<iface><slot> <imported-disk>" "[T] $(translate 'Attach imported disk to VM')"
_cl 8 "qm set <vmid> --boot order=<iface><slot>" "[T] $(translate 'Set VM boot order')"
_cl 9 "lspci -nn | grep -Ei 'SATA|RAID|NVMe'" "$(translate 'Detect controller/NVMe BDF')"
_cl 10 "find /sys/kernel/iommu_groups -type l | grep BDF" "$(translate 'Verify IOMMU group for PCI device')"
_cl 11 "qm set <vmid> --hostpci<slot> <BDF>,pcie=1" "[T] $(translate 'Assign controller/NVMe passthrough')"
_cl 12 "pct config <ctid> | grep '^mp'" "$(translate 'Check container mount points')"
_cl 13 "pct set <ctid> -mp<slot> <disk>,mp=<path>" "[T] $(translate 'Add disk to LXC container')"
_cl 14 "wipefs -a -f /dev/sdX && sgdisk --zap-all /dev/sdX" "[T] $(translate 'Clean disk metadata')"
_cl 15 "parted -s /dev/sdX mklabel gpt mkpart primary" "[T] $(translate 'Create GPT partition')"
_cl 16 "mkfs.ext4 -F /dev/sdX1 (or mkfs.xfs / mkfs.btrfs)" "[T] $(translate 'Format filesystem')"
_cl 17 "pvesm status && zpool status" "$(translate 'Final storage health/status check')"
echo -e " ${DEF} 0) $(translate 'Back to previous menu or Esc + Enter')${CL}"
echo
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}"
read -r user_input
if [[ "$user_input" == $'\x1b' ]]; then
break
fi
mode="exec"
case "$user_input" in
1) cmd="lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL" ;;
2) cmd="ls -lh /dev/disk/by-id/" ;;
3) cmd="qm list && pct list" ;;
4)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"
read -r vmid
cmd="qm config $vmid | grep -E '^(sata|scsi|virtio|ide|hostpci|boot:)'"
;;
5) cmd="pvesm status -content images" ;;
6)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter image full path: ')${CL}"; read -r image_path
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target storage: ')${CL}"; read -r storage
cmd="qm importdisk $vmid $image_path $storage"
mode="template"
;;
7)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter interface (sata/scsi/virtio/ide): ')${CL}"; read -r iface
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter slot number (e.g. 0): ')${CL}"; read -r slot
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter imported disk reference (e.g. local-lvm:vm-100-disk-0): ')${CL}"; read -r imported_disk
cmd="qm set $vmid --${iface}${slot} $imported_disk"
mode="template"
;;
8)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter boot target (e.g. scsi0, sata0, ide0): ')${CL}"; read -r boot_target
cmd="qm set $vmid --boot order=$boot_target"
mode="template"
;;
9) cmd="lspci -nn | grep -Ei 'SATA|RAID|Non-Volatile|NVMe'" ;;
10)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:04:00.0): ')${CL}"
read -r bdf
cmd="find /sys/kernel/iommu_groups -type l | grep $bdf"
;;
11)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r vmid
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter hostpci slot number (e.g. 0): ')${CL}"; read -r slot
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter PCI BDF (e.g. 0000:04:00.0): ')${CL}"; read -r bdf
cmd="qm set $vmid --hostpci${slot} ${bdf},pcie=1"
mode="template"
;;
12)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"
read -r ctid
cmd="pct config $ctid | grep '^mp'"
;;
13)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"; read -r ctid
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter mp slot number (e.g. 0): ')${CL}"; read -r mpslot
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter disk or partition path (prefer /dev/disk/by-id/...): ')${CL}"; read -r disk_part
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter mount point in CT (e.g. /mnt/data): ')${CL}"; read -r mount_point
cmd="pct set $ctid -mp${mpslot} ${disk_part},mp=${mount_point},backup=0,ro=0"
mode="template"
;;
14)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target disk (e.g. /dev/sdX): ')${CL}"
read -r disk
cmd="wipefs -a -f $disk && sgdisk --zap-all $disk"
mode="template"
;;
15)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter target disk (e.g. /dev/sdX): ')${CL}"
read -r disk
cmd="parted -s -f $disk mklabel gpt mkpart primary 1MiB 100%"
mode="template"
;;
16)
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter partition path (e.g. /dev/sdX1): ')${CL}"; read -r part
echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter filesystem (ext4/xfs/btrfs): ')${CL}"; read -r fs
case "$fs" in
ext4) cmd="mkfs.ext4 -F $part" ;;
xfs) cmd="mkfs.xfs -f $part" ;;
btrfs) cmd="mkfs.btrfs -f $part" ;;
*) cmd="mkfs.ext4 -F $part" ;;
esac
mode="template"
;;
17) cmd="pvesm status && zpool status" ;;
0) break ;;
*)
if [[ -n "$user_input" ]]; then
cmd="$user_input"
else
continue
fi
;;
esac
if [[ "$mode" == "template" ]]; then
echo -e "\n${GREEN}$(translate 'Manual command template (copy/paste):')${NC}\n"
echo "$cmd"
echo
msg_success "$(translate 'Press ENTER to continue...')"
read -r tmp
continue
fi
echo -e "\n${GREEN}> $cmd${NC}\n"
bash -c "$cmd"
echo
msg_success "$(translate 'Press ENTER to continue...')"
read -r tmp
done

File diff suppressed because it is too large Load Diff

View File

@@ -6,24 +6,14 @@
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.1
# Last Updated: 29/05/2025
# Version : 1.3
# Last Updated: 12/04/2026
# ==========================================================
# Description:
# This script automates the process of importing disk images into Proxmox VE virtual machines (VMs),
# making it easy to attach pre-existing disk files without manual configuration.
#
# Before running the script, ensure that disk images are available in /var/lib/vz/template/images/.
# The script scans this directory for compatible formats (.img, .qcow2, .vmdk, .raw) and lists the available files.
#
# Using an interactive menu, you can:
# - Select a VM to attach the imported disk.
# - Choose one or multiple disk images for import.
# - Pick a storage volume in Proxmox for disk placement.
# - Assign a suitable interface (SATA, SCSI, VirtIO, or IDE).
# - Enable optional settings like SSD emulation or bootable disk configuration.
#
# Once completed, the script ensures the selected images are correctly attached and ready to use.
# Imports disk images (.img, .qcow2, .vmdk, .raw) into Proxmox VE VMs.
# Supports the default system ISO directory and custom paths.
# All user decisions are collected in Phase 1 (dialogs) before
# any operation is executed in Phase 2 (terminal output).
# ==========================================================
# Configuration ============================================
@@ -31,256 +21,340 @@ LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
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=10
UI_YESNO_W=72
UI_RESULT_H=14
UI_RESULT_W=86
# shellcheck source=/dev/null
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
load_language
initialize_cache
# Configuration ============================================
detect_image_dir() {
for store in $(pvesm status -content images | awk 'NR>1 {print $1}'); do
_get_default_images_dir() {
for dir in /var/lib/vz/template/iso /var/lib/vz/template/images; do
[[ -d "$dir" ]] && echo "$dir" && return 0
done
local store path
for store in $(pvesm status -content images 2>/dev/null | awk 'NR>1 {print $1}'); do
path=$(pvesm path "${store}:template" 2>/dev/null)
if [[ -d "$path" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$path/*.$ext" > /dev/null; then
echo "$path"
return 0
fi
done
for sub in images iso; do
dir="$path/$sub"
if [[ -d "$dir" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$dir/*.$ext" > /dev/null; then
echo "$dir"
return 0
fi
done
fi
done
fi
[[ -d "$path" ]] && echo "$path" && return 0
done
for fallback in /var/lib/vz/template/images /var/lib/vz/template/iso; do
if [[ -d "$fallback" ]]; then
for ext in raw img qcow2 vmdk; do
if compgen -G "$fallback/*.$ext" > /dev/null; then
echo "$fallback"
return 0
fi
done
fi
done
return 1
echo "/var/lib/vz/template/iso"
}
IMAGES_DIR=$(detect_image_dir)
if [[ -z "$IMAGES_DIR" ]]; then
dialog --title "$(translate 'No Images Found')" \
--msgbox "$(translate 'Could not find any directory containing disk images')\n\n$(translate 'Make sure there is at least one file with extension .img, .qcow2, .vmdk or .raw')" 15 60
# ==========================================================
# PHASE 1 — SELECTION
# All dialogs run here. No execution, no show_proxmenux_logo.
# ==========================================================
# ── Step 1: Select VM ─────────────────────────────────────
VM_OPTIONS=()
while read -r vmid vmname _rest; do
VM_OPTIONS+=("$vmid" "${vmname:-VM-$vmid}")
done < <(qm list 2>/dev/null | awk 'NR>1')
stop_spinner
if [[ ${#VM_OPTIONS[@]} -eq 0 ]]; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate 'No VMs Found')" \
--msgbox "\n$(translate 'No VMs available in the system.')" \
$UI_MSG_H $UI_MSG_W
exit 1
fi
IMAGES=$(ls -A "$IMAGES_DIR" | grep -E "\.(img|qcow2|vmdk|raw)$")
if [ -z "$IMAGES" ]; then
dialog --title "$(translate 'No Disk Images Found')" \
--msgbox "$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" 15 60
exit 1
fi
# 1. Select VM
msg_info "$(translate 'Getting VM list')"
VM_LIST=$(qm list | awk 'NR>1 {print $1" "$2}')
if [ -z "$VM_LIST" ]; then
msg_error "$(translate 'No VMs available in the system')"
exit 1
fi
msg_ok "$(translate 'VM list obtained')"
VMID=$(whiptail --title "$(translate 'Select VM')" --menu "$(translate 'Select the VM where you want to import the disk image:')" 15 60 8 $VM_LIST 3>&1 1>&2 2>&3)
if [ -z "$VMID" ]; then
exit 1
fi
# 2. Select storage volume
msg_info "$(translate 'Getting storage volumes')"
STORAGE_LIST=$(pvesm status -content images | awk 'NR>1 {print $1}')
if [ -z "$STORAGE_LIST" ]; then
msg_error "$(translate 'No storage volumes available')"
exit 1
fi
msg_ok "$(translate 'Storage volumes obtained')"
VMID=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Select VM')" \
--menu "$(translate 'Select the VM where you want to import the disk image:')" \
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
"${VM_OPTIONS[@]}" \
2>&1 >/dev/tty)
[[ -z "$VMID" ]] && exit 0
# ── Step 2: Select storage ────────────────────────────────
STORAGE_OPTIONS=()
while read -r storage; do
STORAGE_OPTIONS+=("$storage" "")
done <<< "$STORAGE_LIST"
while read -r storage type _rest; do
STORAGE_OPTIONS+=("$storage" "$type")
done < <(pvesm status -content images 2>/dev/null | awk 'NR>1')
stop_spinner
STORAGE=$(whiptail --title "$(translate 'Select Storage')" --menu "$(translate 'Select the storage volume for disk import:')" 15 60 8 "${STORAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [[ ${#STORAGE_OPTIONS[@]} -eq 0 ]]; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate 'No Storage Found')" \
--msgbox "\n$(translate 'No storage volumes available for disk images.')" \
$UI_MSG_H $UI_MSG_W
exit 1
fi
if [ -z "$STORAGE" ]; then
exit 1
if [[ ${#STORAGE_OPTIONS[@]} -eq 2 ]]; then
# Only one storage available — auto-select it
STORAGE="${STORAGE_OPTIONS[0]}"
else
STORAGE=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Select Storage')" \
--menu "$(translate 'Select the storage volume for disk import:')" \
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
"${STORAGE_OPTIONS[@]}" \
2>&1 >/dev/tty)
[[ -z "$STORAGE" ]] && exit 0
fi
# ── Step 3: Select image source directory ────────────────
ISO_DIR="/var/lib/vz/template/iso"
# 3. Select disk images
msg_info "$(translate 'Scanning disk images')"
if [ -z "$IMAGES" ]; then
msg_warn "$(translate 'No compatible disk images found in') $IMAGES_DIR"
exit 0
DIR_CHOICE=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Image Source Directory')" \
--menu "$(translate 'Select the directory containing disk images:')" \
$UI_SHORT_MENU_H $UI_MENU_W $UI_SHORT_MENU_LIST_H \
"$ISO_DIR" "$(translate 'Default ISO directory')" \
"custom" "$(translate 'Custom path...')" \
2>&1 >/dev/tty)
[[ -z "$DIR_CHOICE" ]] && exit 0
if [[ "$DIR_CHOICE" == "custom" ]]; then
IMAGES_DIR=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Custom Directory')" \
--inputbox "\n$(translate 'Enter the full path to the directory containing disk images:')\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" \
10 $UI_RESULT_W "" \
2>&1 >/dev/tty)
[[ -z "$IMAGES_DIR" ]] && exit 0
else
IMAGES_DIR="$ISO_DIR"
fi
msg_ok "$(translate 'Disk images found')"
if [[ ! -d "$IMAGES_DIR" ]]; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Directory Not Found')" \
--msgbox "\n$(translate 'The specified directory does not exist:')\n\n$IMAGES_DIR" \
$UI_MSG_H $UI_MSG_W
exit 1
fi
IMAGES=$(find "$IMAGES_DIR" -maxdepth 1 -type f \
\( -name "*.img" -o -name "*.qcow2" -o -name "*.vmdk" -o -name "*.raw" \) \
-printf '%f\n' 2>/dev/null | sort)
if [[ -z "$IMAGES" ]]; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate 'No Disk Images Found')" \
--msgbox "\n$(translate 'No compatible disk images found in:')\n\n$IMAGES_DIR\n\n$(translate 'Supported formats: .img, .qcow2, .vmdk, .raw')" \
$UI_RESULT_H $UI_RESULT_W
exit 1
fi
# ── Step 4: Select images ─────────────────────────────────
IMAGE_OPTIONS=()
while read -r img; do
IMAGE_OPTIONS+=("$img" "" "OFF")
while IFS= read -r img; do
IMAGE_OPTIONS+=("$img" "" "OFF")
done <<< "$IMAGES"
SELECTED_IMAGES=$(whiptail --title "$(translate 'Select Disk Images')" --checklist "$(translate 'Select the disk images to import:')" 20 60 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
SELECTED_IMAGES_STR=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Select Disk Images')" \
--checklist "$(translate 'Select one or more disk images to import:')" \
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
"${IMAGE_OPTIONS[@]}" \
2>&1 >/dev/tty)
[[ -z "$SELECTED_IMAGES_STR" ]] && exit 0
if [ -z "$SELECTED_IMAGES" ]; then
exit 1
eval "declare -a SELECTED_ARRAY=($SELECTED_IMAGES_STR)"
# ── Step 5: Per-image options ─────────────────────────────
declare -a IMG_NAMES=()
declare -a IMG_INTERFACES=()
declare -a IMG_SSD_OPTIONS=()
declare -a IMG_BOOTABLE=()
for IMAGE in "${SELECTED_ARRAY[@]}"; do
IMAGE="${IMAGE//\"/}"
INTERFACE=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Interface Type')$IMAGE" \
--default-item "scsi" \
--menu "$(translate 'Select the interface type for:') $IMAGE" \
$UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
"scsi" "SCSI $(translate '(recommended)')" \
"virtio" "VirtIO" \
"sata" "SATA" \
"ide" "IDE" \
2>&1 >/dev/tty)
[[ -z "$INTERFACE" ]] && continue
SSD_OPTION=""
if [[ "$INTERFACE" != "virtio" ]]; then
if dialog --backtitle "$BACKTITLE" \
--title "$(translate 'SSD Emulation')$IMAGE" \
--yesno "\n$(translate 'Enable SSD emulation for this disk?')" \
$UI_YESNO_H $UI_YESNO_W; then
SSD_OPTION=",ssd=1"
fi
fi
BOOTABLE="no"
if dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Boot Disk')$IMAGE" \
--yesno "\n$(translate 'Set this disk as the primary boot disk?')" \
$UI_YESNO_H $UI_YESNO_W; then
BOOTABLE="yes"
fi
IMG_NAMES+=("$IMAGE")
IMG_INTERFACES+=("$INTERFACE")
IMG_SSD_OPTIONS+=("$SSD_OPTION")
IMG_BOOTABLE+=("$BOOTABLE")
done
if [[ ${#IMG_NAMES[@]} -eq 0 ]]; then
exit 0
fi
# ==========================================================
# PHASE 2 — EXECUTION
# show_proxmenux_logo appears here exactly once.
# No dialogs from this point on.
# ==========================================================
# 4. Import each selected image
for IMAGE in $SELECTED_IMAGES; do
show_proxmenux_logo
msg_title "$(translate 'Import Disk Image to VM')"
VM_NAME=$(qm config "$VMID" 2>/dev/null | awk '/^name:/ {print $2}')
msg_ok "$(translate 'VM:') ${VM_NAME:-VM-$VMID} (${VMID})"
msg_ok "$(translate 'Storage:') $STORAGE"
msg_ok "$(translate 'Image directory:') $IMAGES_DIR"
msg_ok "$(translate 'Images to import:') ${#IMG_NAMES[@]}"
echo ""
IMAGE=$(echo "$IMAGE" | tr -d '"')
PROCESSED=0
FAILED=0
for i in "${!IMG_NAMES[@]}"; do
IMAGE="${IMG_NAMES[$i]}"
INTERFACE="${IMG_INTERFACES[$i]}"
SSD_OPTION="${IMG_SSD_OPTIONS[$i]}"
BOOTABLE="${IMG_BOOTABLE[$i]}"
FULL_PATH="$IMAGES_DIR/$IMAGE"
INTERFACE=$(whiptail --title "$(translate 'Interface Type')" --menu "$(translate 'Select the interface type for the image:') $IMAGE" 15 40 4 \
"sata" "SATA" \
"scsi" "SCSI" \
"virtio" "VirtIO" \
"ide" "IDE" 3>&1 1>&2 2>&3)
if [[ ! -f "$FULL_PATH" ]]; then
msg_error "$(translate 'Image file not found:') $FULL_PATH"
FAILED=$((FAILED + 1))
continue
fi
if [ -z "$INTERFACE" ]; then
msg_error "$(translate 'No interface type selected for') $IMAGE"
continue
# Snapshot of unused entries before import for reliable detection
BEFORE_UNUSED=$(qm config "$VMID" 2>/dev/null | grep -E '^unused[0-9]+:' || true)
TEMP_STATUS_FILE=$(mktemp)
TEMP_DISK_FILE=$(mktemp)
msg_info "$(translate 'Importing') $IMAGE..."
(
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1
echo $? > "$TEMP_STATUS_FILE"
) | while IFS= read -r line; do
if [[ "$line" =~ [0-9]+\.[0-9]+% ]]; then
echo -ne "\r${TAB}${BL}$(translate 'Importing') ${IMAGE}${CL} ${BASH_REMATCH[0]} "
fi
if echo "$line" | grep -qiF "successfully imported disk"; then
echo "$line" | sed -n "s/.*successfully imported disk as '\\([^']*\\)'.*/\\1/p" > "$TEMP_DISK_FILE"
fi
done
echo -ne "\n"
IMPORT_STATUS=$(cat "$TEMP_STATUS_FILE" 2>/dev/null)
rm -f "$TEMP_STATUS_FILE"
[[ -z "$IMPORT_STATUS" ]] && IMPORT_STATUS=1
if [[ "$IMPORT_STATUS" -ne 0 ]]; then
msg_error "$(translate 'Failed to import') $IMAGE"
rm -f "$TEMP_DISK_FILE"
FAILED=$((FAILED + 1))
continue
fi
msg_ok "$(translate 'Image imported:') $IMAGE"
# Primary: parse disk name from qm importdisk output
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE" 2>/dev/null | xargs)
rm -f "$TEMP_DISK_FILE"
# Fallback: compare unused entries before/after import
if [[ -z "$IMPORTED_DISK" ]]; then
AFTER_UNUSED=$(qm config "$VMID" 2>/dev/null | grep -E '^unused[0-9]+:' || true)
NEW_LINE=$(comm -13 \
<(echo "$BEFORE_UNUSED" | sort) \
<(echo "$AFTER_UNUSED" | sort) | head -1)
if [[ -n "$NEW_LINE" ]]; then
IMPORTED_DISK=$(echo "$NEW_LINE" | cut -d':' -f2- | xargs)
fi
fi
if [[ -z "$IMPORTED_DISK" ]]; then
msg_error "$(translate 'Could not identify the imported disk in VM config')"
FAILED=$((FAILED + 1))
continue
fi
# Find the unusedN key that holds this disk (needed to clean it up after assignment)
IMPORTED_ID=$(qm config "$VMID" 2>/dev/null | grep -F "$IMPORTED_DISK" | cut -d':' -f1 | head -1)
# Find next available slot for the chosen interface
LAST_SLOT=$(qm config "$VMID" 2>/dev/null | grep -oE "^${INTERFACE}[0-9]+" | grep -oE '[0-9]+' | sort -n | tail -1)
if [[ -z "$LAST_SLOT" ]]; then
NEXT_SLOT=0
else
NEXT_SLOT=$((LAST_SLOT + 1))
fi
msg_info "$(translate 'Configuring disk as') ${INTERFACE}${NEXT_SLOT}..."
if qm set "$VMID" "--${INTERFACE}${NEXT_SLOT}" "${IMPORTED_DISK}${SSD_OPTION}" >/dev/null 2>&1; then
msg_ok "$(translate 'Disk configured as') ${INTERFACE}${NEXT_SLOT}${SSD_OPTION:+ (SSD)}"
# Remove the unusedN entry now that the disk is properly assigned
if [[ -n "$IMPORTED_ID" ]]; then
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
fi
FULL_PATH="$IMAGES_DIR/$IMAGE"
msg_info "$(translate 'Importing image:')"
TEMP_DISK_FILE=$(mktemp)
qm importdisk "$VMID" "$FULL_PATH" "$STORAGE" 2>&1 | while read -r line; do
if [[ "$line" =~ transferred ]]; then
PERCENT=$(echo "$line" | grep -oP "\d+\.\d+(?=%)")
echo -ne "\r${TAB}${BL}-$(translate 'Importing image:') $IMAGE-${CL} ${PERCENT}%"
elif [[ "$line" =~ successfully\ imported\ disk ]]; then
echo "$line" | grep -oP "(?<=successfully imported disk ').*(?=')" > "$TEMP_DISK_FILE"
fi
done
echo -ne "\n"
IMPORT_STATUS=${PIPESTATUS[0]}
if [ $IMPORT_STATUS -eq 0 ]; then
msg_ok "$(translate 'Image imported successfully')"
IMPORTED_DISK=$(cat "$TEMP_DISK_FILE")
rm -f "$TEMP_DISK_FILE"
if [ -z "$IMPORTED_DISK" ]; then
STORAGE_TYPE=$(pvesm status -storage "$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_DISK=$(echo "$UNUSED_LINE" | cut -d: -f2- | xargs)
else
IMPORTED_DISK=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f2- | xargs)
IMPORTED_ID=$(qm config "$VMID" | grep -E 'unused[0-9]+' | tail -1 | cut -d: -f1)
fi
fi
if [ -n "$IMPORTED_DISK" ]; then
EXISTING_DISKS=$(qm config "$VMID" | grep -oP "${INTERFACE}\d+" | sort -n)
if [ -z "$EXISTING_DISKS" ]; then
NEXT_SLOT=0
else
LAST_SLOT=$(echo "$EXISTING_DISKS" | tail -n1 | sed "s/${INTERFACE}//")
NEXT_SLOT=$((LAST_SLOT + 1))
fi
if [ "$INTERFACE" != "virtio" ]; then
if (whiptail --title "$(translate 'SSD Emulation')" --yesno "$(translate 'Do you want to use SSD emulation for this disk?')" 10 60); then
SSD_OPTION=",ssd=1"
else
SSD_OPTION=""
fi
else
SSD_OPTION=""
fi
msg_info "$(translate 'Configuring disk')"
if qm set "$VMID" --${INTERFACE}${NEXT_SLOT} "$IMPORTED_DISK${SSD_OPTION}" &>/dev/null; then
msg_ok "$(translate 'Image') $IMAGE $(translate 'configured as') ${INTERFACE}${NEXT_SLOT}"
if [[ -n "$IMPORTED_ID" ]]; then
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
fi
if (whiptail --title "$(translate 'Make Bootable')" --yesno "$(translate 'Do you want to make this disk bootable?')" 10 60); then
msg_info "$(translate 'Configuring disk as bootable')"
if qm set "$VMID" --boot c --bootdisk ${INTERFACE}${NEXT_SLOT} &>/dev/null; then
msg_ok "$(translate 'Disk configured as bootable')"
else
msg_error "$(translate 'Could not configure the disk as bootable')"
fi
fi
else
msg_error "$(translate 'Could not configure disk') ${INTERFACE}${NEXT_SLOT} $(translate 'for VM') $VMID"
echo "DEBUG: Tried to configure: --${INTERFACE}${NEXT_SLOT} \"$IMPORTED_DISK${SSD_OPTION}\""
echo "DEBUG: VM config after import:"
qm config "$VMID" | grep -E "(unused|${INTERFACE})"
fi
else
msg_error "$(translate 'Could not find the imported disk')"
echo "DEBUG: VM config after import:"
qm config "$VMID"
fi
else
msg_error "$(translate 'Could not import') $IMAGE"
if [[ "$BOOTABLE" == "yes" ]]; then
msg_info "$(translate 'Setting boot order...')"
if qm set "$VMID" --boot "order=${INTERFACE}${NEXT_SLOT}" >/dev/null 2>&1; then
msg_ok "$(translate 'Boot order set to') ${INTERFACE}${NEXT_SLOT}"
else
msg_error "$(translate 'Could not set boot order for') ${INTERFACE}${NEXT_SLOT}"
fi
fi
PROCESSED=$((PROCESSED + 1))
else
msg_error "$(translate 'Could not assign disk') ${INTERFACE}${NEXT_SLOT} $(translate 'to VM') $VMID"
FAILED=$((FAILED + 1))
fi
done
echo ""
if [[ $FAILED -eq 0 ]]; then
msg_ok "$(translate 'All images imported and configured successfully')"
elif [[ $PROCESSED -gt 0 ]]; then
msg_warn "$(translate 'Completed with errors —') $(translate 'imported:') $PROCESSED, $(translate 'failed:') $FAILED"
else
msg_error "$(translate 'All imports failed')"
fi
msg_ok "$(translate 'All selected images have been processed')"
msg_success "$(translate "Press Enter to return to menu...")"
msg_success "$(translate 'Press Enter to return to menu...')"
read -r

View File

@@ -1,353 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Mount independent disk on Proxmox host
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 08/04/2025
# ==========================================================
# Description:
# This script detects unassigned physical disks and allows
# the user to mount one of them on the host Proxmox system.
# - Detects unmounted and unassigned disks.
# - Filters out ZFS, LVM, RAID and system disks.
# - Allows selecting a disk.
# - Prepares partition and filesystem if needed.
# - Mounts the disk in the host at a defined mount point.
# ==========================================================
# Configuration ============================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
echo "$MODEL" "$SIZE"
}
msg_info "$(translate "Detecting available disks...")"
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
ZFS_DISKS=""
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
for entry in $ZFS_RAW; do
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
if [ -e "/dev/disk/by-id/$entry" ]; then
path=$(readlink -f "/dev/disk/by-id/$entry")
fi
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
ZFS_DISKS+="/dev/$base_disk"$'\n'
fi
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
is_disk_in_use() {
local disk="$1"
while read -r part fstype; do
case "$fstype" in
zfs_member|linux_raid_member)
return 0 ;;
esac
if echo "$MOUNTED_DISKS" | grep -q "/dev/$part"; then
return 0
fi
done < <(lsblk -ln -o NAME,FSTYPE "$disk" | tail -n +2)
if echo "$USED_DISKS" | grep -q "$disk" || echo "$ZFS_DISKS" | grep -q "$disk"; then
return 0
fi
return 1
}
FREE_DISKS=()
LVM_DEVICES=$(pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') | xargs -n1 readlink -f | sort -u)
RAID_ACTIVE=$(grep -Po 'md\d+\s*:\s*active\s+raid[0-9]+' /proc/mdstat | awk '{print $1}' | sort -u)
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
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=""
REAL_PATH=$(readlink -f "$DISK")
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
else
for SYMLINK in /dev/disk/by-id/*; do
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
break
fi
fi
done
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
if $IS_MOUNTED; then
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
cleanup
whiptail --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 50
clear
exit 1
fi
msg_ok "$(translate "Available disks detected.")"
MAX_WIDTH=$(printf "%s\n" "${FREE_DISKS[@]}" | awk '{print length}' | sort -nr | head -n1)
TOTAL_WIDTH=$((MAX_WIDTH + 20))
TOTAL_WIDTH=$((TOTAL_WIDTH < 50 ? 50 : TOTAL_WIDTH))
SELECTED=$(whiptail --title "$(translate "Select Disk")" --radiolist \
"$(translate "Select the disk you want to mount on the host:")" 20 $TOTAL_WIDTH 10 "${FREE_DISKS[@]}" 3>&1 1>&2 2>&3)
if [ -z "$SELECTED" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 10 50
clear
exit 1
fi
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
################################################################
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
SKIP_FORMAT=false
DEFAULT_MOUNT="/mnt/data_shared"
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
else
whiptail --title "$(translate "Unsupported Filesystem")" --yesno "$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\\nDo you want to format it?")" 10 70
if [ $? -ne 0 ]; then
exit 0
fi
fi
else
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
PARTITION="$SELECTED"
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
else
whiptail --title "$(translate "No Valid Partitions")" --yesno "$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
if [ $? -ne 0 ]; then
exit 0
fi
echo -e "$(translate "Creating partition table and partition...")"
parted -s "$SELECTED" mklabel gpt
parted -s "$SELECTED" mkpart primary 0% 100%
sleep 2
partprobe "$SELECTED"
sleep 2
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
else
whiptail --title "$(translate "Partition Error")" --msgbox "$(translate "Failed to create partition on disk") $SELECTED." 8 70
exit 1
fi
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
FORMAT_TYPE=$(whiptail --title "$(translate "Select Format Type")" --menu "$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
"xfs" "XFS" \
"btrfs" "Btrfs" 3>&1 1>&2 2>&3)
if [ -z "$FORMAT_TYPE" ]; then
whiptail --title "$(translate "Format Cancelled")" --msgbox "$(translate "Format operation cancelled. The disk will not be added.")" 8 60
exit 0
fi
whiptail --title "$(translate "WARNING")" --yesno "$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\\n\\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\\n\\n$(translate "Are you sure you want to continue")" 15 70
if [ $? -ne 0 ]; then
exit 0
fi
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
case "$FORMAT_TYPE" in
"ext4") mkfs.ext4 -F "$PARTITION" ;;
"xfs") mkfs.xfs -f "$PARTITION" ;;
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
esac
if [ $? -ne 0 ]; then
cleanup
whiptail --title "$(translate "Format Failed")" --msgbox "$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
exit 1
else
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
partprobe "$SELECTED"
sleep 2
fi
fi
################################################################
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/data_shared):")" \
10 60 "$DEFAULT_MOUNT" 3>&1 1>&2 2>&3)
if [ -z "$MOUNT_POINT" ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
exit 1
fi
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
UUID=$(blkid -s UUID -o value "$PARTITION")
# Obtener sistema de archivos real
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
if grep -q "UUID=$UUID" /etc/fstab; then
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
msg_ok "$(translate "fstab entry updated for") $UUID"
else
echo "$FSTAB_ENTRY" >> /etc/fstab
msg_ok "$(translate "fstab entry added for") $UUID"
fi
##################################################################
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
##################################################################
if [ $? -eq 0 ]; then
if ! getent group sharedfiles >/dev/null; then
groupadd sharedfiles
msg_ok "$(translate "Group 'sharedfiles' created")"
else
msg_ok "$(translate "Group 'sharedfiles' already exists")"
fi
chown root:sharedfiles "$MOUNT_POINT"
chmod 2775 "$MOUNT_POINT"
whiptail --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
whiptail --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 1
fi

View File

@@ -1,146 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Mount point from host into LXC container (CT)
# ==========================================================
# Author : MacRimi
# License : MIT
# Description : Mount a folder from /mnt on the host to a mount point in a CT
# ==========================================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
#######################################################
CT_LIST=($(pct list | awk 'NR>1 {print $1":"$3}'))
if [[ ${#CT_LIST[@]} -eq 0 ]]; then
whiptail --title "$(translate "No CTs")" --msgbox "$(translate "No containers found.")" 8 40
exit 0
fi
CT_OPTIONS=()
for entry in "${CT_LIST[@]}"; do
ID="${entry%%:*}"
NAME="${entry##*:}"
CT_OPTIONS+=("$ID" "$NAME")
done
CTID=$(whiptail --title "$(translate "Select CT")" --menu "$(translate "Select the container:")" 20 60 10 "${CT_OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$CTID" ]] && exit 0
CT_STATUS=$(pct status "$CTID" | awk '{print $2}')
if [ "$CT_STATUS" != "running" ]; then
msg_info "$(translate "Starting CT") $CTID..."
pct start "$CTID"
sleep 2
if [ "$(pct status "$CTID" | awk '{print $2}')" != "running" ]; then
msg_error "$(translate "Failed to start the CT.")"
exit 1
fi
msg_ok "$(translate "CT started successfully.")"
fi
#######################################################
select_origin_path() {
METHOD=$(whiptail --title "$(translate "Select Host Folder")" --menu "$(translate "How do you want to select the host folder to mount?")" 15 60 5 \
"auto" "$(translate "Select from /mnt")" \
"manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3)
case "$METHOD" in
auto)
HOST_DIRS=(/mnt/*)
OPTIONS=()
for dir in "${HOST_DIRS[@]}"; do
[[ -d "$dir" ]] && OPTIONS+=("$dir" "")
done
ORIGIN=$(whiptail --title "$(translate "Select Host Folder")" --menu "$(translate "Select the folder to mount:")" 20 60 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$ORIGIN" ]] && return 1
;;
manual)
ORIGIN=$(whiptail --title "$(translate "Enter Path")" --inputbox "$(translate "Enter the full path to the host folder:")" 10 60 "/mnt/" 3>&1 1>&2 2>&3)
[[ -z "$ORIGIN" ]] && return 1
;;
esac
if [[ ! -d "$ORIGIN" ]]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "The selected path is not a valid directory:")\n$ORIGIN" 8 60
return 1
fi
# Preparar permisos en el host para uso compartido
SHARE_GID=999
if ! getent group sharedfiles >/dev/null; then
groupadd -g "$SHARE_GID" sharedfiles
msg_ok "$(translate "Group 'sharedfiles' created in the host with GID $SHARE_GID")"
else
msg_ok "$(translate "Group 'sharedfiles' already exists in the host")"
fi
chown root:sharedfiles "$ORIGIN"
chmod 2775 "$ORIGIN"
setfacl -d -m g:sharedfiles:rwx "$ORIGIN"
setfacl -m g:sharedfiles:rwx "$ORIGIN"
msg_ok "$(translate "Host folder prepared with shared group and permissions.")"
return 0
}
select_origin_path || exit 0
#######################################################
CT_NAME=$(pct config "$CTID" | awk -F: '/hostname/ {print $2}' | xargs)
DEFAULT_MOUNT_POINT="/mnt/host_share"
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point to CT")" \
--inputbox "$(translate "Enter the mount point inside the CT (e.g., /mnt/host_share):")" \
10 70 "$DEFAULT_MOUNT_POINT" 3>&1 1>&2 2>&3)
if [[ -z "$MOUNT_POINT" ]]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "No mount point specified.")" 8 60
exit 1
fi
if ! pct exec "$CTID" -- test -d "$MOUNT_POINT"; then
if whiptail --yesno "$(translate "Directory does not exist in the CT.")\n\n$MOUNT_POINT\n\n$(translate "Do you want to create it?")" 12 70 --title "$(translate "Create Directory")"; then
pct exec "$CTID" -- mkdir -p "$MOUNT_POINT"
msg_ok "$(translate "Directory created inside CT:") $MOUNT_POINT"
else
msg_error "$(translate "Directory not created. Operation cancelled.")"
exit 1
fi
fi
INDEX=0
while pct config "$CTID" | grep -q "mp${INDEX}:"; do
((INDEX++))
[[ $INDEX -ge 100 ]] && msg_error "Too many mount points." && exit 1
done
msg_info "$(translate "Mounting folder from host to CT...")"
RESULT=$(pct set "$CTID" -mp${INDEX} "$ORIGIN,mp=$MOUNT_POINT,backup=0,ro=0,acl=1" 2>&1)
if [[ $? -eq 0 ]]; then
msg_ok "$(translate "Successfully mounted:")\n$ORIGIN$CT_NAME:$MOUNT_POINT"
else
msg_error "$(translate "Error mounting folder:")\n$RESULT"
exit 1
fi
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 0

View File

@@ -1,446 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Mount independent disk on Proxmox host
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 1.3-dialog
# Last Updated: 13/12/2024
# ==========================================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
mount_disk_host_bk() {
get_disk_info() {
local disk=$1
MODEL=$(lsblk -dn -o MODEL "$disk" | xargs)
SIZE=$(lsblk -dn -o SIZE "$disk" | xargs)
echo "$MODEL" "$SIZE"
}
is_usb_disk() {
local disk=$1
local disk_name=$(basename "$disk")
if readlink -f "/sys/block/$disk_name/device" 2>/dev/null | grep -q "usb"; then
return 0
fi
if udevadm info --query=property --name="$disk" 2>/dev/null | grep -q "ID_BUS=usb"; then
return 0
fi
return 1
}
is_system_disk() {
local disk=$1
local disk_name=$(basename "$disk")
local system_mounts=$(df -h | grep -E '^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(/|/boot|/usr|/var|/home)$' | awk '{print $1}')
for mount_dev in $system_mounts; do
local mount_disk=""
if [[ "$mount_dev" =~ ^/dev/mapper/ ]]; then
local vg_name=$(lvs --noheadings -o vg_name "$mount_dev" 2>/dev/null | xargs)
if [[ -n "$vg_name" ]]; then
local pvs_list=$(pvs --noheadings -o pv_name -S vg_name="$vg_name" 2>/dev/null | xargs)
for pv in $pvs_list; do
if [[ -n "$pv" && -e "$pv" ]]; then
mount_disk=$(lsblk -no PKNAME "$pv" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
fi
elif [[ "$mount_dev" =~ ^/dev/[hsv]d[a-z][0-9]* || "$mount_dev" =~ ^/dev/nvme[0-9]+n[0-9]+p[0-9]+ ]]; then
mount_disk=$(lsblk -no PKNAME "$mount_dev" 2>/dev/null)
if [[ -n "$mount_disk" && "/dev/$mount_disk" == "$disk" ]]; then
return 0
fi
fi
done
local fs_type=$(lsblk -no FSTYPE "$disk" 2>/dev/null | head -1)
if [[ "$fs_type" == "btrfs" ]]; then
local temp_mount=$(mktemp -d)
if mount -o ro "$disk" "$temp_mount" 2>/dev/null; then
if btrfs subvolume list "$temp_mount" 2>/dev/null | grep -qE '(@|@home|@var|@boot|@root|root)'; then
umount "$temp_mount" 2>/dev/null
rmdir "$temp_mount" 2>/dev/null
return 0
fi
umount "$temp_mount" 2>/dev/null
fi
rmdir "$temp_mount" 2>/dev/null
while read -r part; do
if [[ -n "$part" ]]; then
local part_fs=$(lsblk -no FSTYPE "/dev/$part" 2>/dev/null)
if [[ "$part_fs" == "btrfs" ]]; then
local mount_point=$(lsblk -no MOUNTPOINT "/dev/$part" 2>/dev/null)
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
fi
local disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
local part_uuids=()
while read -r part; do
if [[ -n "$part" ]]; then
local uuid=$(blkid -s UUID -o value "/dev/$part" 2>/dev/null)
if [[ -n "$uuid" ]]; then
part_uuids+=("$uuid")
fi
fi
done < <(lsblk -ln -o NAME "$disk" | tail -n +2)
for uuid in "${part_uuids[@]}" "$disk_uuid"; do
if [[ -n "$uuid" ]] && grep -q "UUID=$uuid" /etc/fstab; then
local mount_point=$(grep "UUID=$uuid" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
done
if grep -q "$disk" /etc/fstab; then
local mount_point=$(grep "$disk" /etc/fstab | awk '{print $2}')
if [[ "$mount_point" == "/" || "$mount_point" == "/boot" || "$mount_point" == "/home" || "$mount_point" == "/var" ]]; then
return 0
fi
fi
local disk_count=$(lsblk -dn -e 7,11 -o PATH | wc -l)
if [[ "$disk_count" -eq 1 ]]; then
return 0
fi
return 1
}
msg_info "$(translate "Detecting available disks...")"
USED_DISKS=$(lsblk -n -o PKNAME,TYPE | grep 'lvm' | awk '{print "/dev/" $1}')
MOUNTED_DISKS=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
ZFS_DISKS=""
ZFS_RAW=$(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
for entry in $ZFS_RAW; do
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
if [ -e "/dev/disk/by-id/$entry" ]; then
path=$(readlink -f "/dev/disk/by-id/$entry")
fi
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [ -n "$path" ]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
if [ -n "$base_disk" ]; then
ZFS_DISKS+="/dev/$base_disk"$'\n'
fi
fi
done
ZFS_DISKS=$(echo "$ZFS_DISKS" | sort -u)
LVM_DEVICES=$(
pvs --noheadings -o pv_name 2> >(grep -v 'File descriptor .* leaked') |
while read -r dev; do
[[ -n "$dev" && -e "$dev" ]] && readlink -f "$dev"
done | sort -u
)
FREE_DISKS=()
while read -r DISK; do
[[ "$DISK" =~ /dev/zd ]] && continue
INFO=($(get_disk_info "$DISK"))
MODEL="${INFO[@]::${#INFO[@]}-1}"
SIZE="${INFO[-1]}"
LABEL=""
SHOW_DISK=true
IS_MOUNTED=false
IS_RAID=false
IS_ZFS=false
IS_LVM=false
IS_SYSTEM=false
IS_USB=false
if is_system_disk "$DISK"; then
IS_SYSTEM=true
fi
if is_usb_disk "$DISK"; then
IS_USB=true
fi
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=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
if [[ -n "$REAL_PATH" ]] && echo "$LVM_DEVICES" | grep -qFx "$REAL_PATH"; then
IS_MOUNTED=true
fi
USED_BY=""
REAL_PATH=""
if [[ -n "$DISK" && -e "$DISK" ]]; then
REAL_PATH=$(readlink -f "$DISK")
fi
CONFIG_DATA=$(grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
if grep -Fq "$REAL_PATH" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
else
for SYMLINK in /dev/disk/by-id/*; do
[[ -e "$SYMLINK" ]] || continue
if [[ "$(readlink -f "$SYMLINK")" == "$REAL_PATH" ]]; then
if grep -Fq "$SYMLINK" <<< "$CONFIG_DATA"; then
USED_BY="$(translate "In use")"
break
fi
fi
done
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
if $IS_MOUNTED; then SHOW_DISK=false; fi
if $IS_SYSTEM; then 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"
if $IS_USB; then
LABEL+=" USB"
else
LABEL+=" $(translate "Internal")"
fi
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
dialog --title "$(translate "Error")" --msgbox "$(translate "No available disks found on the host.")" 8 60
clear
exit 1
fi
msg_ok "$(translate "Available disks detected.")"
# Building the array for dialog (format: tag item on/off tag item on/off...)
DLG_LIST=()
for ((i=0; i<${#FREE_DISKS[@]}; i+=3)); do
DLG_LIST+=("${FREE_DISKS[i]}" "${FREE_DISKS[i+1]}" "${FREE_DISKS[i+2]}")
done
SELECTED=$(dialog --clear --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
--radiolist "\n$(translate "Select the disk you want to mount on the host:")" 20 90 10 \
"${DLG_LIST[@]}" 2>&1 >/dev/tty)
if [ -z "$SELECTED" ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No disk was selected.")" 8 50
clear
exit 1
fi
msg_ok "$(translate "Disk selected successfully:") $SELECTED"
# ------------------- Partitions and formatting ------------------------
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
SKIP_FORMAT=false
DEFAULT_MOUNT="/mnt/backup"
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
CURRENT_FS=$(lsblk -no FSTYPE "$PARTITION" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
msg_ok "$(translate "Detected existing filesystem") $CURRENT_FS $(translate "on") $PARTITION."
else
dialog --title "$(translate "Unsupported Filesystem")" --yesno \
"$(translate "The partition") $PARTITION $(translate "has an unsupported filesystem ($CURRENT_FS).\nDo you want to format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
fi
else
CURRENT_FS=$(lsblk -no FSTYPE "$SELECTED" | xargs)
if [[ "$CURRENT_FS" == "ext4" || "$CURRENT_FS" == "xfs" || "$CURRENT_FS" == "btrfs" ]]; then
SKIP_FORMAT=true
PARTITION="$SELECTED"
msg_ok "$(translate "Detected filesystem") $CURRENT_FS $(translate "directly on disk") $SELECTED."
else
dialog --title "$(translate "No Valid Partitions")" --yesno \
"$(translate "The disk has no partitions and no valid filesystem. Do you want to create a new partition and format it?")" 10 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Creating partition table and partition...")"
parted -s "$SELECTED" mklabel gpt
parted -s "$SELECTED" mkpart primary 0% 100%
sleep 2
partprobe "$SELECTED"
sleep 2
PARTITION=$(lsblk -rno NAME "$SELECTED" | awk -v disk="$(basename "$SELECTED")" '$1 != disk {print $1; exit}')
if [ -n "$PARTITION" ]; then
PARTITION="/dev/$PARTITION"
else
dialog --title "$(translate "Partition Error")" --msgbox \
"$(translate "Failed to create partition on disk") $SELECTED." 8 70
exit 1
fi
fi
fi
if [ "$SKIP_FORMAT" != true ]; then
FORMAT_TYPE=$(dialog --title "$(translate "Select Format Type")" --menu \
"$(translate "Select the filesystem type for") $PARTITION:" 15 60 5 \
"ext4" "$(translate "Extended Filesystem 4 (recommended)")" \
"xfs" "XFS" \
"btrfs" "Btrfs" 2>&1 >/dev/tty)
if [ -z "$FORMAT_TYPE" ]; then
dialog --title "$(translate "Format Cancelled")" --msgbox \
"$(translate "Format operation cancelled. The disk will not be added.")" 8 60
exit 0
fi
dialog --title "$(translate "WARNING")" --yesno \
"$(translate "WARNING: This operation will FORMAT the disk") $PARTITION $(translate "with") $FORMAT_TYPE.\n\n$(translate "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!")\n\n$(translate "Are you sure you want to continue")" 15 70
if [ $? -ne 0 ]; then exit 0; fi
echo -e "$(translate "Formatting partition") $PARTITION $(translate "with") $FORMAT_TYPE..."
case "$FORMAT_TYPE" in
"ext4") mkfs.ext4 -F "$PARTITION" ;;
"xfs") mkfs.xfs -f "$PARTITION" ;;
"btrfs") mkfs.btrfs -f "$PARTITION" ;;
esac
if [ $? -ne 0 ]; then
cleanup
dialog --title "$(translate "Format Failed")" --msgbox \
"$(translate "Failed to format partition") $PARTITION $(translate "with") $FORMAT_TYPE." 12 70
exit 1
else
msg_ok "$(translate "Partition") $PARTITION $(translate "successfully formatted with") $FORMAT_TYPE."
partprobe "$SELECTED"
sleep 2
fi
fi
# ------------------- Mount point and permissions -------------------
MOUNT_POINT=$(dialog --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the disk (e.g., /mnt/backup):")" \
10 60 "$DEFAULT_MOUNT" 2>&1 >/dev/tty)
if [ -z "$MOUNT_POINT" ]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No mount point was specified.")" 8 40
exit 1
fi
msg_ok "$(translate "Mount point specified:") $MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
UUID=$(blkid -s UUID -o value "$PARTITION")
FS_TYPE=$(lsblk -no FSTYPE "$PARTITION" | xargs)
FSTAB_ENTRY="UUID=$UUID $MOUNT_POINT $FS_TYPE defaults 0 0"
if grep -q "UUID=$UUID" /etc/fstab; then
sed -i "s|^.*UUID=$UUID.*|$FSTAB_ENTRY|" /etc/fstab
msg_ok "$(translate "fstab entry updated for") $UUID"
else
echo "$FSTAB_ENTRY" >> /etc/fstab
msg_ok "$(translate "fstab entry added for") $UUID"
fi
mount "$MOUNT_POINT" 2> >(grep -v "systemd still uses")
if [ $? -eq 0 ]; then
if ! getent group sharedfiles >/dev/null; then
groupadd sharedfiles
msg_ok "$(translate "Group 'sharedfiles' created")"
else
msg_ok "$(translate "Group 'sharedfiles' already exists")"
fi
chown root:sharedfiles "$MOUNT_POINT"
chmod 2775 "$MOUNT_POINT"
dialog --title "$(translate "Success")" --msgbox "$(translate "The disk has been successfully mounted at") $MOUNT_POINT" 8 60
echo "$MOUNT_POINT" > /usr/local/share/proxmenux/last_backup_mount.txt
msg_ok "$(translate "Disk mounted at") $MOUNT_POINT"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
dialog --title "$(translate "Mount Error")" --msgbox "$(translate "Failed to mount the disk at") $MOUNT_POINT" 8 60
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 1
fi
}

View File

@@ -0,0 +1,399 @@
#!/bin/bash
# ==========================================================
# ProxMenux - SMART Disk Health & Test Tool
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 12/04/2026
# ==========================================================
# Description:
# SMART health check and disk testing tool for Proxmox VE.
# Supports SATA/SAS disks (smartmontools) and NVMe drives (nvme-cli).
# Exports results as JSON to /usr/local/share/proxmenux/smart/
# for ProxMenux Monitor integration.
# Long tests run on the drive hardware and persist after terminal close.
# ==========================================================
# Configuration ============================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
BACKTITLE="ProxMenux"
SMART_DIR="$BASE_DIR/smart"
UI_MENU_H=22
UI_MENU_W=84
UI_MENU_LIST_H=12
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_RESULT_H=14
UI_RESULT_W=86
# shellcheck source=/dev/null
[[ -f "$UTILS_FILE" ]] && source "$UTILS_FILE"
load_language
initialize_cache
SCRIPT_DIR_SMART="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_SCRIPTS_LOCAL="$(cd "$SCRIPT_DIR_SMART/.." && pwd)"
if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/utils-install-functions.sh" ]]; then
source "$LOCAL_SCRIPTS_LOCAL/global/utils-install-functions.sh"
elif [[ -f "$LOCAL_SCRIPTS/global/utils-install-functions.sh" ]]; then
source "$LOCAL_SCRIPTS/global/utils-install-functions.sh"
fi
# Configuration ============================================
# ==========================================================
# Helpers
# ==========================================================
_smart_is_nvme() {
[[ "$1" == *nvme* ]]
}
_smart_disk_label() {
local disk="$1"
local model size
model=$(lsblk -dn -o MODEL "$disk" 2>/dev/null | xargs)
size=$(lsblk -dn -o SIZE "$disk" 2>/dev/null | xargs)
[[ -z "$model" ]] && model="Unknown"
[[ -z "$size" ]] && size="?"
printf '%-8s — %s' "$size" "$model"
}
_smart_json_path() {
local disk="$1"
echo "${SMART_DIR}/$(basename "$disk").json"
}
_smart_ensure_packages() {
local need_smartctl=0 need_nvme=0
command -v smartctl >/dev/null 2>&1 || need_smartctl=1
command -v nvme >/dev/null 2>&1 || need_nvme=1
if [[ $need_smartctl -eq 1 || $need_nvme -eq 1 ]]; then
show_proxmenux_logo
msg_title "$(translate 'SMART Disk Health & Test')"
ensure_repositories
[[ $need_smartctl -eq 1 ]] && install_single_package "smartmontools" "smartctl" "SMART monitoring tools"
[[ $need_nvme -eq 1 ]] && install_single_package "nvme-cli" "nvme" "NVMe management tools"
fi
}
# ==========================================================
# PHASE 1 — SELECTION
# All dialogs run here. No execution, no show_proxmenux_logo.
# ==========================================================
# ── Install packages if missing ───────────────────────────
_smart_ensure_packages
# ── Step 1: Detect disks ──────────────────────────────────
DISK_OPTIONS=()
while read -r disk; do
[[ -z "$disk" ]] && continue
[[ "$disk" =~ ^/dev/zd ]] && continue
label=$(_smart_disk_label "$disk")
DISK_OPTIONS+=("$disk" "$label")
done < <(lsblk -dn -e 7,11 -o PATH 2>/dev/null | grep -E '^/dev/(sd|nvme|vd|hd)')
stop_spinner
if [[ ${#DISK_OPTIONS[@]} -eq 0 ]]; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate 'No Disks Found')" \
--msgbox "\n$(translate 'No physical disks detected for SMART testing.')" \
$UI_MSG_H $UI_MSG_W
exit 1
fi
# ── Step 2: Select disk ───────────────────────────────────
SELECTED_DISK=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Select Disk')" \
--menu "\n$(translate 'Select the disk to test or inspect:')" \
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
"${DISK_OPTIONS[@]}" \
2>&1 >/dev/tty)
[[ -z "$SELECTED_DISK" ]] && exit 0
# ── Steps 3+: Action loop for the selected disk ───────────
DISK_LABEL=$(_smart_disk_label "$SELECTED_DISK")
mkdir -p "$SMART_DIR"
while true; do
# ── Select action ───────────────────────────────────────
ACTION=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate 'SMART Action')$(basename "$SELECTED_DISK") (${DISK_LABEL})" \
--menu "\n$(translate 'Select what to do with this disk:')" \
$UI_MENU_H $UI_MENU_W $UI_MENU_LIST_H \
"status" "$(translate 'Quick health status — overall SMART result + key attributes')" \
"report" "$(translate 'Full report — complete SMART data (scrollable)')" \
"short" "$(translate 'Short test — ~2 minutes, basic surface check')" \
"long" "$(translate 'Long test — full scan, runs in background if closed')" \
"progress" "$(translate 'Check test progress — show active or last test result')" \
2>&1 >/dev/tty)
[[ -z "$ACTION" ]] && exit 0
# ── Long test confirmation ───────────────────────────────
if [[ "$ACTION" == "long" ]]; then
DISK_SIZE=$(lsblk -dn -o SIZE "$SELECTED_DISK" 2>/dev/null | xargs)
if ! dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Long Test — Background')" \
--yesno "\n$(translate 'The long test runs directly on the disk hardware.')\n\n$(translate 'Disk:') $SELECTED_DISK ($DISK_SIZE)\n\n$(translate 'The test will continue even if you close this terminal.')\n$(translate 'Results will be saved automatically to:')\n$(_smart_json_path "$SELECTED_DISK")\n\n$(translate 'Start long test now?')" \
16 $UI_RESULT_W; then
continue
fi
fi
# ========================================================
# PHASE 2 — EXECUTION
# show_proxmenux_logo appears here exactly once per action.
# No dialogs from this point until "Press Enter".
# ========================================================
show_proxmenux_logo
msg_title "$(translate 'SMART Disk Health & Test')"
msg_ok "$(translate 'Disk:') ${BL}${SELECTED_DISK}${DISK_LABEL}${CL}"
echo ""
case "$ACTION" in
# ── Quick status ────────────────────────────────────────
status)
if _smart_is_nvme "$SELECTED_DISK"; then
msg_info "$(translate 'Reading NVMe SMART data...')"
OUTPUT=$(nvme smart-log "$SELECTED_DISK" 2>/dev/null)
stop_spinner
if [[ -z "$OUTPUT" ]]; then
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
else
HEALTH=$(echo "$OUTPUT" | grep -i "critical_warning" | awk '{print $NF}')
if [[ "$HEALTH" == "0" ]]; then
msg_ok "$(translate 'NVMe health status: PASSED')"
else
msg_warn "$(translate 'NVMe health status: WARNING (critical_warning =') $HEALTH)"
fi
echo ""
echo "$OUTPUT" | head -20
fi
else
msg_info "$(translate 'Reading SMART data...')"
HEALTH=$(smartctl -H "$SELECTED_DISK" 2>/dev/null | grep -i "overall-health")
ATTRS=$(smartctl -A "$SELECTED_DISK" 2>/dev/null)
stop_spinner
if [[ -z "$HEALTH" ]]; then
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
else
if echo "$HEALTH" | grep -qi "PASSED"; then
msg_ok "$(translate 'SMART health status: PASSED')"
else
msg_warn "$HEALTH"
fi
echo ""
echo "$ATTRS" | awk 'NR==1 || /Reallocated_Sector|Current_Pending|Uncorrectable|Temperature_Celsius|Power_On_Hours|Wear_Leveling|Media_Wearout/'
fi
fi
;;
# ── Full report (scrollable) ────────────────────────────
report)
msg_info "$(translate 'Reading full SMART report...')"
TMPFILE=$(mktemp)
if _smart_is_nvme "$SELECTED_DISK"; then
nvme smart-log "$SELECTED_DISK" > "$TMPFILE" 2>/dev/null
nvme id-ctrl "$SELECTED_DISK" >> "$TMPFILE" 2>/dev/null
else
smartctl -x "$SELECTED_DISK" > "$TMPFILE" 2>/dev/null
fi
stop_spinner
if [[ -s "$TMPFILE" ]]; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate 'Full SMART Report')$SELECTED_DISK" \
--textbox "$TMPFILE" 40 $UI_RESULT_W
else
msg_error "$(translate 'Could not read SMART data from') $SELECTED_DISK"
fi
rm -f "$TMPFILE"
;;
# ── Short test ──────────────────────────────────────────
short)
if _smart_is_nvme "$SELECTED_DISK"; then
msg_info "$(translate 'Starting NVMe short self-test...')"
if nvme device-self-test "$SELECTED_DISK" --self-test-code=1 >/dev/null 2>&1; then
stop_spinner
msg_ok "$(translate 'Short self-test started on') $SELECTED_DISK"
msg_ok "$(translate 'Test typically completes in ~2 minutes.')"
msg_ok "$(translate 'Use "Check test progress" to see results.')"
else
stop_spinner
msg_error "$(translate 'Failed to start self-test on') $SELECTED_DISK"
fi
else
msg_info "$(translate 'Starting SMART short self-test...')"
OUTPUT=$(smartctl -t short "$SELECTED_DISK" 2>/dev/null)
stop_spinner
if echo "$OUTPUT" | grep -qi "Test will complete"; then
msg_ok "$(translate 'Short self-test started on') $SELECTED_DISK"
ESTIMATE=$(echo "$OUTPUT" | grep -i "complete after" | head -1)
[[ -n "$ESTIMATE" ]] && msg_ok "$ESTIMATE"
msg_ok "$(translate 'Use "Check test progress" to see results.')"
else
msg_error "$(translate 'Failed to start self-test on') $SELECTED_DISK"
echo "$OUTPUT" | tail -5
fi
fi
;;
# ── Long test (background) ──────────────────────────────
long)
JSON_PATH=$(_smart_json_path "$SELECTED_DISK")
DISK_SAFE=$(printf '%q' "$SELECTED_DISK")
JSON_SAFE=$(printf '%q' "$JSON_PATH")
if _smart_is_nvme "$SELECTED_DISK"; then
msg_info "$(translate 'Starting NVMe long self-test...')"
if nvme device-self-test "$SELECTED_DISK" --self-test-code=2 >/dev/null 2>&1; then
stop_spinner
msg_ok "$(translate 'Long self-test started on') $SELECTED_DISK"
DISK_LABEL_SAFE=$(printf '%q' "$DISK_LABEL")
NOTIFY_SCRIPT="/usr/bin/notification_manager.py"
nohup bash -c "
while nvme device-self-test ${DISK_SAFE} --self-test-code=0 2>/dev/null | grep -qi 'in progress'; do
sleep 60
done
nvme smart-log -o json ${DISK_SAFE} > ${JSON_SAFE} 2>/dev/null
# Send notification when test completes
if [[ -f \"${NOTIFY_SCRIPT}\" ]]; then
HOSTNAME=\$(hostname -s)
TEST_RESULT=\$(nvme self-test-log ${DISK_SAFE} 2>/dev/null | head -20)
if echo \"\$TEST_RESULT\" | grep -qi 'completed without error\|success'; then
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
--message \"NVMe disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed successfully.\" 2>/dev/null || true
else
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity WARNING \
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
--message \"NVMe disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed. Check results for details.\" 2>/dev/null || true
fi
fi
" >/dev/null 2>&1 &
disown $!
else
stop_spinner
msg_error "$(translate 'Failed to start long self-test on') $SELECTED_DISK"
fi
else
msg_info "$(translate 'Starting SMART long self-test...')"
OUTPUT=$(smartctl -t long "$SELECTED_DISK" 2>/dev/null)
stop_spinner
if echo "$OUTPUT" | grep -qi "Test will complete"; then
msg_ok "$(translate 'Long self-test started on') $SELECTED_DISK"
ESTIMATE=$(echo "$OUTPUT" | grep -i "complete after" | head -1)
[[ -n "$ESTIMATE" ]] && msg_ok "$ESTIMATE"
echo ""
msg_ok "$(translate 'Test runs on the drive hardware — safe to close this terminal.')"
msg_ok "$(translate 'Results will be saved to:') $JSON_PATH"
DISK_LABEL_SAFE=$(printf '%q' "$DISK_LABEL")
NOTIFY_SCRIPT="/usr/bin/notification_manager.py"
nohup bash -c "
while smartctl -c ${DISK_SAFE} 2>/dev/null | grep -qiE 'Self-test routine in progress|[1-9][0-9]?% of test remaining'; do
sleep 60
done
smartctl --json=c ${DISK_SAFE} > ${JSON_SAFE} 2>/dev/null
# Send notification when test completes
if [[ -f \"${NOTIFY_SCRIPT}\" ]]; then
HOSTNAME=\$(hostname -s)
TEST_RESULT=\$(smartctl -l selftest ${DISK_SAFE} 2>/dev/null | grep -E '^# ?1')
if echo \"\$TEST_RESULT\" | grep -qi 'Completed without error'; then
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed successfully.\" 2>/dev/null || true
elif echo \"\$TEST_RESULT\" | grep -qi 'error\|fail'; then
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity CRITICAL \
--title \"\${HOSTNAME}: SMART Long Test FAILED\" \
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed with ERRORS. Check disk health immediately.\" 2>/dev/null || true
else
python3 \"${NOTIFY_SCRIPT}\" --action send-raw --severity INFO \
--title \"\${HOSTNAME}: SMART Long Test Completed\" \
--message \"Disk ${DISK_SAFE} (${DISK_LABEL_SAFE}) - Long self-test completed. Check results for details.\" 2>/dev/null || true
fi
fi
" >/dev/null 2>&1 &
disown $!
else
msg_error "$(translate 'Failed to start long self-test on') $SELECTED_DISK"
echo "$OUTPUT" | tail -5
fi
fi
;;
# ── Check progress ──────────────────────────────────────
progress)
if _smart_is_nvme "$SELECTED_DISK"; then
msg_info "$(translate 'Reading NVMe self-test log...')"
OUTPUT=$(nvme self-test-log "$SELECTED_DISK" 2>/dev/null)
stop_spinner
if [[ -z "$OUTPUT" ]]; then
msg_warn "$(translate 'No self-test log available for') $SELECTED_DISK"
else
echo "$OUTPUT" | head -30
fi
else
msg_info "$(translate 'Reading SMART self-test log...')"
# Active test: only "X% of test remaining" appears when a test is actually running
ACTIVE=$(smartctl -c "$SELECTED_DISK" 2>/dev/null | grep -iE "[1-9][0-9]?% of test remaining|Self-test routine in progress")
# Log: grab only result rows (^# N ...) and the column header (^Num)
LOG_OUT=$(smartctl -l selftest "$SELECTED_DISK" 2>/dev/null)
LOG_HEADER=$(echo "$LOG_OUT" | grep -E "^Num")
LOG_ENTRIES=$(echo "$LOG_OUT" | grep -E "^# ?[0-9]")
stop_spinner
if [[ -n "$ACTIVE" ]]; then
msg_ok "$(translate 'Test in progress:')"
echo "$ACTIVE"
echo ""
else
msg_ok "$(translate 'No test currently running')"
echo ""
fi
if [[ -n "$LOG_ENTRIES" ]]; then
msg_ok "$(translate 'Recent test results:')"
[[ -n "$LOG_HEADER" ]] && echo "$LOG_HEADER"
echo "$LOG_ENTRIES"
else
msg_warn "$(translate 'No self-test history found for') $SELECTED_DISK"
fi
fi
;;
esac
# ── Auto-export JSON (except long — handled by background monitor)
if [[ "$ACTION" != "long" && "$ACTION" != "report" ]]; then
JSON_PATH=$(_smart_json_path "$SELECTED_DISK")
if _smart_is_nvme "$SELECTED_DISK"; then
nvme smart-log -o json "$SELECTED_DISK" > "$JSON_PATH" 2>/dev/null
else
smartctl --json=c "$SELECTED_DISK" > "$JSON_PATH" 2>/dev/null
fi
[[ -s "$JSON_PATH" ]] || rm -f "$JSON_PATH"
fi
# ── "report" uses dialog --textbox, no Press Enter needed
if [[ "$ACTION" != "report" ]]; then
echo ""
msg_success "$(translate 'Press Enter to continue...')"
read -r
fi
done

View File

@@ -1,73 +0,0 @@
#!/bin/bash
# ==========================================================
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: 28/01/2025
# Description : Allows unmounting a previously mounted disk
# ==========================================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
MOUNTED_DISKS=($(mount | grep '^/dev/' | grep 'on /mnt/' | awk '{print $3}'))
if [[ ${#MOUNTED_DISKS[@]} -eq 0 ]]; then
whiptail --title "$(translate "No Disks")" --msgbox "$(translate "No mounted disks found under /mnt.")" 8 50
exit 0
fi
MENU_ITEMS=()
for MNT in "${MOUNTED_DISKS[@]}"; do
UUID=$(blkid | grep "$MNT" | awk '{print $2}' | tr -d '"')
DESC="$MNT $UUID"
MENU_ITEMS+=("$MNT" "$DESC")
done
SELECTED=$(whiptail --title "$(translate "Unmount Disk")" --menu "$(translate "Select the disk you want to unmount:")" 20 70 10 "${MENU_ITEMS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED" ]] && exit 0
whiptail --title "$(translate "Confirm Unmount")" --yesno "$(translate "Are you sure you want to unmount") $SELECTED?" 10 60 || exit 0
umount "$SELECTED" 2>/dev/null
if [ $? -ne 0 ]; then
whiptail --title "$(translate "Error")" --msgbox "$(translate "Failed to unmount disk at") $SELECTED" 8 60
exit 1
else
msg_ok "$(translate "Unmounted:") $SELECTED"
fi
whiptail --title "$(translate "Delete Mount Folder")" --yesno "$(translate "Do you want to delete the mount point folder") $SELECTED?" 10 60
if [ $? -eq 0 ]; then
rm -rf "$SELECTED"
msg_ok "$(translate "Deleted folder:") $SELECTED"
fi
DEVICE=$(findmnt -no SOURCE "$SELECTED")
UUID=$(blkid -s UUID -o value "$DEVICE")
if [ -n "$UUID" ]; then
sed -i "/UUID=$UUID/d" /etc/fstab
msg_ok "$(translate "fstab entry removed for") $UUID"
fi
whiptail --title "$(translate "Done")" --msgbox "$(translate "Disk unmounted and cleaned successfully.")" 8 60