#!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: MickLesk (CanbiZ) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/refs/heads/main/misc/core.func) source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) 2>/dev/null || true load_functions declare -f init_tool_telemetry &>/dev/null && init_tool_telemetry "storage-share-helper" "pve" set -eEuo pipefail function header_info() { clear cat <<"EOF" _____ __ ___ _________ / ___// /_____ _________ _____ ____ / | / _/ __ \ \__ \/ __/ __ \/ ___/ __ `/ __ `/ _ \ / /| | / // / / / ___/ / /_/ /_/ / / / /_/ / /_/ / __/ / ___ |_/ // /_/ / /____/\__/\____/_/ \__,_/\__, /\___/ /_/ |_/___/_____/ /____/ Proxmox Storage Allrounder SMB | NFS | iSCSI | LVM-on-iSCSI | LXC Mountpoints | Host Shares EOF } pause() { read -r -p "Press Enter to continue..." _ } 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?" 13 100 } read_input() { local title="$1" local prompt="$2" local default_value="${3:-}" whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --inputbox "$prompt" 11 100 "$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 100 3>&1 1>&2 2>&3 } confirm_yes_no() { local title="$1" local prompt="$2" local height="${3:-11}" local width="${4:-100}" whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --yesno "$prompt" "$height" "$width" } # Destructive confirmation: defaults to "No" so a stray Enter never removes anything. confirm_danger() { local title="$1" local prompt="$2" local height="${3:-15}" local width="${4:-100}" whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --defaultno --yesno "$prompt" "$height" "$width" } # Shown before slow data gathering. Goes to stderr: inside $(...) captures, stdout is # swallowed — stderr is the real terminal. show_loading() { echo -e "${YW}Loading... gathering data, this can take a moment (CTRL+C to abort)${CL}" >&2 } # Info popup that stays visible inside $(...) captures: whiptail draws its UI on # stdout, so without 1>&2 the dialog would be captured and the script appears hung. notice_box() { local title="$1" local text="$2" whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --msgbox "$text" 10 80 1>&2 } # Interactive container picker (read-only): returns the chosen CTID on stdout. pick_container() { local title="$1" local -a rows=() local ctid status name label show_loading while IFS=$'\t' read -r ctid status name; do [[ -z "$ctid" ]] && continue label=$(printf '%-58s' "${name:-} [${status}]") rows+=("$ctid" "$label") done < <(pct list 2>/dev/null | awk 'NR>1 {print $1"\t"$2"\t"$NF}') if [[ ${#rows[@]} -eq 0 ]]; then notice_box "$title" "No LXC containers found on this host." return 1 fi whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --menu "Select a container:" 24 80 16 "${rows[@]}" 3>&1 1>&2 2>&3 } # Interactive multi-select picker over ALL mountpoints across ALL containers (read-only). # Returns chosen entries as ":" tokens on stdout. # Reads /etc/pve/lxc/*.conf directly (one 'pct config' per CT is far too slow); # awk stops at the first '[section]' so snapshot mp entries are not listed. pick_all_mountpoints_multi() { local title="$1" local -a rows=() local conf ctid name line key def label show_loading for conf in /etc/pve/lxc/*.conf; do [[ -e "$conf" ]] || continue ctid="$(basename "$conf" .conf)" name=$(awk '/^\[/{exit} $1=="hostname:"{print $2; exit}' "$conf") while IFS= read -r line; do [[ -z "$line" ]] && continue key="${line%%:*}" def="${line#*: }" label=$(printf '%-86s' "CT ${ctid} ${name:-} | ${key}: ${def}") rows+=("${ctid}:${key}" "$label" "OFF") done < <(awk '/^\[/{exit} /^mp[0-9]+:/{print}' "$conf") done if [[ ${#rows[@]} -eq 0 ]]; then notice_box "$title" "No LXC mountpoints found on any container." return 1 fi whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --checklist "Select mountpoint(s) to remove across all containers (Space to toggle, Enter to confirm):" \ 24 112 16 "${rows[@]}" 3>&1 1>&2 2>&3 } # Interactive multi-select storage picker (read-only): returns chosen storage IDs on stdout. pick_storages_multi() { local title="$1" local -a rows=() local stype sid label show_loading while read -r stype sid; do [[ -z "$sid" ]] && continue label=$(printf '%-58s' "[${stype}]") rows+=("$sid" "$label" "OFF") done < <(awk -F': ' '/^[a-z]+: /{print $1, $2}' /etc/pve/storage.cfg 2>/dev/null) if [[ ${#rows[@]} -eq 0 ]]; then notice_box "$title" "No storages found in /etc/pve/storage.cfg." return 1 fi whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --checklist "Select storage(s) to remove (Space to toggle, Enter to confirm):" \ 24 80 16 "${rows[@]}" 3>&1 1>&2 2>&3 } # Interactive multi-select container picker (read-only): returns chosen CTIDs on stdout. pick_containers_multi() { local title="$1" local -a rows=() local ctid status name label show_loading while IFS=$'\t' read -r ctid status name; do [[ -z "$ctid" ]] && continue label=$(printf '%-58s' "${name:-} [${status}]") rows+=("$ctid" "$label" "OFF") done < <(pct list 2>/dev/null | awk 'NR>1 {print $1"\t"$2"\t"$NF}') if [[ ${#rows[@]} -eq 0 ]]; then notice_box "$title" "No LXC containers found on this host." return 1 fi whiptail --backtitle "Proxmox VE Helper Scripts" --title "$title" \ --checklist "Select one or more containers (Space to toggle, Enter to confirm):" \ 24 80 16 "${rows[@]}" 3>&1 1>&2 2>&3 } 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 selection storage_id local -a results=() selection=$(pick_storages_multi "Remove Storage") || return if [[ -z "$selection" ]]; then msg_warn "No storage selected." pause return fi # Confirm each selected storage separately (defaults to No). for storage_id in $selection; do storage_id="${storage_id//\"/}" [[ -z "$storage_id" ]] && continue if confirm_danger "Remove Storage" \ "Really remove storage '${storage_id}' from the Proxmox config? This removes the storage definition only. Data on the underlying share/target is NOT deleted."; then if pvesm remove "$storage_id" >/dev/null 2>&1; then results+=("removed ${storage_id}") else results+=("FAILED ${storage_id}") fi else results+=("skipped ${storage_id}") fi done header_info echo -e "${BL}Remove storage — result${CL}\n" printf ' %s\n' "${results[@]}" echo 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=$(pick_container "LXC: add mountpoint") || 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 [[ ! -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 confirm_yes_no "LXC: add mountpoint" \ "Add mp${mp_slot} to CT ${ctid}? ${host_path} -> ${ct_path}" 13 100 || return 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 selection entry ctid mp_key mp_def local -a results=() selection=$(pick_all_mountpoints_multi "LXC: remove mountpoint") || return if [[ -z "$selection" ]]; then msg_warn "No mountpoint selected." pause return fi # Confirm each selected mountpoint separately (defaults to No). for entry in $selection; do entry="${entry//\"/}" [[ -z "$entry" ]] && continue ctid="${entry%%:*}" mp_key="${entry#*:}" mp_def=$(pct config "$ctid" | awk -F': ' -v k="$mp_key" '$1==k{print $2}') if confirm_danger "LXC: remove mountpoint" \ "Remove ${mp_key} from CT ${ctid}? ${mp_key}: ${mp_def} This only detaches the mountpoint from the container. Data on the host is NOT deleted."; then if pct set "$ctid" -delete "$mp_key" >/dev/null 2>&1; then results+=("removed CT ${ctid} ${mp_key}") else results+=("FAILED CT ${ctid} ${mp_key}") fi else results+=("skipped CT ${ctid} ${mp_key}") fi done header_info echo -e "${BL}Remove mountpoint — result${CL}\n" printf ' %s\n' "${results[@]}" echo pause } list_lxc_mountpoints() { header_info local selection ctid mps selection=$(pick_containers_multi "LXC: list mountpoints") || return if [[ -z "$selection" ]]; then msg_warn "No container selected." pause return fi for ctid in $selection; do ctid="${ctid//\"/}" [[ -z "$ctid" ]] && continue echo -e "${BL}Mountpoints for CT ${ctid}${CL}" mps=$(pct config "$ctid" | awk '/^mp[0-9]+:/{print}') if [[ -n "$mps" ]]; then echo "$mps" else echo " (no mountpoints configured)" fi echo done 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 (read-only actions are safe; write/remove actions ask for confirmation):" 30 116 22 \ " " "────────── READ-ONLY (safe, no changes) ──────────" \ "1" "Test | SMB: manual mount test" \ "2" "Test | NFS: manual mount test" \ "3" "Test | iSCSI: discovery test" \ "4" "Read | LXC: list mountpoints" \ "5" "Read | Show storage/mount/iSCSI status" \ " " "────────── WRITE (modifies config / host) ──────────" \ "6" "Write | Proxmox: add SMB/CIFS storage" \ "7" "Write | Proxmox: add NFS storage" \ "8" "Write | Proxmox: add iSCSI storage" \ "9" "Write | Proxmox: add LVM on base storage (e.g. iSCSI)" \ "10" "Write | LXC: add bind mountpoint (pct set -mpX)" \ "11" "Write | Host: install Samba + create SMB share" \ "12" "Write | Host: install NFS server + create export" \ " " "────────── REMOVE (destructive, defaults to No) ──────────" \ "13" "Remove | Proxmox: remove storage definition" \ "14" "Remove | LXC: remove mountpoint (pct set -delete mpX)" \ "0" "Exit" 3>&1 1>&2 2>&3) || break # '|| true' so a cancelled sub-dialog (whiptail returns non-zero) returns to the # menu instead of tripping 'set -e' and exiting the whole script. case "$choice" in 1) manual_smb_test || true ;; 2) manual_nfs_test || true ;; 3) manual_iscsi_discovery || true ;; 4) list_lxc_mountpoints || true ;; 5) show_status || true ;; 6) add_smb_storage || true ;; 7) add_nfs_storage || true ;; 8) add_iscsi_storage || true ;; 9) add_lvm_on_base_storage || true ;; 10) add_lxc_mountpoint || true ;; 11) host_create_samba_share || true ;; 12) host_create_nfs_export || true ;; 13) remove_storage || true ;; 14) remove_lxc_mountpoint || true ;; 0) break ;; *) ;; esac done } header_info root_check pve_check require_pve ensure_packages confirm_start || exit 0 main_menu header_info msg_ok "Finished."