From 829704e09e20431f291829c3f4a1317ee299094f Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:45:56 +0200 Subject: [PATCH] pve-tool: storage share helper script for Proxmox This script provides a menu-driven interface for managing various storage types in Proxmox, including SMB, NFS, and iSCSI. It includes functions for testing mounts, adding storage, and managing LXC mountpoints. --- tools/pve/storage-share-helper.sh | 517 ++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 tools/pve/storage-share-helper.sh diff --git a/tools/pve/storage-share-helper.sh b/tools/pve/storage-share-helper.sh new file mode 100644 index 000000000..f4931dd16 --- /dev/null +++ b/tools/pve/storage-share-helper.sh @@ -0,0 +1,517 @@ +#!/usr/bin/env bash +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE + +set -eEuo pipefail + +function header_info() { + clear + cat <<"EOF" + _____ __ ___ _________ + / ___// /_____ _________ _____ ____ / | / _/ __ \ + \__ \/ __/ __ \/ ___/ __ `/ __ `/ _ \ / /| | / // / / / + ___/ / /_/ /_/ / / / /_/ / /_/ / __/ / ___ |_/ // /_/ / + /____/\__/\____/_/ \__,_/\__, /\___/ /_/ |_/___/_____/ + /____/ + + Proxmox Storage Allrounder + SMB | NFS | iSCSI | LVM-on-iSCSI | LXC Mountpoints | Host Shares +EOF +} + +YW="\033[33m" +BL="\033[36m" +GN="\033[1;92m" +RD="\033[01;31m" +CL="\033[m" + +msg_info() { echo -e "${BL}[INFO]${CL} ${YW}$1${CL}"; } +msg_ok() { echo -e "${GN}[OK]${CL} $1"; } +msg_warn() { echo -e "${YW}[WARN]${CL} $1"; } +msg_error() { echo -e "${RD}[ERROR]${CL} $1"; } + +pause() { + read -r -p "Press Enter to continue..." _ +} + +require_root() { + if [[ $EUID -ne 0 ]]; then + msg_error "Run this script as root." + exit 1 + fi +} + +require_pve() { + if ! command -v pct >/dev/null 2>&1 || ! command -v pvesm >/dev/null 2>&1; then + msg_error "This script must run on a Proxmox VE host (pct/pvesm missing)." + exit 1 + fi +} + +ensure_packages() { + local packages=() + + command -v whiptail >/dev/null 2>&1 || packages+=("whiptail") + command -v mount.cifs >/dev/null 2>&1 || packages+=("cifs-utils") + command -v showmount >/dev/null 2>&1 || packages+=("nfs-common") + command -v iscsiadm >/dev/null 2>&1 || packages+=("open-iscsi") + + if [[ ${#packages[@]} -gt 0 ]]; then + msg_info "Installing required packages: ${packages[*]}" + apt update >/dev/null 2>&1 + apt install -y "${packages[@]}" >/dev/null 2>&1 + msg_ok "Dependencies installed" + fi +} + +confirm_start() { + whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Share Allrounder" \ + --yesno "This AIO wizard can test and configure SMB/NFS/iSCSI, create/remove Proxmox storages, manage LXC mountpoints and optionally create host shares. Proceed?" 12 96 +} + +read_input() { + local title="$1" + local prompt="$2" + local default_value="${3:-}" + + whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ + --inputbox "$prompt" 11 84 "$default_value" 3>&1 1>&2 2>&3 +} + +read_password() { + local title="$1" + local prompt="$2" + + whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ + --passwordbox "$prompt" 11 84 3>&1 1>&2 2>&3 +} + +confirm_yes_no() { + local title="$1" + local prompt="$2" + + whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ + --yesno "$prompt" 11 84 +} + +manual_smb_test() { + header_info + local server share username password domain vers mount_dir mount_opts + + server=$(read_input "SMB Test" "SMB server/IP (e.g. 10.0.1.9)") || return + share=$(read_input "SMB Test" "Share name (without leading //)" "Proxmox") || return + username=$(read_input "SMB Test" "Username" "proxmox") || return + password=$(read_password "SMB Test" "Password for ${username}") || return + domain=$(read_input "SMB Test" "Domain/Workgroup (optional, leave empty if not needed)") || return + vers=$(read_input "SMB Test" "SMB version (e.g. 3.0, 3.1.1)" "3.1.1") || return + mount_dir="/mnt/test-smb" + + mkdir -p "$mount_dir" + + mount_opts="username=${username},password=${password},vers=${vers},sec=ntlmssp" + if [[ -n "$domain" ]]; then + mount_opts+=";domain=${domain}" + fi + + msg_info "Testing SMB mount on ${mount_dir}" + if mount -t cifs "//${server}/${share}" "$mount_dir" -o "${mount_opts//;/,}" >/dev/null 2>&1; then + touch "${mount_dir}/smb-test-$(date +%s)" >/dev/null 2>&1 || true + umount "$mount_dir" >/dev/null 2>&1 || true + msg_ok "SMB test successful" + else + msg_error "SMB test failed. Check network/firewall/credentials/share permissions." + fi + + pause +} + +manual_nfs_test() { + header_info + local server export_path mount_dir + + server=$(read_input "NFS Test" "NFS server/IP (e.g. 10.0.6.159)") || return + export_path=$(read_input "NFS Test" "NFS export path (e.g. /srv/proxmox-nfs)") || return + mount_dir="/mnt/test-nfs" + + mkdir -p "$mount_dir" + + msg_info "Testing NFS mount on ${mount_dir}" + if mount -t nfs "${server}:${export_path}" "$mount_dir" >/dev/null 2>&1; then + touch "${mount_dir}/nfs-test-$(date +%s)" >/dev/null 2>&1 || true + umount "$mount_dir" >/dev/null 2>&1 || true + msg_ok "NFS test successful" + else + msg_error "NFS test failed. Check export/firewall/network permissions." + fi + + pause +} + +manual_iscsi_discovery() { + header_info + local portal + + portal=$(read_input "iSCSI Discovery" "iSCSI portal IP/FQDN (e.g. 10.0.1.20)") || return + msg_info "Running iSCSI target discovery on ${portal}" + if iscsiadm -m discovery -t sendtargets -p "$portal"; then + msg_ok "iSCSI discovery completed" + else + msg_error "iSCSI discovery failed" + fi + + pause +} + +add_smb_storage() { + header_info + local storage_id server share username password content nodes options + + storage_id=$(read_input "Add SMB/CIFS Storage" "Storage ID (unique, e.g. smb-media)") || return + server=$(read_input "Add SMB/CIFS Storage" "SMB server/IP") || return + share=$(read_input "Add SMB/CIFS Storage" "Share name (without //)") || return + username=$(read_input "Add SMB/CIFS Storage" "Username") || return + password=$(read_password "Add SMB/CIFS Storage" "Password for ${username}") || return + content=$(read_input "Add SMB/CIFS Storage" "Content types (comma separated)" "backup,iso,vztmpl,snippets") || return + nodes=$(read_input "Add SMB/CIFS Storage" "Nodes (optional, comma separated)") || return + options=$(read_input "Add SMB/CIFS Storage" "Mount options (optional, e.g. vers=3.1.1,domain=WORKGROUP)") || return + + local cmd=(pvesm add cifs "$storage_id" --server "$server" --share "$share" --username "$username" --password "$password" --content "$content") + [[ -n "$nodes" ]] && cmd+=(--nodes "$nodes") + [[ -n "$options" ]] && cmd+=(--options "$options") + + if "${cmd[@]}" >/dev/null 2>&1; then + msg_ok "SMB storage '${storage_id}' added" + else + msg_error "Failed to add SMB storage '${storage_id}'" + fi + + pause +} + +add_nfs_storage() { + header_info + local storage_id server export_path content nodes options + + storage_id=$(read_input "Add NFS Storage" "Storage ID (unique, e.g. nfs-vmdata)") || return + server=$(read_input "Add NFS Storage" "NFS server/IP") || return + export_path=$(read_input "Add NFS Storage" "Export path") || return + content=$(read_input "Add NFS Storage" "Content types (comma separated)" "images,rootdir") || return + nodes=$(read_input "Add NFS Storage" "Nodes (optional, comma separated)") || return + options=$(read_input "Add NFS Storage" "Mount options (optional)") || return + + local cmd=(pvesm add nfs "$storage_id" --server "$server" --export "$export_path" --content "$content") + [[ -n "$nodes" ]] && cmd+=(--nodes "$nodes") + [[ -n "$options" ]] && cmd+=(--options "$options") + + if "${cmd[@]}" >/dev/null 2>&1; then + msg_ok "NFS storage '${storage_id}' added" + else + msg_error "Failed to add NFS storage '${storage_id}'" + fi + + pause +} + +add_iscsi_storage() { + header_info + local storage_id portal target nodes + + storage_id=$(read_input "Add iSCSI Storage" "Storage ID (unique, e.g. iscsi-synology)") || return + portal=$(read_input "Add iSCSI Storage" "Portal IP/FQDN") || return + target=$(read_input "Add iSCSI Storage" "Target IQN (e.g. iqn.2000-01.com.synology:...)") || return + nodes=$(read_input "Add iSCSI Storage" "Nodes (optional, comma separated)") || return + + local cmd=(pvesm add iscsi "$storage_id" --portal "$portal" --target "$target") + [[ -n "$nodes" ]] && cmd+=(--nodes "$nodes") + + if "${cmd[@]}" >/dev/null 2>&1; then + msg_ok "iSCSI storage '${storage_id}' added" + else + msg_error "Failed to add iSCSI storage '${storage_id}'" + fi + + pause +} + +add_lvm_on_base_storage() { + header_info + local storage_id base_storage vgname content shared + + storage_id=$(read_input "Add LVM Storage" "LVM Storage ID (unique, e.g. lvm-iscsi01)") || return + base_storage=$(read_input "Add LVM Storage" "Base storage ID (usually iSCSI storage ID)") || return + vgname=$(read_input "Add LVM Storage" "Volume Group name on target") || return + content=$(read_input "Add LVM Storage" "Content types (comma separated)" "images,rootdir") || return + shared=$(read_input "Add LVM Storage" "Shared across nodes? 1=yes, 0=no" "1") || return + + local cmd=(pvesm add lvm "$storage_id" --base "$base_storage" --vgname "$vgname" --content "$content" --shared "$shared") + if "${cmd[@]}" >/dev/null 2>&1; then + msg_ok "LVM storage '${storage_id}' added" + else + msg_error "Failed to add LVM storage '${storage_id}'" + fi + + pause +} + +remove_storage() { + header_info + local storage_id + + storage_id=$(read_input "Remove Storage" "Storage ID to remove") || return + confirm_yes_no "Remove Storage" "Really remove storage '${storage_id}' from Proxmox config?" || return + + if pvesm remove "$storage_id" >/dev/null 2>&1; then + msg_ok "Storage '${storage_id}' removed" + else + msg_error "Failed to remove storage '${storage_id}'" + fi + + pause +} + +find_next_mp_slot() { + local ctid="$1" + local used + + used=$(pct config "$ctid" | awk -F: '/^mp[0-9]+:/ {gsub("mp", "", $1); print $1}') + for i in $(seq 0 255); do + if ! grep -qx "$i" <<<"$used"; then + echo "$i" + return + fi + done + + echo "" +} + +add_lxc_mountpoint() { + header_info + local ctid host_path ct_path mp_slot + + ctid=$(read_input "LXC Mountpoint" "Container ID (CTID)") || return + host_path=$(read_input "LXC Mountpoint" "Host path (must exist, e.g. /mnt/pve/smb-media)") || return + ct_path=$(read_input "LXC Mountpoint" "Container path (e.g. /mnt/media)") || return + + if ! pct config "$ctid" >/dev/null 2>&1; then + msg_error "Container ${ctid} not found" + pause + return + fi + + if [[ ! -d "$host_path" ]]; then + msg_error "Host path does not exist: ${host_path}" + pause + return + fi + + mp_slot=$(find_next_mp_slot "$ctid") + if [[ -z "$mp_slot" ]]; then + msg_error "No free mp slot available for container ${ctid}" + pause + return + fi + + if pct set "$ctid" -mp"$mp_slot" "$host_path",mp="$ct_path" >/dev/null 2>&1; then + msg_ok "Added mp${mp_slot}: ${host_path} -> ${ct_path} on CT ${ctid}" + msg_warn "If CT is unprivileged, ensure UID/GID mapping and filesystem permissions fit your workload." + else + msg_error "Failed to add mountpoint to CT ${ctid}" + fi + + pause +} + +remove_lxc_mountpoint() { + header_info + local ctid mp_key + + ctid=$(read_input "Remove LXC Mountpoint" "Container ID (CTID)") || return + mp_key=$(read_input "Remove LXC Mountpoint" "Mountpoint key (e.g. mp0, mp1)") || return + + if ! pct config "$ctid" >/dev/null 2>&1; then + msg_error "Container ${ctid} not found" + pause + return + fi + + if pct set "$ctid" -delete "$mp_key" >/dev/null 2>&1; then + msg_ok "Removed ${mp_key} from CT ${ctid}" + else + msg_error "Failed to remove ${mp_key} from CT ${ctid}" + fi + + pause +} + +list_lxc_mountpoints() { + header_info + local ctid + + ctid=$(read_input "List LXC Mountpoints" "Container ID (CTID)") || return + if ! pct config "$ctid" >/dev/null 2>&1; then + msg_error "Container ${ctid} not found" + pause + return + fi + + echo -e "${BL}Mountpoints for CT ${ctid}${CL}\n" + pct config "$ctid" | awk '/^mp[0-9]+:/{print}' + echo + pause +} + +host_create_samba_share() { + header_info + local share_name share_path user_name user_pass + + share_name=$(read_input "Host Samba Share" "Share name (e.g. data)") || return + share_path=$(read_input "Host Samba Share" "Share path on host (e.g. /srv/samba/data)" "/srv/samba/${share_name}") || return + user_name=$(read_input "Host Samba Share" "Linux/Samba username") || return + user_pass=$(read_password "Host Samba Share" "Password for ${user_name}") || return + + msg_info "Installing Samba on host if needed" + apt update >/dev/null 2>&1 + apt install -y samba >/dev/null 2>&1 + + mkdir -p "$share_path" + getent group sambashare >/dev/null 2>&1 || groupadd sambashare + id "$user_name" >/dev/null 2>&1 || useradd -M -s /usr/sbin/nologin -G sambashare "$user_name" + usermod -aG sambashare "$user_name" + chown -R root:sambashare "$share_path" + chmod 2775 "$share_path" + + if ! ( + echo "$user_pass" + echo "$user_pass" + ) | smbpasswd -s -a "$user_name" >/dev/null 2>&1; then + msg_error "Failed to set Samba password for ${user_name}" + pause + return + fi + + if ! grep -q "^\[${share_name}\]" /etc/samba/smb.conf; then + cat <>/etc/samba/smb.conf + +[${share_name}] + comment = Proxmox Host Share (${share_name}) + path = ${share_path} + browseable = yes + read only = no + guest ok = no + create mask = 0664 + directory mask = 2775 + valid users = @sambashare +EOF + fi + + testparm -s >/dev/null 2>&1 || { + msg_error "Samba config validation failed (testparm). Check /etc/samba/smb.conf" + pause + return + } + + systemctl enable --now smbd nmbd >/dev/null 2>&1 + systemctl restart smbd nmbd >/dev/null 2>&1 + + msg_ok "Host SMB share created: //$(hostname -I | awk '{print $1}')/${share_name}" + msg_warn "Best practice: prefer running Samba in a dedicated LXC/VM for cleaner host separation." + + pause +} + +host_create_nfs_export() { + header_info + local export_path subnet options + + export_path=$(read_input "Host NFS Export" "Export path on host (e.g. /srv/proxmox-nfs)" "/srv/proxmox-nfs") || return + subnet=$(read_input "Host NFS Export" "Allowed subnet/CIDR (e.g. 10.0.0.0/16)") || return + options=$(read_input "Host NFS Export" "Export options" "rw,sync,no_subtree_check,no_root_squash") || return + + msg_info "Installing NFS server on host if needed" + apt update >/dev/null 2>&1 + apt install -y nfs-kernel-server >/dev/null 2>&1 + + mkdir -p "$export_path" + chmod 0770 "$export_path" + + if ! grep -qE "^${export_path//\//\/}[[:space:]]+${subnet//\//\/}\(" /etc/exports; then + echo "${export_path} ${subnet}(${options})" >>/etc/exports + fi + + exportfs -ra >/dev/null 2>&1 + systemctl enable --now nfs-kernel-server >/dev/null 2>&1 + + msg_ok "Host NFS export created: ${export_path} ${subnet}(${options})" + msg_warn "Best practice: use host exports carefully; dedicated storage VM/LXC is often cleaner." + pause +} + +show_status() { + header_info + echo -e "${BL}pvesm status${CL}" + pvesm status || true + echo + echo -e "${BL}Mounted /mnt/pve paths${CL}" + mount | grep /mnt/pve || true + echo + echo -e "${BL}Mounted CIFS/NFS/iSCSI related paths${CL}" + mount | grep -E ' type (cifs|nfs|nfs4)' || true + echo + echo -e "${BL}iSCSI sessions${CL}" + iscsiadm -m session 2>/dev/null || true + echo + pause +} + +main_menu() { + while true; do + local choice + choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Share Allrounder" \ + --menu "Select action:" 30 110 20 \ + "1" "SMB: manual mount test" \ + "2" "NFS: manual mount test" \ + "3" "iSCSI: discovery test" \ + "4" "Proxmox: add SMB/CIFS storage" \ + "5" "Proxmox: add NFS storage" \ + "6" "Proxmox: add iSCSI storage" \ + "7" "Proxmox: add LVM on base storage (e.g. iSCSI)" \ + "8" "Proxmox: remove storage definition" \ + "9" "LXC: add bind mountpoint (pct set -mpX)" \ + "10" "LXC: remove mountpoint (pct set -delete mpX)" \ + "11" "LXC: list mountpoints" \ + "12" "Host: install Samba + create SMB share" \ + "13" "Host: install NFS server + create export" \ + "14" "Show storage/mount/iSCSI status" \ + "0" "Exit" 3>&1 1>&2 2>&3) || break + + case "$choice" in + 1) manual_smb_test ;; + 2) manual_nfs_test ;; + 3) manual_iscsi_discovery ;; + 4) add_smb_storage ;; + 5) add_nfs_storage ;; + 6) add_iscsi_storage ;; + 7) add_lvm_on_base_storage ;; + 8) remove_storage ;; + 9) add_lxc_mountpoint ;; + 10) remove_lxc_mountpoint ;; + 11) list_lxc_mountpoints ;; + 12) host_create_samba_share ;; + 13) host_create_nfs_export ;; + 14) show_status ;; + 0) break ;; + *) ;; + esac + done +} + +header_info +require_root +require_pve +ensure_packages +confirm_start || exit 0 +main_menu + +header_info +msg_ok "Finished."