mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 17:06:26 +00:00
Update scripts
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
183
scripts/storage/disk-storage-manual-guide.sh
Normal file
183
scripts/storage/disk-storage-manual-guide.sh
Normal 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
}
|
||||
|
||||
399
scripts/storage/smart-disk-test.sh
Normal file
399
scripts/storage/smart-disk-test.sh
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user