#!/bin/bash # ========================================================== # ProxMenux - Secure Disk Formatter # ========================================================== # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : GPL-3.0 # Version : 2.0 # Last Updated: 11/04/2026 # ========================================================== # 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. # ========================================================== 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 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 # 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 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 # ────────────────────────────────────────────────────────────────────────────── get_disk_info() { 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" } # 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 } # ────────────────────────────────────────────────────────────────────────────── # 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 } # ────────────────────────────────────────────────────────────────────────────── # 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%%/*}" fi } # ────────────────────────────────────────────────────────────────────────────── # 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) fi [[ -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 } # 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}$") } # ────────────────────────────────────────────────────────────────────────────── # 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 } # 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' 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}' ) printf '%s' "$result" } # 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 $? fi 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 } # ────────────────────────────────────────────────────────────────────────────── # 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 fi # ── 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" fi # 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. # ── Build display label ─────────────────────────────────────────── local model size desc IFS=$'\t' read -r model size < <(get_disk_info "$disk") desc=$(printf "%-30s %10s" "$model" "$size") DISK_OPTIONS+=("$disk" "$desc" "OFF") done < <(lsblk -dn -e 7,11 -o PATH,RO,TYPE 2>/dev/null) } # ────────────────────────────────────────────────────────────────────────────── # Disk selection dialog # ────────────────────────────────────────────────────────────────────────────── select_target_disk() { build_disk_candidates 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 local prompt_text prompt_text="\n$(translate "Select the disk you want to format:")" 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 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 [[ -z "$selected" ]] && return 1 SELECTED_DISK="$selected" return 0 } # ────────────────────────────────────────────────────────────────────────────── # 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" } # ────────────────────────────────────────────────────────────────────────────── # 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 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 # 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 msg+="\n$(translate "WARNING: This will ERASE all data on this disk.")\n" msg+="$(translate "Do you want to continue?")" dialog --backtitle "$BACKTITLE" \ --title "$(translate "Confirm Format")" \ --yesno "\n$msg" $UI_YESNO_H $UI_YESNO_W || return 1 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 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 } # ────────────────────────────────────────────────────────────────────────────── # 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 } # ────────────────────────────────────────────────────────────────────────────── # Pre-execution safety revalidation # Refreshes state and blocks if the selected disk becomes system-critical, # mounted, swapped, or referenced by a running VM/CT. # ────────────────────────────────────────────────────────────────────────────── revalidate_selected_disk() { REVALIDATE_ERROR_DETAIL="" if declare -F _refresh_host_storage_cache >/dev/null 2>&1; then _refresh_host_storage_cache fi # 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 # 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 return 0 } # ────────────────────────────────────────────────────────────────────────────── # 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 } 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 ZFS_POOL_NAME="$name" return 0 } # ────────────────────────────────────────────────────────────────────────────── # Filesystem tool check / install # ────────────────────────────────────────────────────────────────────────────── 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 } # ────────────────────────────────────────────────────────────────────────────── # Terminal phase helpers # ────────────────────────────────────────────────────────────────────────────── show_terminal_stage_header() { show_proxmenux_logo msg_title "$(translate "Secure Disk Formatter")" } wait_for_enter_to_main() { echo msg_success "$(translate "Press Enter to return to menu...")" read -r } # ────────────────────────────────────────────────────────────────────────────── # Main # ────────────────────────────────────────────────────────────────────────────── main() { select_target_disk || exit 0 select_operation_mode || exit 0 confirm_format_action || exit 0 if [[ "$OPERATION_MODE" == "clean_and_format" ]]; then select_filesystem || exit 0 if [[ "$FORMAT_TYPE" == "zfs" ]]; then prompt_zfs_pool_name || exit 0 fi fi 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 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 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 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 # 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 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 exit 1 } main "$@"