#!/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