Files
ProxMenux/scripts/share/disk_host.sh
2026-04-12 20:32:34 +02:00

1064 lines
40 KiB
Bash

#!/bin/bash
# ==========================================================
# ProxMenux - Local Disk Manager for Proxmox Host
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# ==========================================================
# Description:
# Adds local SCSI/SATA/NVMe disks as Proxmox directory storage
# (pvesm add dir) or ZFS pool storage (pvesm add zfspool).
# The disk can be formatted (ext4/xfs/zfs) and registered in Proxmox.
# ==========================================================
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
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
load_language
initialize_cache
if ! command -v pveversion >/dev/null 2>&1; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
--msgbox "$(translate "This script must be run on a Proxmox host.")" 8 60
exit 1
fi
# ==========================================================
# SYSTEM STORAGE DETECTION
# ==========================================================
# Returns the name of the ZFS pool containing the root filesystem, if any.
_get_system_zfs_pool() {
local root_fs
root_fs=$(df / 2>/dev/null | awk 'NR==2 {print $1}')
if [[ "$root_fs" != /dev/* && "$root_fs" == */* ]]; then
echo "${root_fs%%/*}"
fi
}
# Returns 0 if the given pvesm storage is a user-created disk storage
# that should appear in add/remove menus. Returns 1 for system storages.
_is_user_disk_storage() {
local storage_id="$1"
local storage_type="$2"
local sys_pool
local cfg_path pool
cfg_path=$(get_storage_config "$storage_id" | awk '$1 == "path" {print $2}')
pool=$(get_storage_config "$storage_id" | awk '$1 == "pool" {print $2}')
case "$storage_type" in
dir)
# User-created dir storages are always mounted under /mnt/
[[ "$cfg_path" == /mnt/* ]] && return 0
return 1
;;
zfspool)
# User-created ZFS pool storages are NOT on the root pool or its datasets
sys_pool=$(_get_system_zfs_pool)
if [[ -n "$sys_pool" ]]; then
# Skip if pool is the root pool or a dataset within it (e.g. rpool/data)
[[ "$pool" == "$sys_pool" || "$pool" == "$sys_pool/"* ]] && return 1
fi
return 0
;;
*)
return 1
;;
esac
}
# ==========================================================
# STORAGE CONFIG READER
# ==========================================================
get_storage_config() {
local storage_id="$1"
awk -v id="$storage_id" '
/^[a-z]+: / { found = ($0 ~ ": "id"$"); next }
found && /^[^ \t]/ { exit }
found { print }
' /etc/pve/storage.cfg
}
# ==========================================================
# DISK DETECTION
# ==========================================================
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
if [[ -n "$real_path" ]] && grep -Fq "$real_path" <<< "$config_data"; then
return 0
fi
for link in /dev/disk/by-id/*; do
[[ -e "$link" ]] || continue
[[ "$(readlink -f "$link" 2>/dev/null)" == "$real_path" ]] || continue
if grep -Fq "$link" <<< "$config_data"; then
return 0
fi
done
return 1
}
disk_used_by_host_storage() {
local disk="$1"
if declare -F _disk_is_host_system_used >/dev/null 2>&1; then
_disk_is_host_system_used "$disk"
return $?
fi
local mounted_disks swap_disks lvm_devices zfs_disks real_path path base_disk
local part fstype part_path
mounted_disks=$(lsblk -ln -o NAME,MOUNTPOINT | awk '$2!="" {print "/dev/" $1}')
swap_disks=$(swapon --noheadings --raw --show=NAME 2>/dev/null)
lvm_devices=$(pvs --noheadings -o pv_name 2>/dev/null | xargs -r -n1 readlink -f | sort -u)
zfs_disks=""
while read -r part fstype; do
[[ -z "$part" ]] && continue
part_path="/dev/$part"
if grep -qFx "$part_path" <<< "$mounted_disks"; then
return 0
fi
if grep -qFx "$part_path" <<< "$swap_disks"; then
return 0
fi
case "$fstype" in
zfs_member|linux_raid_member|LVM2_member)
return 0
;;
esac
done < <(lsblk -ln -o NAME,FSTYPE "$disk" 2>/dev/null)
while read -r entry; do
[[ -z "$entry" ]] && continue
path=""
if [[ "$entry" == wwn-* || "$entry" == ata-* ]]; then
[[ -e "/dev/disk/by-id/$entry" ]] && path=$(readlink -f "/dev/disk/by-id/$entry")
elif [[ "$entry" == /dev/* ]]; then
path="$entry"
fi
if [[ -n "$path" ]]; then
base_disk=$(lsblk -no PKNAME "$path" 2>/dev/null)
[[ -n "$base_disk" ]] && zfs_disks+="/dev/$base_disk"$'\n'
fi
done < <(zpool list -v -H 2>/dev/null | awk '{print $1}' | grep -v '^NAME$' | grep -v '^-' | grep -v '^mirror')
real_path=$(readlink -f "$disk" 2>/dev/null)
if [[ -n "$real_path" && -n "$lvm_devices" ]] && grep -qFx "$real_path" <<< "$lvm_devices"; then
return 0
fi
if [[ -n "$zfs_disks" ]] && grep -qFx "$disk" <<< "$(echo "$zfs_disks" | sort -u)"; then
return 0
fi
return 1
}
get_disk_info() {
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="$(translate "Unknown model")"
[[ -z "$size" ]] && size="$(translate "Unknown size")"
printf '%s\t%s\n' "$model" "$size"
}
get_available_disks() {
if declare -F _refresh_host_storage_cache >/dev/null 2>&1; then
_refresh_host_storage_cache
fi
while read -r disk ro type; do
[[ -z "$disk" ]] && continue
[[ "$type" != "disk" ]] && continue
[[ "$ro" == "1" ]] && continue
[[ "$disk" =~ ^/dev/zd ]] && continue
if disk_used_by_host_storage "$disk"; then
continue
fi
if disk_referenced_in_guest_configs "$disk"; then
continue
fi
local model size
IFS=$'\t' read -r model size < <(get_disk_info "$disk")
[[ -z "$model" || "$model" == " " ]] && model="-"
echo "$disk|$size$model"
done < <(lsblk -dn -e 7,11 -o PATH,RO,TYPE 2>/dev/null)
}
select_disk() {
local disk_list
disk_list=$(get_available_disks)
if [[ -z "$disk_list" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Disks Found")" \
--msgbox "\n$(translate "No available disks found.")\n\n$(translate "All disks may already be in use or mounted.")" 10 60
return 1
fi
local options=()
while IFS='|' read -r device info; do
[[ -n "$device" ]] && options+=("$device" "$info")
done <<< "$disk_list"
if [[ ${#options[@]} -eq 0 ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Disks Found")" \
--msgbox "\n$(translate "No suitable disks found.")" 8 60
return 1
fi
SELECTED_DISK=$(dialog --backtitle "ProxMenux" --title "$(translate "Select Disk")" \
--menu "\n$(translate "Select the disk to add as Proxmox storage:")\n$(translate "WARNING: All data on selected disk will be ERASED if formatted.")" \
20 84 10 "${options[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED_DISK" ]] && return 1
return 0
}
inspect_disk() {
local disk="$1"
# Check existing partitions/filesystem
local partition_info
partition_info=$(lsblk -no NAME,SIZE,FSTYPE,MOUNTPOINT "$disk" 2>/dev/null | tail -n +2)
local existing_fs existing_node
existing_fs=$(blkid -s TYPE -o value "$disk" 2>/dev/null || true)
existing_node="$disk"
if [[ -z "$existing_fs" ]]; then
while read -r node fstype mountpoint; do
[[ -z "$node" || -z "$fstype" ]] && continue
[[ -n "$mountpoint" ]] && continue
existing_fs="$fstype"
existing_node="$node"
break
done < <(lsblk -lnpo NAME,FSTYPE,MOUNTPOINT "$disk" 2>/dev/null | awk 'NR>1 {print $1, $2, $3}')
fi
DISK_HAS_DATA=false
DISK_EXISTING_FS=""
DISK_EXISTING_NODE=""
if [[ -n "$partition_info" || -n "$existing_fs" ]]; then
DISK_HAS_DATA=true
DISK_EXISTING_FS="$existing_fs"
DISK_EXISTING_NODE="$existing_node"
fi
return 0
}
select_partition_action() {
local disk="$1"
inspect_disk "$disk"
local disk_size
disk_size=$(lsblk -ndo SIZE "$disk" 2>/dev/null)
local menu_items=()
menu_items+=("format" "$(translate "Format disk (ERASE all data)")")
[[ -n "$DISK_EXISTING_FS" ]] && menu_items+=("use_existing" "$(translate "Use existing filesystem")")
menu_items+=("cancel" "$(translate "Cancel")")
local menu_text
if [[ "$DISK_HAS_DATA" == "true" ]]; then
menu_text="$(translate "Disk:"): $disk ($disk_size)\n"
[[ -n "$DISK_EXISTING_FS" ]] && menu_text+="$(translate "Existing filesystem:"): $DISK_EXISTING_FS\n"
menu_text+="\n$(translate "Options:")\n"
menu_text+="$(translate "Format: ERASE all data and create new filesystem")\n"
[[ -n "$DISK_EXISTING_FS" ]] && menu_text+="$(translate "Use existing: mount without formatting")\n"
menu_text+="\n$(translate "Continue?")"
else
menu_text="$(translate "Disk:"): $disk ($disk_size)\n\n$(translate "Disk appears empty. It will be formatted.")"
fi
DISK_ACTION=$(dialog --backtitle "ProxMenux" --title "$(translate "Disk Setup")" \
--menu "$menu_text" 20 84 8 \
"${menu_items[@]}" 3>&1 1>&2 2>&3)
[[ -z "$DISK_ACTION" || "$DISK_ACTION" == "cancel" ]] && return 1
return 0
}
select_filesystem() {
FILESYSTEM=$(dialog --backtitle "ProxMenux" --title "$(translate "Select Filesystem")" \
--menu "\n$(translate "Choose filesystem for the disk:")" 16 72 5 \
"ext4" "$(translate "ext4 — Proxmox dir storage (recommended)")" \
"xfs" "$(translate "xfs — Proxmox dir storage (large files and VMs)")" \
"btrfs" "$(translate "btrfs — Proxmox dir storage (snapshots, compression)")" \
"zfs" "$(translate "zfs — Proxmox ZFS pool storage")" \
3>&1 1>&2 2>&3)
[[ -z "$FILESYSTEM" ]] && return 1
return 0
}
# ==========================================================
# STORAGE CONFIGURATION
# ==========================================================
configure_disk_storage() {
local disk_name
disk_name=$(basename "$SELECTED_DISK")
STORAGE_ID=$(dialog --backtitle "ProxMenux" --title "$(translate "Storage ID")" \
--inputbox "$(translate "Enter storage ID for Proxmox:")" \
10 60 "disk-${disk_name}" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
[[ -z "$STORAGE_ID" ]] && STORAGE_ID="disk-${disk_name}"
if [[ ! "$STORAGE_ID" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Invalid ID")" \
--msgbox "$(translate "Invalid storage ID. Use only letters, numbers, hyphens and underscores.")" 8 74
return 1
fi
if [[ "${FILESYSTEM:-}" == "zfs" && ! "$STORAGE_ID" =~ ^[a-zA-Z][a-zA-Z0-9_.:-]*$ ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Invalid ID")" \
--msgbox "$(translate "For ZFS, storage ID must start with a letter and use only letters, numbers, dot, dash, underscore or colon.")" 9 86
return 1
fi
MOUNT_PATH="/mnt/${STORAGE_ID}"
MOUNT_PATH=$(dialog --backtitle "ProxMenux" --title "$(translate "Mount Path")" \
--inputbox "$(translate "Enter mount path on host:")" \
10 60 "$MOUNT_PATH" 3>&1 1>&2 2>&3)
[[ $? -ne 0 || -z "$MOUNT_PATH" ]] && return 1
CONTENT_TYPE=$(dialog --backtitle "ProxMenux" --title "$(translate "Content Types")" \
--menu "$(translate "Select content types for this storage:")" 16 70 5 \
"1" "$(translate "VM Storage (images, backup)")" \
"2" "$(translate "Standard NAS (backup, iso, vztmpl)")" \
"3" "$(translate "All types (images, backup, iso, vztmpl, snippets)")" \
"4" "$(translate "Custom")" \
3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
case "$CONTENT_TYPE" in
1) MOUNT_CONTENT="images,backup" ;;
2) MOUNT_CONTENT="backup,iso,vztmpl" ;;
3) MOUNT_CONTENT="images,backup,iso,vztmpl,snippets" ;;
4)
MOUNT_CONTENT=$(dialog --backtitle "ProxMenux" --title "$(translate "Custom Content")" \
--inputbox "$(translate "Enter content types (comma-separated):")" \
10 70 "images,backup" 3>&1 1>&2 2>&3)
[[ $? -ne 0 || -z "$MOUNT_CONTENT" ]] && MOUNT_CONTENT="images,backup"
;;
*) return 1 ;;
esac
return 0
}
# ==========================================================
# DISK SETUP AND MOUNT
# ==========================================================
format_and_mount_disk() {
local disk="$1"
local mount_path="$2"
local filesystem="$3"
# Final confirmation before any destructive operation
local disk_size
disk_size=$(lsblk -ndo SIZE "$disk" 2>/dev/null)
if ! dialog --backtitle "ProxMenux" --title "$(translate "CONFIRM FORMAT")" --yesno \
"$(translate "FINAL CONFIRMATION — DATA WILL BE ERASED")\n\n$(translate "Disk:"): $disk ($disk_size)\n$(translate "Filesystem:"): $filesystem\n$(translate "Mount path:"): $mount_path\n\n$(translate "ALL DATA ON") $disk $(translate "WILL BE PERMANENTLY ERASED.")\n\n$(translate "Are you absolutely sure?")" \
14 80; then
return 1
fi
show_proxmenux_logo
msg_title "$(translate "Add Local Disk as Proxmox Storage")"
local _disk_model _disk_size _disk_label
IFS=$'\t' read -r _disk_model _disk_size < <(get_disk_info "$disk")
_disk_label="$disk"
[[ -n "$_disk_size" && -n "$_disk_model" ]] && _disk_label="$disk$_disk_size$_disk_model"
msg_ok "$(translate "Disk:") ${BL}${_disk_label}${CL}"
msg_ok "$(translate "Action:") $DISK_ACTION"
[[ "$DISK_ACTION" == "format" ]] && msg_ok "$(translate "Filesystem:") $FILESYSTEM"
msg_ok "$(translate "Mount path:") $MOUNT_PATH"
msg_ok "$(translate "Storage ID:") $STORAGE_ID"
msg_ok "$(translate "Content:") $MOUNT_CONTENT"
msg_info "$(translate "Wiping existing partition table...")"
doh_wipe_disk "$disk"
msg_ok "$(translate "Partition table wiped")"
msg_info "$(translate "Creating partition...")"
if ! doh_create_partition "$disk"; then
msg_error "$(translate "Failed to create partition table")"
[[ -n "$DOH_PARTITION_ERROR_DETAIL" ]] && \
msg_error "$(translate "Details"): $(printf '%s' "$DOH_PARTITION_ERROR_DETAIL" | head -n1)"
return 1
fi
msg_ok "$(translate "Partition created")"
local partition="$DOH_CREATED_PARTITION"
# ZFS pre-flight checks (pool existence must be verified before format)
if [[ "$filesystem" == "zfs" ]]; then
if ! command -v zpool >/dev/null 2>&1; then
msg_error "$(translate "zpool command not found. Install zfsutils-linux and retry.")"
return 1
fi
if zpool list "$STORAGE_ID" >/dev/null 2>&1; then
msg_error "$(translate "A ZFS pool with this name already exists:") $STORAGE_ID"
return 1
fi
fi
msg_info "$(translate "Formatting as") $filesystem..."
if ! doh_format_partition "$partition" "$filesystem" "$STORAGE_ID" "$STORAGE_ID" "$mount_path"; then
msg_error "$(translate "Failed to format disk as") $filesystem"
return 1
fi
msg_ok "$(translate "Disk formatted as") $filesystem"
DISK_PARTITION="$partition"
return 0
}
mount_disk_permanently() {
local partition="$1"
local mount_path="$2"
local filesystem="$3"
if [[ "$filesystem" == "zfs" ]]; then
if ! zpool list "$STORAGE_ID" >/dev/null 2>&1; then
msg_error "$(translate "ZFS pool is not available after creation:") $STORAGE_ID"
return 1
fi
msg_ok "$(translate "ZFS pool created and mounted at") $mount_path"
return 0
fi
msg_info "$(translate "Creating mount point...")"
if ! mkdir -p "$mount_path"; then
msg_error "$(translate "Failed to create mount point:") $mount_path"
return 1
fi
msg_ok "$(translate "Mount point created")"
msg_info "$(translate "Mounting disk...")"
if ! mount -t "$filesystem" "$partition" "$mount_path" 2>/dev/null; then
msg_error "$(translate "Failed to mount disk")"
return 1
fi
msg_ok "$(translate "Disk mounted at") $mount_path"
msg_info "$(translate "Adding to /etc/fstab for permanent mounting...")"
local disk_uuid
disk_uuid=$(blkid -s UUID -o value "$partition" 2>/dev/null)
if [[ -n "$disk_uuid" ]]; then
# Remove any existing fstab entry for this UUID or mount point
sed -i "\|UUID=$disk_uuid|d" /etc/fstab
sed -i "\|[[:space:]]${mount_path}[[:space:]]|d" /etc/fstab
echo "UUID=$disk_uuid $mount_path $filesystem defaults,nofail 0 2" >> /etc/fstab
msg_ok "$(translate "Added to /etc/fstab using UUID")"
else
sed -i "\|[[:space:]]${mount_path}[[:space:]]|d" /etc/fstab
echo "$partition $mount_path $filesystem defaults,nofail 0 2" >> /etc/fstab
msg_ok "$(translate "Added to /etc/fstab using device path")"
fi
systemctl daemon-reload 2>/dev/null || true
return 0
}
mount_existing_disk() {
local disk="$1"
local mount_path="$2"
local existing_fs
existing_fs=$(blkid -s TYPE -o value "$disk" 2>/dev/null || true)
if [[ -z "$existing_fs" ]]; then
msg_error "$(translate "Cannot detect filesystem on") $disk"
return 1
fi
msg_info "$(translate "Creating mount point...")"
mkdir -p "$mount_path"
msg_ok "$(translate "Mount point created")"
msg_info "$(translate "Mounting existing") $existing_fs $(translate "filesystem...")"
if ! mount "$disk" "$mount_path" 2>/dev/null; then
msg_error "$(translate "Failed to mount disk")"
return 1
fi
msg_ok "$(translate "Disk mounted at") $mount_path"
# Add to fstab
local disk_uuid
disk_uuid=$(blkid -s UUID -o value "$disk" 2>/dev/null)
if [[ -n "$disk_uuid" ]]; then
sed -i "\|UUID=$disk_uuid|d" /etc/fstab
sed -i "\|[[:space:]]${mount_path}[[:space:]]|d" /etc/fstab
echo "UUID=$disk_uuid $mount_path $existing_fs defaults,nofail 0 2" >> /etc/fstab
msg_ok "$(translate "Added to /etc/fstab")"
fi
DISK_PARTITION="$disk"
systemctl daemon-reload 2>/dev/null || true
return 0
}
add_proxmox_dir_storage() {
local storage_id="$1"
local path="$2"
local content="$3"
local storage_kind="dir"
local pool_name="$storage_id"
if [[ "${FILESYSTEM:-}" == "zfs" ]]; then
storage_kind="zfspool"
fi
if ! command -v pvesm >/dev/null 2>&1; then
msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")"
return 1
fi
if pvesm status "$storage_id" >/dev/null 2>&1; then
msg_warn "$(translate "Storage ID already exists:") $storage_id"
if ! dialog --backtitle "ProxMenux" --title "$(translate "Storage Exists")" --yesno \
"$(translate "Storage ID already exists. Do you want to remove and recreate it?")" \
8 60; then
return 0
fi
pvesm remove "$storage_id" 2>/dev/null || true
fi
msg_info "$(translate "Registering disk as Proxmox storage...")"
local pvesm_output
local add_ok=false
if [[ "$storage_kind" == "zfspool" ]]; then
if pvesm_output=$(pvesm add zfspool "$storage_id" \
--pool "$pool_name" \
--content "$content" 2>&1); then
add_ok=true
fi
else
if pvesm_output=$(pvesm add dir "$storage_id" \
--path "$path" \
--content "$content" 2>&1); then
add_ok=true
fi
fi
if [[ "$add_ok" == "true" ]]; then
if [[ "$storage_kind" == "zfspool" ]]; then
msg_ok "$(translate "ZFS storage added successfully to Proxmox!")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}"
echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}"
echo -e "${TAB}${BGN}$(translate "Type:")${CL} ${BL}zfspool${CL}"
echo -e "${TAB}${BGN}$(translate "Pool:")${CL} ${BL}$pool_name${CL}"
echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}"
else
msg_ok "$(translate "Directory storage added successfully to Proxmox!")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Storage Added:")${CL}"
echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}"
echo -e "${TAB}${BGN}$(translate "Path:")${CL} ${BL}$path${CL}"
echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}"
fi
echo -e ""
msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")"
return 0
else
msg_error "$(translate "Failed to add storage to Proxmox.")"
echo -e "${TAB}$(translate "Error details:"): $pvesm_output"
echo -e ""
msg_info2 "$(translate "You can add it manually through:")"
if [[ "$storage_kind" == "zfspool" ]]; then
echo -e "${TAB}$(translate "Proxmox web interface: Datacenter > Storage > Add > ZFS")"
echo -e "${TAB}• pvesm add zfspool $storage_id --pool $pool_name --content $content"
else
echo -e "${TAB}$(translate "Proxmox web interface: Datacenter > Storage > Add > Directory")"
echo -e "${TAB}• pvesm add dir $storage_id --path $path --content $content"
fi
return 1
fi
}
# ==========================================================
# MAIN OPERATIONS
# ==========================================================
add_disk_to_proxmox() {
# Check required tools
for tool in parted mkfs.ext4 mkfs.xfs blkid lsblk sgdisk; do
if ! command -v "$tool" >/dev/null 2>&1; then
show_proxmenux_logo
msg_title "$(translate "Add Local Disk as Proxmox Storage")"
msg_info "$(translate "Installing required tools...")"
apt-get update &>/dev/null
apt-get install -y parted e2fsprogs util-linux xfsprogs gdisk btrfs-progs &>/dev/null
stop_spinner
break
fi
done
# Step 1: Select disk
select_disk || return
# Step 2: Inspect and choose action
select_partition_action "$SELECTED_DISK" || return
# Step 3: Filesystem selection (only if formatting)
if [[ "$DISK_ACTION" == "format" ]]; then
select_filesystem || return
if [[ "$FILESYSTEM" == "zfs" ]] && ! command -v zpool >/dev/null 2>&1; then
msg_error "$(translate "zpool not found. Install zfsutils-linux and retry.")"
echo
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
fi
# Step 4: Configure storage options
configure_disk_storage || return
if declare -F _refresh_host_storage_cache >/dev/null 2>&1; then
_refresh_host_storage_cache
fi
if disk_used_by_host_storage "$SELECTED_DISK"; then
msg_error "$(translate "Safety check failed: selected disk is now used by host/system.")"
echo
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
if disk_referenced_in_guest_configs "$SELECTED_DISK"; then
msg_error "$(translate "Safety check failed: selected disk is referenced by a VM/LXC config.")"
echo
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
fi
# Step 5: Format/mount
case "$DISK_ACTION" in
format)
format_and_mount_disk "$SELECTED_DISK" "$MOUNT_PATH" "$FILESYSTEM" || {
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
}
mount_disk_permanently "$DISK_PARTITION" "$MOUNT_PATH" "$FILESYSTEM" || {
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
}
;;
use_existing)
local existing_node
existing_node="${DISK_EXISTING_NODE:-$SELECTED_DISK}"
mount_existing_disk "$existing_node" "$MOUNT_PATH" || {
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return 1
}
;;
esac
# Step 6: Register in Proxmox
add_proxmox_dir_storage "$STORAGE_ID" "$MOUNT_PATH" "$MOUNT_CONTENT"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
view_disk_storages() {
show_proxmenux_logo
msg_title "$(translate "Local Disk Storages in Proxmox")"
echo "=================================================="
echo ""
if ! command -v pvesm >/dev/null 2>&1; then
msg_error "$(translate "pvesm not found.")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return
fi
# Show local storages managed by this menu (Directory + ZFS Pool), excluding system ones
DIR_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "dir" || $2 == "zfspool" {print $1, $2, $3}')
local user_storage_found=false
if [[ -n "$DIR_STORAGES" ]]; then
while IFS=" " read -r s_id s_type _; do
[[ -z "$s_id" ]] && continue
_is_user_disk_storage "$s_id" "$s_type" || continue
user_storage_found=true
break
done <<< "$DIR_STORAGES"
fi
if [[ "$user_storage_found" == "false" ]]; then
msg_warn "$(translate "No local storage configured in Proxmox.")"
echo ""
msg_info2 "$(translate "Use option 1 to add a local disk as Proxmox storage.")"
else
echo -e "${BOLD}$(translate "Local Storages:")${CL}"
echo ""
while IFS=" " read -r storage_id storage_type storage_status; do
[[ -z "$storage_id" ]] && continue
_is_user_disk_storage "$storage_id" "$storage_type" || continue
local storage_info path content pool
storage_info=$(get_storage_config "$storage_id")
path=$(echo "$storage_info" | awk '$1 == "path" {print $2}')
pool=$(echo "$storage_info" | awk '$1 == "pool" {print $2}')
content=$(echo "$storage_info" | awk '$1 == "content" {print $2}')
local disk_device=""
if [[ -n "$path" ]]; then
disk_device=$(findmnt -n -o SOURCE "$path" 2>/dev/null || true)
fi
local disk_size=""
if [[ -n "$disk_device" ]]; then
disk_size=$(lsblk -ndo SIZE "$disk_device" 2>/dev/null || true)
fi
echo -e "${TAB}${BOLD}$storage_id${CL}"
echo -e "${TAB} ${BGN}$(translate "Type:")${CL} ${BL}${storage_type:-unknown}${CL}"
echo -e "${TAB} ${BGN}$(translate "Path:")${CL} ${BL}$path${CL}"
[[ -n "$pool" ]] && echo -e "${TAB} ${BGN}$(translate "Pool:")${CL} ${BL}$pool${CL}"
[[ -n "$disk_device" ]] && echo -e "${TAB} ${BGN}$(translate "Device:")${CL} ${BL}$disk_device${CL}"
[[ -n "$disk_size" ]] && echo -e "${TAB} ${BGN}$(translate "Size:")${CL} ${BL}$disk_size${CL}"
echo -e "${TAB} ${BGN}$(translate "Content:")${CL} ${BL}$content${CL}"
if [[ "$storage_status" == "active" ]]; then
echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${GN}$(translate "Active")${CL}"
else
echo -e "${TAB} ${BGN}$(translate "Status:")${CL} ${RD}$storage_status${CL}"
fi
echo ""
done <<< "$DIR_STORAGES"
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
_remove_pvesm_storage() {
local storage_id="$1"
local path pool content stype
path=$(get_storage_config "$storage_id" | awk '$1 == "path" {print $2}')
pool=$(get_storage_config "$storage_id" | awk '$1 == "pool" {print $2}')
content=$(get_storage_config "$storage_id" | awk '$1 == "content" {print $2}')
stype=$(pvesm status 2>/dev/null | awk -v id="$storage_id" '$1==id {print $2}')
local msg
msg="$(translate "WARNING: You are about to remove this Proxmox storage:")\n\n"
msg+=" $(translate "Storage ID:") $storage_id\n"
msg+=" $(translate "Type:") ${stype:-unknown}\n"
[[ -n "$path" ]] && msg+=" $(translate "Mount path:") $path\n"
[[ -n "$pool" ]] && msg+=" $(translate "ZFS pool:") $pool\n"
[[ -n "$content" ]] && msg+=" $(translate "Content:") $content\n"
msg+="\n$(translate "⚠ Disk data will NOT be erased.")\n"
[[ -n "$path" ]] && msg+="$(translate "⚠ Disk will be unmounted and removed from /etc/fstab.")\n"
[[ -n "$pool" ]] && msg+="$(translate "⚠ ZFS pool stays active — run 'zpool export $pool' to detach.")\n"
msg+="\n$(translate "Continue?")"
if ! dialog --backtitle "ProxMenux" --title "$(translate "Confirm Remove")" --yesno "$msg" 22 84; then
return
fi
show_proxmenux_logo
msg_title "$(translate "Remove Disk Storage")"
# Step 1: Remove from Proxmox
msg_info "$(translate "Removing storage from Proxmox...")"
if ! pvesm remove "$storage_id" 2>/dev/null; then
msg_error "$(translate "Failed to remove storage from Proxmox.")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return
fi
msg_ok "$(translate "Storage") $storage_id $(translate "removed from Proxmox")"
# Step 2: Unmount if mounted (dir-backed storages only)
if [[ -n "$path" ]] && mountpoint -q "$path" 2>/dev/null; then
msg_info "$(translate "Unmounting disk...")"
if umount "$path" 2>/dev/null; then
msg_ok "$(translate "Disk unmounted from") $path"
else
msg_warn "$(translate "Could not unmount") $path $(translate "— disk may be busy. Skipping fstab removal.")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
return
fi
fi
# Step 3: Remove /etc/fstab entry
if [[ -n "$path" ]] && grep -q "[[:space:]]${path}[[:space:]]" /etc/fstab 2>/dev/null; then
msg_info "$(translate "Removing from /etc/fstab...")"
local tmp
tmp=$(mktemp)
awk -v mp="$path" '$2 != mp' /etc/fstab > "$tmp" && mv "$tmp" /etc/fstab
systemctl daemon-reload 2>/dev/null || true
msg_ok "$(translate "Removed from /etc/fstab")"
fi
# Step 3b: Export ZFS pool if applicable
if [[ -n "$pool" ]] && zpool list "$pool" >/dev/null 2>&1; then
msg_info "$(translate "Exporting ZFS pool...") $pool"
if zpool export "$pool" 2>/dev/null; then
msg_ok "$(translate "ZFS pool exported:") $pool"
else
msg_warn "$(translate "Could not export ZFS pool") $pool $(translate "— pool may be busy. Run manually: zpool export $pool")"
fi
fi
# Step 4: Reboot prompt
echo ""
if whiptail --title "$(translate "Reboot Required")" --yesno \
"\n$(translate "The storage has been removed and the disk unmounted.")\n\n$(translate "A server reboot is recommended for all changes to take full effect.")\n\n$(translate "Reboot now?")" \
14 72; then
msg_success "$(translate "Press Enter to continue...")"
read -r
echo ""
msg_warn "$(translate "Rebooting the system...")"
reboot
else
echo ""
msg_info2 "$(translate "Reboot pending — changes will take full effect after the next restart.")"
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
_remove_fstab_entry() {
local mount_point="$1"
local fs fstype
while IFS= read -r line; do
[[ "$line" =~ ^# || -z "$line" ]] && continue
local f mp ft
read -r f mp ft _ <<< "$line"
if [[ "$mp" == "$mount_point" ]]; then
fs="$f"; fstype="$ft"; break
fi
done < /etc/fstab
local device="$fs"
if [[ "$fs" == UUID=* ]]; then
device=$(blkid -U "${fs#UUID=}" 2>/dev/null || echo "$fs")
fi
local size=""
[[ -b "$device" ]] && size=$(lsblk -ndo SIZE "$device" 2>/dev/null)
local mounted=false
findmnt -n "$mount_point" >/dev/null 2>&1 && mounted=true
local msg
msg="$(translate "WARNING: You are about to remove this disk mount:")\n\n"
msg+=" $(translate "Mount point:") $mount_point\n"
[[ -n "$device" && "$device" != "$fs" ]] && msg+=" $(translate "Device:") $device\n"
msg+=" $(translate "Filesystem:") $fstype\n"
[[ -n "$size" ]] && msg+=" $(translate "Size:") $size\n"
local mounted_label; $mounted && mounted_label="$(translate "Yes")" || mounted_label="$(translate "No")"
msg+=" $(translate "Currently mounted:") $mounted_label\n"
msg+="\n$(translate "⚠ The disk will be unmounted.")\n"
msg+="$(translate "⚠ The /etc/fstab entry will be removed.")\n"
msg+="$(translate "⚠ Disk data will NOT be erased.")\n"
msg+="\n$(translate "Continue?")"
if dialog --backtitle "ProxMenux" --title "$(translate "Confirm Remove")" --yesno "$msg" 20 80; then
show_proxmenux_logo
msg_title "$(translate "Remove Disk from fstab")"
if $mounted; then
msg_info "$(translate "Unmounting") $mount_point..."
if umount "$mount_point" 2>/dev/null; then
msg_ok "$(translate "Unmounted successfully")"
else
msg_warn "$(translate "Could not unmount — disk may be busy. Removing fstab entry anyway.")"
fi
fi
msg_info "$(translate "Removing from /etc/fstab...")"
local tmp
tmp=$(mktemp)
awk -v mp="$mount_point" '$2 != mp' /etc/fstab > "$tmp"
mv "$tmp" /etc/fstab
systemctl daemon-reload 2>/dev/null || true
msg_ok "$(translate "Removed from /etc/fstab")"
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
fi
}
remove_disk_storage() {
if ! command -v pvesm >/dev/null 2>&1; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
--msgbox "\n$(translate "pvesm not found.")" 8 60
return
fi
local OPTIONS=()
local pvesm_paths=()
# --- Source 1: pvesm user-created storages ---
local ALL_STORAGES
ALL_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "dir" || $2 == "zfspool" {print $1, $2}')
while IFS= read -r line; do
local storage_id storage_type
storage_id=$(echo "$line" | awk '{print $1}')
storage_type=$(echo "$line" | awk '{print $2}')
[[ -z "$storage_id" ]] && continue
_is_user_disk_storage "$storage_id" "$storage_type" || continue
local path pool
path=$(get_storage_config "$storage_id" | awk '$1 == "path" {print $2}')
pool=$(get_storage_config "$storage_id" | awk '$1 == "pool" {print $2}')
[[ -n "$path" ]] && pvesm_paths+=("$path")
local label="[pvesm] ${storage_type}"
[[ -n "$path" ]] && label+="$path"
[[ -z "$path" && -n "$pool" ]] && label+=" — pool: $pool"
OPTIONS+=("pvesm:$storage_id" "$label")
done <<< "$ALL_STORAGES"
# --- Source 2: fstab /mnt/ entries not already covered by pvesm ---
while IFS= read -r line; do
[[ "$line" =~ ^# || -z "$line" ]] && continue
local fs mp fstype
read -r fs mp fstype _ <<< "$line"
[[ "$mp" == /mnt/* ]] || continue
[[ "$fstype" == "proc" || "$fstype" == "sysfs" || "$fstype" == "tmpfs" || "$fstype" == "devtmpfs" || "$fstype" == "none" ]] && continue
local covered=false
for p in "${pvesm_paths[@]}"; do [[ "$p" == "$mp" ]] && covered=true && break; done
$covered && continue
local device="$fs"
[[ "$fs" == UUID=* ]] && device=$(blkid -U "${fs#UUID=}" 2>/dev/null || echo "$fs")
local size=""
[[ -b "$device" ]] && size=$(lsblk -ndo SIZE "$device" 2>/dev/null)
local label="[fstab] $mp ($fstype)"
[[ -n "$size" ]] && label+=" [$size]"
findmnt -n "$mp" >/dev/null 2>&1 && label+=" ✓" || label+=" (not mounted)"
OPTIONS+=("fstab:$mp" "$label")
done < /etc/fstab
if [[ ${#OPTIONS[@]} -eq 0 ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Disk Storage")" \
--msgbox "\n$(translate "No user-created disk storage or fstab mount found.")" 8 68
return
fi
local SELECTED
SELECTED=$(dialog --backtitle "ProxMenux" --title "$(translate "Remove Disk Storage")" \
--menu "$(translate "Select storage to remove:")" 20 88 12 \
"${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED" ]] && return
case "${SELECTED%%:*}" in
pvesm) _remove_pvesm_storage "${SELECTED#pvesm:}" ;;
fstab) _remove_fstab_entry "${SELECTED#fstab:}" ;;
esac
}
list_available_disks() {
show_proxmenux_logo
msg_title "$(translate "Available Disks on Host")"
echo "=================================================="
echo ""
echo -e "${BOLD}$(translate "All block devices:")${CL}"
echo ""
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL 2>/dev/null
echo ""
echo -e "${BOLD}$(translate "Proxmox local storages:")${CL}"
if command -v pvesm >/dev/null 2>&1; then
pvesm status 2>/dev/null | awk '$2 == "dir" || $2 == "zfspool" {print " " $1, $2, $3}' || echo " $(translate "None")"
fi
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
# ==========================================================
# MAIN MENU
# ==========================================================
while true; do
CHOICE=$(dialog --backtitle "ProxMenux" \
--title "$(translate "Local Disk Manager - Proxmox Host")" \
--menu "$(translate "Choose an option:")" 18 70 6 \
"1" "$(translate "Add Local Disk as Proxmox Storage")" \
"2" "$(translate "View Disk Storages")" \
"3" "$(translate "Remove Disk Storage")" \
"4" "$(translate "List Available Disks")" \
"5" "$(translate "Exit")" \
3>&1 1>&2 2>&3)
RETVAL=$?
if [[ $RETVAL -ne 0 ]]; then
exit 0
fi
case $CHOICE in
1) add_disk_to_proxmox ;;
2) view_disk_storages ;;
3) remove_disk_storage ;;
4) list_available_disks ;;
5) exit 0 ;;
*) exit 0 ;;
esac
done