#!/usr/bin/env bash # Copyright (c) 2021-2026 community-scripts ORG # Author: GitHub Copilot # 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."