Refactor SMB mount script for clarity and error handling

Refactor SMB mount script to improve readability and error handling. Added cleanup for credential files and enhanced logging for mount operations.
This commit is contained in:
Alexandre
2026-01-06 17:11:28 +01:00
committed by GitHub
parent c41987b514
commit 5fbf733a68

View File

@@ -1,9 +1,10 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=
# shellcheck disable=SC2086,SC2001,SC2015,SC2154
set -e
if ! bashio::supervisor.ping 2> /dev/null; then
if ! bashio::supervisor.ping 2>/dev/null; then
bashio::log.blue "Disabled : please use another method"
exit 0
fi
@@ -14,65 +15,72 @@ bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Ins
# DEFINE FUNCTIONS #
####################
test_mount() {
cleanup_cred() {
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
rm -f "$CRED_FILE" || true
fi
CRED_FILE=""
}
test_mount() {
# Set initial test
MOUNTED=false
ERROR_MOUNT=false
# Exit if not mounted
if ! mountpoint -q /mnt/"$diskname"; then
if ! mountpoint -q "/mnt/$diskname"; then
return 0
fi
# Exit if can't write
[[ -e "/mnt/$diskname/testaze" ]] && rm -r "/mnt/$diskname/testaze"
# shellcheck disable=SC2015
mkdir "/mnt/$diskname/testaze" && touch "/mnt/$diskname/testaze/testaze" && rm -r "/mnt/$diskname/testaze" || ERROR_MOUNT=true
[[ -e "/mnt/$diskname/testaze" ]] && rm -rf "/mnt/$diskname/testaze"
mkdir "/mnt/$diskname/testaze" && touch "/mnt/$diskname/testaze/testaze" && rm -rf "/mnt/$diskname/testaze" || ERROR_MOUNT=true
# Only CIFS has the noserverino fallback
if [[ "$ERROR_MOUNT" == "true" && "$FSTYPE" == "cifs" ]]; then
# Test write permissions
if [[ "$MOUNTOPTIONS" == *"noserverino"* ]]; then
bashio::log.fatal "Disk is mounted, however unable to write in the shared disk. Please check UID/GID for permissions, and if the share is rw"
else
MOUNTOPTIONS="$MOUNTOPTIONS,noserverino"
MOUNTOPTIONS="${MOUNTOPTIONS},noserverino"
echo "... testing with noserverino"
mount_drive "$MOUNTOPTIONS"
return 0
fi
fi
# CRITICAL: for non-CIFS too, do not claim success if mounted but not writable
if [[ "$ERROR_MOUNT" == "true" ]]; then
MOUNTED=false
bashio::log.fatal "Disk is mounted, however unable to write in the shared disk. Please check permissions/export options (rw), and UID/GID mapping."
return 0
fi
# Set correctly mounted bit
MOUNTED=true
return 0
}
mount_drive() {
# Define options
MOUNTED=true
MOUNTOPTIONS="$1"
# Try mounting (type depends on detected FSTYPE)
# Try mounting (type depends on (detected) FSTYPE)
if [[ "$FSTYPE" == "cifs" ]]; then
mount -t cifs -o "$MOUNTOPTIONS" "$disk" /mnt/"$diskname" 2> ERRORCODE || MOUNTED=false
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
elif [[ "$FSTYPE" == "nfs" ]]; then
mount -t nfs -o "$MOUNTOPTIONS" "$disk" /mnt/"$diskname" 2> ERRORCODE || MOUNTED=false
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
fi
# Test if successful
if [[ "$MOUNTED" == "true" ]]; then
# shellcheck disable=SC2015
test_mount
fi
}
####################
########################
# MOUNT NETWORK SHARES #
####################
########################
if bashio::config.has_value 'networkdisks'; then
@@ -83,16 +91,16 @@ if bashio::config.has_value 'networkdisks'; then
bashio::log.warning "------------------------"
fi
echo 'Mounting network share(s)...'
echo "Mounting network share(s)..."
####################
# Define variables #
####################
# Set variables
MOREDISKS=$(bashio::config 'networkdisks')
USERNAME=$(bashio::config 'cifsusername')
PASSWORD=$(bashio::config 'cifspassword')
MOREDISKS="$(bashio::config 'networkdisks')"
USERNAME="$(bashio::config 'cifsusername')"
PASSWORD="$(bashio::config 'cifspassword')"
SMBVERS=""
SECVERS=""
CHARSET=",iocharset=utf8"
@@ -103,15 +111,15 @@ if bashio::config.has_value 'networkdisks'; then
MOREDISKS=${MOREDISKS// /"\040"}
# Is domain set (CIFS only)
DOMAIN=""
DOMAINCLIENT=""
CIFSDOMAIN=""
if bashio::config.has_value 'cifsdomain'; then
echo "... using domain $(bashio::config 'cifsdomain')"
DOMAIN=",domain=$(bashio::config 'cifsdomain')"
DOMAINCLIENT="--workgroup=$(bashio::config 'cifsdomain')"
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
echo "... using domain $CIFSDOMAIN"
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
fi
# Is UID/GID set (used for CIFS mount options)
# UID/GID (used for CIFS mount options)
PUID=",uid=$(id -u)"
PGID=",gid=$(id -g)"
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
@@ -124,15 +132,15 @@ if bashio::config.has_value 'networkdisks'; then
# Mounting disks #
##################
# shellcheck disable=SC2086
for disk in ${MOREDISKS//,/ }; do # Separate comma separated values
for disk in ${MOREDISKS//,/ }; do
CRED_FILE=""
cleanup_cred
# Clean name of network share
# shellcheck disable=SC2116,SC2001
disk=$(echo $disk | sed "s,/$,,") # Remove / at end of name
disk="${disk//"\040"/ }" # replace \040 with space
disk="$(echo "$disk" | sed "s,/$,,")" # Remove trailing /
disk="${disk//"\040"/ }" # replace \040 with space
# Detect filesystem type by pattern (CIFS: //ip/share ; NFS: ip:/export[/path] or nfs://ip:/export[/path])
# Detect filesystem type by pattern
FSTYPE="cifs"
if [[ "$disk" =~ ^nfs:// ]]; then
FSTYPE="nfs"
@@ -143,119 +151,139 @@ if bashio::config.has_value 'networkdisks'; then
# Determine server for reachability checks
if [[ "$FSTYPE" == "cifs" ]]; then
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+")"
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
else
server="${disk%%:*}"
fi
diskname="$disk"
diskname="${diskname//\\//}" # replace \ with /
diskname="${diskname##*/}" # Get only last part of the name
diskname="${diskname##*/}" # keep only last part of the name
# CRITICAL: per-disk error file (avoid collisions / missing file reads)
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
: >"$ERRORCODE_FILE" || true
MOUNTED=false
SMBVERS_FORCE=""
SECVERS_FORCE=""
SMBVERS=""
SECVERS=""
# Start
echo "... mounting ($FSTYPE) $disk"
# Data validation
if [[ "$FSTYPE" == "cifs" ]]; then
if [[ ! "$disk" =~ ^.*+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]+.*+$ ]]; then
bashio::log.fatal "...... the structure of your \"networkdisks\" option : \"$disk\" doesn't seem correct, please use a structure like //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2. If you don't use it, you can simply remove the text, this will avoid this error message in the future."
touch ERRORCODE
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
bashio::log.fatal "...... the structure of your \"networkdisks\" option : \"$disk\" doesn't seem correct, please use a structure like //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2."
echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue
fi
else
if [[ ! "$disk" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:/.+ ]]; then
bashio::log.fatal "...... invalid NFS path \"$disk\". Use a structure like 123.12.12.12:/export/path"
touch ERRORCODE
echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
continue
fi
fi
# Prepare mount point
mkdir -p /mnt/"$diskname"
chown root:root /mnt/"$diskname"
mkdir -p "/mnt/$diskname"
chown root:root "/mnt/$diskname"
# Create credentials file only for CIFS (avoids comma/special-char issues in -o)
if [[ "$FSTYPE" == "cifs" ]]; then
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
chmod 600 "$CRED_FILE"
{
printf 'username=%s\n' "$USERNAME"
printf 'password=%s\n' "$PASSWORD"
if [[ -n "${CIFSDOMAIN:-}" ]]; then
printf 'domain=%s\n' "$CIFSDOMAIN"
fi
} >"$CRED_FILE"
fi
# Quickly try to mount with defaults
if [[ "$FSTYPE" == "cifs" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,username=${USERNAME},password=${PASSWORD},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}${DOMAIN}"
elif [[ "$FSTYPE" == "nfs" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
else
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
fi
# Deeper analysis if failed
if [ "$MOUNTED" = false ]; then
if [[ "$MOUNTED" == "false" ]]; then
if [[ "$FSTYPE" == "cifs" ]]; then
# Does server exist (SMB port 445)
output="$(nmap -F $server -T5 -oG -)"
if ! echo "$output" | grep 445/open &> /dev/null; then
if echo "$output" | grep /open &> /dev/null; then
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
touch ERRORCODE
if command -v nmap >/dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -q "445/open"; then
if echo "$output" | grep -q "/open"; then
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
else
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
fi
cleanup_cred
continue
else
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
touch ERRORCODE
echo "...... $server is confirmed reachable"
fi
else
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
fi
# Are credentials correct (use server, not share path)
if command -v smbclient >/dev/null 2>&1; then
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME"%"$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
if ! smbclient -t 2 -L "$server" -N $DOMAINCLIENT -c "exit" &>/dev/null; then
bashio::log.fatal "...... perhaps a workgroup must be specified"
fi
cleanup_cred
continue
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1"
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
SMBVERS_FORCE=",vers=1.0"
SECVERS_FORCE=",sec=ntlm"
elif ! echo "$OUTPUT" | grep -q "Disk"; then
echo "... testing path"
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
else
echo "...... credentials are valid"
fi
else
echo "...... $server is confirmed reachable"
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
fi
# Are credentials correct
OUTPUT="$(smbclient -t 2 -L "$disk" -U "$USERNAME"%"$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
touch ERRORCODE
# Should there be a workgroup
if ! smbclient -t 2 -L $disk -N $DOMAINCLIENT -c "exit" &> /dev/null; then
bashio::log.fatal "...... perhaps a workgroup must be specified"
touch ERRORCODE
fi
continue
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
echo "... using SMBv1"
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
SMBVERS_FORCE=",vers=1.0"
SECVERS_FORCE=",sec=ntlm"
elif ! echo "$OUTPUT" | grep -q "Disk"; then
echo "... testing path"
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
else
echo "...... credentials are valid"
# Extract SMB dialect from nmap and map to mount.cifs vers=
SMBRAW=""
if command -v nmap >/dev/null 2>&1; then
SMBRAW="$(
nmap --script smb-protocols -p 445 "$server" 2>/dev/null \
| awk '/SMB2_DIALECT_/ {print $NF}' \
| sed 's/SMB2_DIALECT_//' \
| tr -d '_' \
| sort -V | tail -n 1 || true
)"
fi
# Extracting SMB versions and normalize output
# shellcheck disable=SC2210,SC2094
SMBVERS="$(nmap --script smb-protocols "$server" -p 445 2> 1 | awk '/ [0-9]/' | awk '{print $NF}' | cut -c -3 | sort -V | tail -n 1 || true)"
# Avoid :
SMBVERS="${SMBVERS/:/.}"
# Manage output
if [ -n "$SMBVERS" ]; then
case $SMBVERS in
"202" | "200" | "20")
SMBVERS="2.0"
;;
21)
SMBVERS="2.1"
;;
302)
SMBVERS="3.02"
;;
311)
SMBVERS="3.1.1"
;;
"3.1")
echo "SMB 3.1 detected, converting to 3.0"
SMBVERS="3.0"
;;
esac
echo "...... SMB version detected : $SMBVERS"
SMBVERS=",vers=$SMBVERS"
elif smbclient -t 2 -L "$server" -m NT1 -N $DOMAINCLIENT &> /dev/null; then
SMBVERS=""
case "$SMBRAW" in
311) SMBVERS=",vers=3.1.1" ;;
302) SMBVERS=",vers=3.02" ;;
300) SMBVERS=",vers=3.0" ;;
210) SMBVERS=",vers=2.1" ;;
202|200) SMBVERS=",vers=2.0" ;;
*) SMBVERS="" ;;
esac
if [[ -n "$SMBVERS" ]]; then
echo "...... SMB version detected : ${SMBVERS#,vers=}"
elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -N $DOMAINCLIENT &>/dev/null; then
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
SECVERS=",sec=ntlm"
SMBVERS=",vers=1.0"
@@ -264,94 +292,95 @@ if bashio::config.has_value 'networkdisks'; then
SMBVERS=""
fi
# Apply forced SMBv1 options when initial connection required NT1 fallback
# Apply forced SMBv1 options when needed
if [[ -n "$SMBVERS_FORCE" ]]; then
if [[ -z "$SMBVERS" ]]; then
SMBVERS="$SMBVERS_FORCE"
fi
if [[ -z "$SECVERS" ]]; then
SECVERS="$SECVERS_FORCE"
fi
[[ -z "$SMBVERS" ]] && SMBVERS="$SMBVERS_FORCE"
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
fi
# Ensure the Samba client allows SMBv1 when those options are required
# Ensure Samba client allows SMBv1 when required
if [[ "${SMBVERS}${SMBVERS_FORCE}" == *"vers=1.0"* ]]; then
if [[ -f /etc/samba/smb.conf ]]; then
bashio::log.warning "...... enabling SMBv1 support in Samba client configuration"
sed -i '/\[global\]/!b;n;/client min protocol = NT1/!a\
client min protocol = NT1' /etc/samba/smb.conf
client min protocol = NT1' /etc/samba/smb.conf || true
fi
fi
# Test with different security versions
#######################################
for SECVERS in "$SECVERS" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
if [ "$MOUNTED" = false ]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,username=${USERNAME},password=${PASSWORD},nobrl${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}${DOMAIN}"
# Try with different security modes (do not overwrite SECVERS base accidentally)
SECVERS_BASE="$SECVERS"
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
fi
done
elif [[ "$FSTYPE" == "nfs" ]]; then
# Add NFS-specific port check (2049) similar to SMB (445)
output="$(nmap -F $server -T5 -oG -)"
if ! echo "$output" | grep -E '(2049|111)/open' &> /dev/null; then
bashio::log.fatal "...... $server is reachable but NFS ports not open"
continue
else
# NFS: check ports (111/2049) and try common versions
if command -v nmap >/dev/null 2>&1; then
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
bashio::log.fatal "...... $server is reachable but NFS ports not open"
continue
fi
else
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
fi
# NFS fallback attempts: try common versions until one works
for NFVER in 4.2 4.1 4 3; do
if [ "$MOUNTED" = false ]; then
if [[ "$MOUNTED" == "false" ]]; then
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
fi
done
fi
fi
# Messages
if [ "$MOUNTED" = true ]; then
# Messages / finalization
if [[ "$MOUNTED" == "true" ]]; then
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
# Remove errorcode
if [ -f ERRORCODE ]; then
rm ERRORCODE
fi
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
# Alert if smbv1
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"1.0"* ]]; then
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
bashio::log.warning ""
bashio::log.warning "Your smb system requires smbv1. This is an obsolete protocol. Please correct this to prevent issues."
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
bashio::log.warning ""
fi
cleanup_cred
else
# Mounting failed messages
if [[ "$FSTYPE" == "cifs" ]]; then
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME, $PASSWORD. Please check your remote share path, username, password, domain, try putting 0 in UID and GID"
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check remote share path, username, password, domain; try UID/GID 0."
bashio::log.fatal "Here is some debugging info :"
smbclient -t 2 -L $disk -U "$USERNAME%$PASSWORD" -c "exit"
if command -v smbclient >/dev/null 2>&1; then
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
else
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
fi
# last-ditch try: minimal options (still uses credentials file)
SMBVERS=""
SECVERS=""
PUID=""
PGID=""
CHARSET=""
mount_drive "rw,file_mode=0775,dir_mode=0775,username=${USERNAME},password=${PASSWORD},nobrl${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}${DOMAIN}"
elif [[ "$FSTYPE" == "nfs" ]]; then
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check the export path and that NFS server allows this client (and NFSv4)."
# last-ditch try with very basic options
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
else
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check the export path and that the NFS server allows this client (and NFSv4)."
mount_drive "rw"
fi
bashio::log.fatal "Error read : $(< ERRORCODE), addon will stop in 1 min"
ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
# clean folder
umount "/mnt/$diskname" 2> /dev/null || true
rmdir "/mnt/$diskname" || true
umount "/mnt/$diskname" 2>/dev/null || true
rmdir "/mnt/$diskname" 2>/dev/null || true
cleanup_cred
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
# Stop addon
bashio::addon.stop
fi
done
fi