2025-04-08 22:31:25 +02:00
#!/bin/bash
# ==========================================================
2026-04-12 20:32:34 +02:00
# ProxMenux - Secure Disk Formatter
2025-04-08 22:31:25 +02:00
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
2026-04-12 20:32:34 +02:00
# License : GPL-3.0
# Version : 2.0
# Last Updated: 11/04/2026
2025-04-08 22:31:25 +02:00
# ==========================================================
2026-04-12 20:32:34 +02:00
# Description:
# Formats a physical disk with strict safety controls.
#
# Visibility rules:
# SHOWN — only fully free disks:
# not system-used and not referenced by VM/LXC configs.
# HIDDEN — host/system disks (root pool, swap, mounted, active ZFS/LVM/RAID).
# HIDDEN — disks referenced by VM/LXC (running or stopped).
#
# Safety at confirmation:
# - Disks with stale/active metadata show detailed warnings.
# - Disks used by running VMs are hard-blocked at confirmation.
# - Disks with mounted partitions are hard-blocked at execution (revalidation).
# - Double confirmation required: yesno + type full disk path.
2025-04-14 18:59:51 +02:00
# ==========================================================
2026-04-12 20:32:34 +02:00
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
LOCAL_SCRIPTS_LOCAL = " $( cd " $SCRIPT_DIR /.. " && pwd ) "
LOCAL_SCRIPTS_DEFAULT = "/usr/local/share/proxmenux/scripts"
LOCAL_SCRIPTS = " $LOCAL_SCRIPTS_DEFAULT "
2025-04-08 22:31:25 +02:00
BASE_DIR = "/usr/local/share/proxmenux"
2026-04-12 20:32:34 +02:00
UTILS_FILE = " $LOCAL_SCRIPTS /utils.sh "
if [ [ -f " $LOCAL_SCRIPTS_LOCAL /utils.sh " ] ] ; then
LOCAL_SCRIPTS = " $LOCAL_SCRIPTS_LOCAL "
UTILS_FILE = " $LOCAL_SCRIPTS /utils.sh "
elif [ [ ! -f " $UTILS_FILE " ] ] ; then
UTILS_FILE = " $BASE_DIR /utils.sh "
fi
2025-04-08 22:31:25 +02:00
if [ [ -f " $UTILS_FILE " ] ] ; then
source " $UTILS_FILE "
fi
load_language
initialize_cache
2025-04-14 18:59:51 +02:00
2026-04-12 20:32:34 +02:00
if [ [ -f " $LOCAL_SCRIPTS_LOCAL /global/vm_storage_helpers.sh " ] ] ; then
source " $LOCAL_SCRIPTS_LOCAL /global/vm_storage_helpers.sh "
elif [ [ -f " $LOCAL_SCRIPTS_DEFAULT /global/vm_storage_helpers.sh " ] ] ; then
source " $LOCAL_SCRIPTS_DEFAULT /global/vm_storage_helpers.sh "
fi
if [ [ -f " $LOCAL_SCRIPTS_LOCAL /global/disk_ops_helpers.sh " ] ] ; then
source " $LOCAL_SCRIPTS_LOCAL /global/disk_ops_helpers.sh "
elif [ [ -f " $LOCAL_SCRIPTS_DEFAULT /global/disk_ops_helpers.sh " ] ] ; then
source " $LOCAL_SCRIPTS_DEFAULT /global/disk_ops_helpers.sh "
fi
2025-04-14 18:59:51 +02:00
2026-04-12 20:32:34 +02:00
# shellcheck source=/dev/null
if [ [ -f " $LOCAL_SCRIPTS_LOCAL /global/utils-install-functions.sh " ] ] ; then
source " $LOCAL_SCRIPTS_LOCAL /global/utils-install-functions.sh "
elif [ [ -f " $LOCAL_SCRIPTS_DEFAULT /global/utils-install-functions.sh " ] ] ; then
source " $LOCAL_SCRIPTS_DEFAULT /global/utils-install-functions.sh "
fi
2025-04-14 18:59:51 +02:00
2026-04-12 20:32:34 +02:00
BACKTITLE = "ProxMenux"
UI_MENU_H = 20
UI_MENU_W = 84
UI_MENU_LIST_H = 10
UI_MSG_H = 10
UI_MSG_W = 72
UI_YESNO_H = 20
UI_YESNO_W = 86
UI_RESULT_H = 14
UI_RESULT_W = 86
OPERATION_MODE = ""
REVALIDATE_ERROR_DETAIL = ""
ZFS_POOL_NAME = ""
declare -A DISK_RUNNING_VM_FLAG
# ──────────────────────────────────────────────────────────────────────────────
# Basic disk info
# ──────────────────────────────────────────────────────────────────────────────
2025-04-08 22:31:25 +02:00
get_disk_info( ) {
2026-04-12 20:32:34 +02:00
local disk = " $1 " 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 model"
[ [ -z " $size " ] ] && size = "Unknown size"
printf '%s\t%s' " $model " " $size "
2025-04-08 22:31:25 +02:00
}
2026-04-12 20:32:34 +02:00
# Collect command stdout with timeout protection (best-effort).
_fmt_collect_cmd( ) {
local seconds = " $1 "
shift
if command -v timeout >/dev/null 2>& 1; then
timeout --kill-after= 2 " ${ seconds } s " " $@ " 2>/dev/null || true
else
" $@ " 2>/dev/null || true
fi
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Mount classification helpers
# ──────────────────────────────────────────────────────────────────────────────
# Returns 0 if the mountpoint is part of the OS root filesystem tree.
# These mounts trigger a hard block — the disk contains the running OS.
_is_system_mount( ) {
local mp = " $1 "
case " $mp " in
/| /boot| /boot/*| /usr| /usr/*| /var| /var/*| /etc| /lib| /lib/*| /lib64| /run| /proc| /sys)
return 0 ; ;
*) return 1 ; ;
esac
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# ZFS root-pool detection
# ──────────────────────────────────────────────────────────────────────────────
# Returns the name of the ZFS pool that holds the root filesystem, or empty
# if root is on a traditional block device (ext4/xfs/btrfs).
_get_zfs_root_pool( ) {
local root_fs
root_fs = $( df / 2>/dev/null | awk 'NR==2 {print $1}' )
# A ZFS dataset looks like "rpool/ROOT/pve-1" — not /dev/
if [ [ " $root_fs " != /dev/* && " $root_fs " = = */* ] ] ; then
echo " ${ root_fs %%/* } "
2025-04-08 22:31:25 +02:00
fi
2026-04-12 20:32:34 +02:00
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# ZFS pool membership helpers
# ──────────────────────────────────────────────────────────────────────────────
# Resolve a raw ZFS device entry (from zpool list -v -H) to a canonical
# /dev/sdX path. Handles: full /dev/ paths, by-id names, short kernel names.
_resolve_zfs_entry( ) {
local entry = " $1 " path base
if [ [ " $entry " = = /dev/* ] ] ; then
path = $( readlink -f " $entry " 2>/dev/null)
elif [ [ -e " /dev/disk/by-id/ $entry " ] ] ; then
path = $( readlink -f " /dev/disk/by-id/ $entry " 2>/dev/null)
elif [ [ -e " /dev/ $entry " ] ] ; then
path = $( readlink -f " /dev/ $entry " 2>/dev/null)
2025-04-08 22:31:25 +02:00
fi
2026-04-12 20:32:34 +02:00
[ [ -z " $path " ] ] && return
base = $( lsblk -no PKNAME " $path " 2>/dev/null)
if [ [ -n " $base " ] ] ; then
echo " /dev/ $base "
else
echo " $path " # whole-disk vdev — path is the disk itself
fi
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# Emit one /dev/sdX line per disk that is a member of a SPECIFIC ZFS pool.
_build_pool_disks( ) {
local pool_name = " $1 " entry
while read -r entry; do
[ [ -z " $entry " ] ] && continue
_resolve_zfs_entry " $entry "
done < <( _fmt_collect_cmd 8 zpool list -v -H " $pool_name " | awk '{print $1}' | \
grep -v '^-' | grep -v '^mirror' | grep -v '^raidz' | \
grep -v " ^ ${ pool_name } $" )
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# VM/CT config helpers
# ──────────────────────────────────────────────────────────────────────────────
# Return 0 if $disk appears (by real path or any by-id link) in $cfg_text.
_disk_in_config_text( ) {
local disk = " $1 " cfg_text = " $2 "
[ [ -z " $cfg_text " ] ] && return 1
local rp link
rp = $( readlink -f " $disk " 2>/dev/null)
[ [ -n " $rp " ] ] && grep -qF " $rp " <<< " $cfg_text " && return 0
for link in /dev/disk/by-id/*; do
[ [ -e " $link " ] ] || continue
[ [ " $( readlink -f " $link " 2>/dev/null) " = = " $rp " ] ] || continue
grep -qF " $link " <<< " $cfg_text " && return 0
done
return 1
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# Return the concatenated config text of all CURRENTLY RUNNING VMs and CTs.
_get_running_vm_config_text( ) {
local result = "" vmid state conf
while read -r vmid state; do
[ [ -z " $vmid " || " $state " != "running" ] ] && continue
for conf in " /etc/pve/qemu-server/ ${ vmid } .conf " " /etc/pve/lxc/ ${ vmid } .conf " ; do
[ [ -f " $conf " ] ] && result += $( grep -vE '^\s*#' " $conf " 2>/dev/null) $'\n'
2025-04-08 22:31:25 +02:00
done
2026-04-12 20:32:34 +02:00
done < <(
qm list --noborder 2>/dev/null | awk 'NR>1 {print $1, $3}'
pct list --noborder 2>/dev/null | awk 'NR>1 {print $1, $2}'
)
printf '%s' " $result "
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# Wrapper for disk_referenced_in_guest_configs (uses global helper when available).
disk_referenced_in_guest_configs( ) {
local disk = " $1 "
if declare -F _disk_used_in_guest_configs >/dev/null 2>& 1; then
_disk_used_in_guest_configs " $disk "
return $?
2025-04-08 22:31:25 +02:00
fi
2026-04-12 20:32:34 +02:00
local real_path config_data link
real_path = $( readlink -f " $disk " 2>/dev/null)
config_data = $( grep -vE '^\s*#' /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf 2>/dev/null)
[ [ -z " $config_data " ] ] && return 1
[ [ -n " $real_path " ] ] && grep -Fq " $real_path " <<< " $config_data " && return 0
for link in /dev/disk/by-id/*; do
[ [ -e " $link " ] ] || continue
[ [ " $( readlink -f " $link " 2>/dev/null) " = = " $real_path " ] ] || continue
grep -Fq " $link " <<< " $config_data " && return 0
done
return 1
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Build candidate disk list with smart classification
# ──────────────────────────────────────────────────────────────────────────────
#
# Hard blocks (disk hidden completely):
# • Any partition mounted at a system path (/, /boot, /usr, /var, etc.)
# • Disk is a member of the ZFS pool that holds the root filesystem
# • Any partition is active swap
#
# Strict free-disk policy:
# - Only show disks that are NOT used by host system and NOT referenced by
# any VM/CT config (running or stopped).
# - If a disk is shown, it is considered free for formatting.
#
# Populates: DISK_OPTIONS[] (DISK_RUNNING_VM_FLAG kept for compatibility)
# ──────────────────────────────────────────────────────────────────────────────
build_disk_candidates( ) {
DISK_OPTIONS = ( )
DISK_RUNNING_VM_FLAG = ( )
if declare -F _refresh_host_storage_cache >/dev/null 2>& 1; then
_refresh_host_storage_cache
2025-04-08 22:31:25 +02:00
fi
2026-04-12 20:32:34 +02:00
# ── Detect ZFS root pool (its disks are hard-blocked) ─────────────────
local root_pool root_pool_disks = ""
root_pool = $( _get_zfs_root_pool)
[ [ -n " $root_pool " ] ] && root_pool_disks = $( _build_pool_disks " $root_pool " | sort -u)
# ── Classify mounts: system (hard block) ─────────────────────────────
local sys_blocked_disks = "" swap_parts
swap_parts = $( swapon --noheadings --raw --show= NAME 2>/dev/null)
while read -r name mp; do
_is_system_mount " $mp " || continue
local parent
parent = $( lsblk -no PKNAME " /dev/ $name " 2>/dev/null)
[ [ -z " $parent " ] ] && parent = " $name "
sys_blocked_disks += " /dev/ $parent " $'\n'
done < <( lsblk -ln -o NAME,MOUNTPOINT 2>/dev/null | awk '$2!=""' )
sys_blocked_disks = $( sort -u <<< " $sys_blocked_disks " )
# ── Build running VM config text (done once) ──────────────────────────
local running_cfg = "" vmid state conf
while read -r vmid state; do
[ [ -z " $vmid " || " $state " != "running" ] ] && continue
for conf in " /etc/pve/qemu-server/ ${ vmid } .conf " " /etc/pve/lxc/ ${ vmid } .conf " ; do
[ [ -f " $conf " ] ] && running_cfg += $( grep -vE '^\s*#' " $conf " 2>/dev/null) $'\n'
done
done < <(
qm list --noborder 2>/dev/null | awk 'NR>1 {print $1, $3}'
pct list --noborder 2>/dev/null | awk 'NR>1 {print $1, $2}'
)
# ── Main disk enumeration ─────────────────────────────────────────────
local disk ro type
while read -r disk ro type; do
[ [ -z " $disk " ] ] && continue
[ [ " $type " != "disk" ] ] && continue
[ [ " $ro " = = "1" ] ] && continue
[ [ " $disk " = ~ ^/dev/zd ] ] && continue
local real_disk
real_disk = $( readlink -f " $disk " 2>/dev/null)
# ── Hard blocks ───────────────────────────────────────────────────
# Disk contains a system-critical mount (/, /boot, /usr, /var, ...)
grep -qFx " $disk " <<< " $sys_blocked_disks " && continue
[ [ -n " $real_disk " ] ] && grep -qFx " $real_disk " <<< " $sys_blocked_disks " && continue
# Disk has an active swap partition
local has_swap = 0 part_name
while read -r part_name; do
[ [ -z " $part_name " ] ] && continue
grep -qFx " /dev/ $part_name " <<< " $swap_parts " && { has_swap = 1; break; }
done < <( lsblk -ln -o NAME " $disk " 2>/dev/null)
( ( has_swap ) ) && continue
# Disk is a member of the ZFS root pool
grep -qFx " $disk " <<< " $root_pool_disks " && continue
[ [ -n " $real_disk " ] ] && grep -qFx " $real_disk " <<< " $root_pool_disks " && continue
# Running VM/CT reference => show but flag for hard block at confirmation
if _disk_in_config_text " $disk " " $running_cfg " ; then
DISK_RUNNING_VM_FLAG[ " $disk " ] = "1"
else
DISK_RUNNING_VM_FLAG[ " $disk " ] = "0"
2025-04-08 22:31:25 +02:00
fi
2026-04-12 20:32:34 +02:00
# NOTE: stopped VM reference, active ZFS/LVM/RAID, and mounted data
# partitions are NOT hidden — they show with metadata warnings in the
# confirmation dialog. The revalidation step handles auto-unmount/export.
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ── Build display label ───────────────────────────────────────────
local model size desc
IFS = $'\t' read -r model size < <( get_disk_info " $disk " )
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
desc = $( printf "%-30s %10s" " $model " " $size " )
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
DISK_OPTIONS += ( " $disk " " $desc " "OFF" )
done < <( lsblk -dn -e 7,11 -o PATH,RO,TYPE 2>/dev/null)
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Disk selection dialog
# ──────────────────────────────────────────────────────────────────────────────
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
select_target_disk( ) {
build_disk_candidates
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
if [ [ ${# DISK_OPTIONS [@] } -eq 0 ] ] ; then
dialog --backtitle " $BACKTITLE " \
--title " $( translate "No Disks Available" ) " \
--msgbox " \n $( translate "No format-safe disks are available." ) \n\n $( translate "Only fully free disks are shown (not system-used and not referenced by VM/LXC)." ) " \
$UI_RESULT_H $UI_RESULT_W
return 1
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
local prompt_text
prompt_text = " \n $( translate "Select the disk you want to format:" ) "
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
local max_width total_width selected
max_width = $( printf "%s\n" " ${ DISK_OPTIONS [@] } " | awk '{print length}' | sort -nr | head -n1)
total_width = $(( max_width + 22 ))
( ( total_width < UI_MENU_W ) ) && total_width = $UI_MENU_W
( ( total_width > 116 ) ) && total_width = 116
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
selected = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Select Disk" ) " \
--radiolist " $prompt_text " $UI_MENU_H " $total_width " $UI_MENU_LIST_H \
" ${ DISK_OPTIONS [@] } " \
2>& 1 >/dev/tty) || return 1
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
[ [ -z " $selected " ] ] && return 1
SELECTED_DISK = " $selected "
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Metadata flag reader (for confirmation dialog display)
# ──────────────────────────────────────────────────────────────────────────────
get_disk_metadata_flags( ) {
local disk = " $1 " flags = "" fstype mp
while read -r fstype; do
case " $fstype " in
linux_raid_member) [ [ " $flags " != *"RAID" * ] ] && flags += " RAID" ; ;
LVM2_member) [ [ " $flags " != *"LVM" * ] ] && flags += " LVM" ; ;
zfs_member) [ [ " $flags " != *"ZFS" * ] ] && flags += " ZFS" ; ;
esac
done < <( lsblk -ln -o FSTYPE " $disk " 2>/dev/null | awk 'NF' )
# Mounted data partitions
while read -r mp; do
[ [ -z " $mp " ] ] && continue
_is_system_mount " $mp " && continue
[ [ " $flags " != *"MOUNT" * ] ] && flags += " MOUNT ( $mp ) "
done < <( lsblk -ln -o MOUNTPOINT " $disk " 2>/dev/null | awk 'NF' )
echo " $flags "
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Confirmation dialogs
# ──────────────────────────────────────────────────────────────────────────────
confirm_format_action( ) {
# Hard block: disk is currently referenced by a RUNNING VM/CT
if [ [ " ${ DISK_RUNNING_VM_FLAG [ $SELECTED_DISK ] :- 0 } " = = "1" ] ] ; then
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Disk In Use by Running VM" ) " \
--msgbox " \n⛔ $( translate "CRITICAL: The selected disk is referenced by a RUNNING VM or CT." ) \n\n $( translate "Stop the VM/CT before formatting this disk." ) " \
$UI_RESULT_H $UI_RESULT_W
return 1
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
local model size flags msg typed
IFS = $'\t' read -r model size < <( get_disk_info " $SELECTED_DISK " )
flags = $( get_disk_metadata_flags " $SELECTED_DISK " )
msg = " $( translate "Target disk" ) : $SELECTED_DISK \n "
msg += " $( translate "Model" ) : $model \n "
msg += " $( translate "Size" ) : $size \n "
case " $OPERATION_MODE " in
wipe_all)
msg += " $( translate "Operation" ) : $( translate "Wipe all — remove partitions + metadata" ) \n " ; ;
clean_sigs)
msg += " $( translate "Operation" ) : $( translate "Remove FS labels — partitions and data preserved" ) \n " ; ;
wipe_data)
msg += " $( translate "Operation" ) : $( translate "Zero all data — partition table preserved" ) \n " ; ;
clean_and_format)
msg += " $( translate "Operation" ) : $( translate "Full format: clean + new GPT partition + filesystem" ) \n " ; ;
esac
[ [ -n " $flags " ] ] && msg += " $( translate "Detected" ) : $flags \n "
# Stopped VM warning
if disk_referenced_in_guest_configs " $SELECTED_DISK " ; then
msg += " \n⚠ $( translate "WARNING: This disk is referenced in a stopped VM/LXC config." ) \n "
msg += " $( translate "The VM/LXC will lose access to this disk after formatting." ) \n "
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# Mounted partition warning
if [ [ " $flags " = = *"MOUNT" * ] ] ; then
msg += " \n⚠ $( translate "WARNING: This disk has a mounted partition." ) \n "
msg += " $( translate "Unmount it before proceeding. The script will verify this at execution." ) \n "
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
msg += " \n $( translate "WARNING: This will ERASE all data on this disk." ) \n "
msg += " $( translate "Do you want to continue?" ) "
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Confirm Format" ) " \
--yesno " \n $msg " $UI_YESNO_H $UI_YESNO_W || return 1
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
typed = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Final Confirmation" ) " \
--inputbox " $( translate "Type the full disk path to confirm" ) :\n $SELECTED_DISK " $UI_MSG_H $UI_MSG_W \
2>& 1 >/dev/tty) || return 1
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
if [ [ " $typed " != " $SELECTED_DISK " ] ] ; then
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Confirmation Failed" ) " \
--msgbox " \n $( translate "Typed value does not match selected disk. Operation cancelled." ) " $UI_MSG_H $UI_MSG_W
return 1
fi
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Operation and filesystem selection
# ──────────────────────────────────────────────────────────────────────────────
select_operation_mode( ) {
local selected
selected = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Format Mode" ) " \
--menu " \n $( translate "Choose what to do with the selected disk:" ) " 16 76 4 \
"1" " $( translate "Wipe all — erase partitions + metadata" ) " \
"2" " $( translate "Remove FS labels — partitions and data preserved" ) " \
"3" " $( translate "Zero all data — partition table preserved, data wiped" ) " \
"4" " $( translate "Full format — new GPT partition + filesystem" ) " \
2>& 1 >/dev/tty) || return 1
[ [ -z " $selected " ] ] && return 1
case " $selected " in
1) OPERATION_MODE = "wipe_all" ; ;
2) OPERATION_MODE = "clean_sigs" ; ;
3) OPERATION_MODE = "wipe_data" ; ;
4) OPERATION_MODE = "clean_and_format" ; ;
*) return 1 ; ;
esac
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Pre-execution safety revalidation
# Refreshes state and blocks if the selected disk becomes system-critical,
# mounted, swapped, or referenced by a running VM/CT.
# ──────────────────────────────────────────────────────────────────────────────
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
revalidate_selected_disk( ) {
REVALIDATE_ERROR_DETAIL = ""
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
if declare -F _refresh_host_storage_cache >/dev/null 2>& 1; then
_refresh_host_storage_cache
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# Hard block: disk now contains a system-critical mount
local name mp parent
while read -r name mp; do
_is_system_mount " $mp " || continue
parent = $( lsblk -no PKNAME " /dev/ $name " 2>/dev/null)
[ [ " /dev/ ${ parent :- $name } " = = " $SELECTED_DISK " ] ] && {
REVALIDATE_ERROR_DETAIL = " $( translate "The selected disk now contains a system-critical mount. Aborting." ) "
return 1
}
done < <( lsblk -ln -o NAME,MOUNTPOINT 2>/dev/null | awk '$2!=""' )
# Hard block: disk is now a member of the ZFS root pool
local root_pool root_pool_disks
root_pool = $( _get_zfs_root_pool)
if [ [ -n " $root_pool " ] ] ; then
root_pool_disks = $( _build_pool_disks " $root_pool " | sort -u)
if grep -qFx " $SELECTED_DISK " <<< " $root_pool_disks " ; then
REVALIDATE_ERROR_DETAIL = " $( translate "The selected disk is now part of the system ZFS pool. Aborting." ) "
return 1
fi
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# Hard block: disk has a swap partition
local swap_parts pname
swap_parts = $( swapon --noheadings --raw --show= NAME 2>/dev/null)
while read -r pname; do
[ [ -z " $pname " ] ] && continue
if grep -qFx " /dev/ $pname " <<< " $swap_parts " ; then
REVALIDATE_ERROR_DETAIL = " $( translate "The selected disk has an active swap partition. Aborting." ) "
return 1
fi
done < <( lsblk -ln -o NAME " $SELECTED_DISK " 2>/dev/null)
# Auto-unmount data partitions still mounted on this disk
while read -r pname mp; do
[ [ -z " $mp " ] ] && continue
_is_system_mount " $mp " && continue # already blocked above
local disk_of_part
disk_of_part = $( lsblk -no PKNAME " /dev/ $pname " 2>/dev/null)
[ [ " /dev/ ${ disk_of_part :- $pname } " = = " $SELECTED_DISK " ] ] || continue
if ! umount " /dev/ $pname " 2>/dev/null; then
REVALIDATE_ERROR_DETAIL = " $( translate "Partition" ) /dev/ $pname $( translate "is mounted at" ) $mp $( translate "and could not be unmounted — disk may be busy." ) "
return 1
fi
done < <( lsblk -ln -o NAME,MOUNTPOINT " $SELECTED_DISK " 2>/dev/null | awk '$2!=""' )
# Auto-export any active ZFS pool that contains this disk
local pool
while read -r pool; do
[ [ -z " $pool " ] ] && continue
if _build_pool_disks " $pool " 2>/dev/null | grep -qFx " $SELECTED_DISK " ; then
zpool export " $pool " 2>/dev/null || true
fi
done < <( _fmt_collect_cmd 5 zpool list -H -o name 2>/dev/null)
# Hard block: disk is currently referenced by a RUNNING VM or CT
local running_cfg
running_cfg = $( _get_running_vm_config_text)
if _disk_in_config_text " $SELECTED_DISK " " $running_cfg " ; then
REVALIDATE_ERROR_DETAIL = " $( translate "The selected disk is currently used by a RUNNING VM or CT. Stop it before formatting." ) "
return 1
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Filesystem selection and ZFS pool name prompt
# ──────────────────────────────────────────────────────────────────────────────
select_filesystem( ) {
local selected
selected = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "Select Filesystem" ) " \
--menu " \n $( translate "Choose the filesystem for the new partition:" ) " 18 76 8 \
"ext4" " $( translate "Extended Filesystem 4 (recommended)" ) " \
"xfs" "XFS" \
"exfat" " $( translate "exFAT (portable: Windows/Linux/macOS)" ) " \
"btrfs" "Btrfs" \
"zfs" "ZFS" \
2>& 1 >/dev/tty) || return 1
[ [ -z " $selected " ] ] && return 1
FORMAT_TYPE = " $selected "
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
prompt_zfs_pool_name( ) {
local disk_suffix suggested name
disk_suffix = $( basename " $SELECTED_DISK " | sed 's|[^a-zA-Z0-9_-]|-|g' )
suggested = " pool_ ${ disk_suffix } "
name = $( dialog --backtitle " $BACKTITLE " \
--title " $( translate "ZFS Pool Name" ) " \
--inputbox " $( translate "Enter ZFS pool name for the selected disk:" ) " \
10 72 " $suggested " 2>& 1 >/dev/tty) || return 1
[ [ -n " $name " ] ] || return 1
if [ [ ! " $name " = ~ ^[ a-zA-Z] [ a-zA-Z0-9_.:-] *$ ] ] ; then
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Invalid name" ) " \
--msgbox " \n $( translate "Invalid ZFS pool name." ) " $UI_MSG_H $UI_MSG_W
return 1
fi
if zpool list " $name " >/dev/null 2>& 1; then
dialog --backtitle " $BACKTITLE " \
--title " $( translate "Pool exists" ) " \
--msgbox " \n $( translate "A ZFS pool with this name already exists." ) \n\n $name " $UI_MSG_H $UI_RESULT_W
return 1
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
ZFS_POOL_NAME = " $name "
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Filesystem tool check / install
# ──────────────────────────────────────────────────────────────────────────────
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
ensure_fs_tool( ) {
case " $FORMAT_TYPE " in
exfat)
command -v mkfs.exfat >/dev/null 2>& 1 && return 0
if declare -F ensure_repositories >/dev/null 2>& 1; then
ensure_repositories || true
fi
if DEBIAN_FRONTEND = noninteractive apt-get install -y exfatprogs >/dev/null 2>& 1; then
command -v mkfs.exfat >/dev/null 2>& 1 && {
msg_ok " $( translate "exFAT tools installed successfully." ) "
return 0
}
fi
msg_error " $( translate "Could not install exFAT tools automatically." ) "
msg_info3 " $( translate "Install manually and retry: apt-get install -y exfatprogs" ) "
return 1
; ;
btrfs)
command -v mkfs.btrfs >/dev/null 2>& 1 && return 0
msg_error " $( translate "mkfs.btrfs not found. Install btrfs-progs and retry." ) "
return 1
; ;
zfs)
command -v zpool >/dev/null 2>& 1 && command -v zfs >/dev/null 2>& 1 && return 0
msg_error " $( translate "ZFS tools not found. Install zfsutils-linux and retry." ) "
return 1
; ;
esac
return 0
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Terminal phase helpers
# ──────────────────────────────────────────────────────────────────────────────
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
show_terminal_stage_header( ) {
show_proxmenux_logo
msg_title " $( translate "Secure Disk Formatter" ) "
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
wait_for_enter_to_main( ) {
echo
msg_success " $( translate "Press Enter to return to menu..." ) "
read -r
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# ──────────────────────────────────────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────────────────────────────────────
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
main( ) {
select_target_disk || exit 0
select_operation_mode || exit 0
confirm_format_action || exit 0
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
if [ [ " $OPERATION_MODE " = = "clean_and_format" ] ] ; then
select_filesystem || exit 0
if [ [ " $FORMAT_TYPE " = = "zfs" ] ] ; then
prompt_zfs_pool_name || exit 0
fi
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
show_terminal_stage_header
local _model _size
IFS = $'\t' read -r _model _size < <( get_disk_info " $SELECTED_DISK " )
msg_ok " $( translate "Disk" ) : ${ CL } ${ BL } $SELECTED_DISK — $_model $_size ${ CL } "
case " $OPERATION_MODE " in
wipe_all) msg_ok " $( translate "Mode" ) : $( translate "Wipe all — remove partitions + metadata" ) " ; ;
clean_sigs) msg_ok " $( translate "Mode" ) : $( translate "Remove FS labels — partitions and data preserved" ) " ; ;
wipe_data) msg_ok " $( translate "Mode" ) : $( translate "Zero all data — partition table preserved" ) " ; ;
clean_and_format) msg_ok " $( translate "Mode" ) : $( translate "Full format — new GPT partition + filesystem" ) "
msg_ok " $( translate "Filesystem" ) : $FORMAT_TYPE "
[ [ " $FORMAT_TYPE " = = "zfs" ] ] && msg_ok " $( translate "ZFS pool" ) : $ZFS_POOL_NAME " ; ;
esac
echo
if [ [ " $OPERATION_MODE " = = "clean_and_format" ] ] ; then
if ! ensure_fs_tool; then
wait_for_enter_to_main
exit 1
fi
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
msg_info " $( translate "Validating disk safety..." ) "
if ! revalidate_selected_disk; then
msg_error " ${ REVALIDATE_ERROR_DETAIL :- $( translate "Disk safety revalidation failed." ) } "
wait_for_enter_to_main
exit 1
fi
msg_ok " $( translate "Disk safety validation passed." ) "
# ── Execute the selected operation ────────────────────────────────────────
export DOH_SHOW_PROGRESS = 0
export DOH_ENABLE_STACK_RELEASE = 0
if [ [ " $OPERATION_MODE " = = "wipe_all" ] ] ; then
msg_info " $( translate "Wiping partitions and metadata..." ) "
doh_wipe_disk " $SELECTED_DISK "
msg_ok " $( translate "All partitions and metadata removed." ) "
echo
msg_success " $( translate "Disk is ready to be added to Proxmox storage." ) "
echo
wait_for_enter_to_main
exit 0
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
if [ [ " $OPERATION_MODE " = = "clean_sigs" ] ] ; then
msg_info " $( translate "Removing filesystem signatures..." ) "
wipefs -af " $SELECTED_DISK " >/dev/null 2>& 1 || true
local pname
while read -r pname; do
[ [ -z " $pname " ] ] && continue
[ [ " /dev/ $pname " = = " $SELECTED_DISK " ] ] && continue
[ [ -b " /dev/ $pname " ] ] && wipefs -af " /dev/ $pname " >/dev/null 2>& 1 || true
done < <( lsblk -ln -o NAME " $SELECTED_DISK " 2>/dev/null | tail -n +2)
msg_ok " $( translate "Signatures removed. Partition table preserved." ) "
echo
msg_success " $( translate "Disk is ready for VM passthrough." ) "
echo
wait_for_enter_to_main
exit 0
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
if [ [ " $OPERATION_MODE " = = "wipe_data" ] ] ; then
local wiped = 0 part_path
while read -r pname; do
[ [ -z " $pname " ] ] && continue
part_path = " /dev/ $pname "
[ [ " $part_path " = = " $SELECTED_DISK " ] ] && continue
if [ [ -b " $part_path " ] ] ; then
msg_info " $( translate "Zeroing partition" ) : $part_path "
if dd if = /dev/zero of = " $part_path " bs = 4M status = none 2>/dev/null; then
msg_ok " $part_path $( translate "zeroed." ) "
wiped = $(( wiped + 1 ))
else
msg_warn " $( translate "Could not fully zero" ) : $part_path "
fi
fi
done < <( lsblk -ln -o NAME " $SELECTED_DISK " 2>/dev/null | tail -n +2)
echo
if ( ( wiped = = 0 ) ) ; then
msg_warn " $( translate "No partitions found on disk. Nothing was wiped." ) "
else
msg_ok " $( translate "Data wiped from" ) $wiped $( translate "partition(s). Partition table preserved." ) "
echo
msg_success " $( translate "Data wipe complete." ) "
fi
echo
wait_for_enter_to_main
exit 0
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
# OPERATION_MODE == "clean_and_format"
msg_info " $( translate "Cleaning disk metadata..." ) "
doh_wipe_disk " $SELECTED_DISK "
msg_ok " $( translate "Disk metadata cleaned." ) "
msg_info " $( translate "Creating partition..." ) "
if ! doh_create_partition " $SELECTED_DISK " ; then
msg_error " $( translate "Failed to create partition." ) "
local detail_msg
detail_msg = " $( printf '%s' " $DOH_PARTITION_ERROR_DETAIL " | head -n 3) "
[ [ -n " $detail_msg " ] ] && msg_warn " $( translate "Details" ) : $detail_msg "
wait_for_enter_to_main
exit 1
fi
PARTITION = " $DOH_CREATED_PARTITION "
msg_ok " $( translate "Partition created" ) : $PARTITION "
msg_info " $( translate "Formatting" ) $PARTITION $( translate "as" ) $FORMAT_TYPE ... "
if doh_format_partition " $PARTITION " " $FORMAT_TYPE " "" " $ZFS_POOL_NAME " ; then
if [ [ " $FORMAT_TYPE " = = "zfs" ] ] ; then
msg_ok " $( translate "ZFS pool created" ) : $ZFS_POOL_NAME "
else
msg_ok " $PARTITION $( translate "formatted as" ) $FORMAT_TYPE "
fi
echo
msg_success " $( translate "Disk formatted successfully." ) "
echo
wait_for_enter_to_main
exit 0
fi
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
msg_error " $( translate "Failed to format the partition." ) "
[ [ -n " $DOH_FORMAT_ERROR_DETAIL " ] ] && msg_warn " $( translate "Details" ) : $DOH_FORMAT_ERROR_DETAIL "
echo
wait_for_enter_to_main
2025-04-08 22:31:25 +02:00
exit 1
2026-04-12 20:32:34 +02:00
}
2025-04-08 22:31:25 +02:00
2026-04-12 20:32:34 +02:00
main " $@ "