New secubox-app-smbfs package for SMB/CIFS remote directory management with smbfsctl CLI (add/remove/mount/umount/test/status), UCI config, auto-mount init script, and Jellyfin/Lyrion media path integration. Glances LXC: host bind mounts (/rom, /overlay, /boot, /srv), Docker socket fix (symlink loop), fs plugin @exit_after patch, hostname/OS identity, pre-generated /etc/mtab. KISS READMEs for secubox-app-jellyfin and luci-app-jellyfin. Planning files updated with Domoticz IoT, AI Gateway strategy, App Store P2P emancipation, and v2 roadmap items. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
505 lines
13 KiB
Bash
505 lines
13 KiB
Bash
#!/bin/sh
|
|
# SecuBox SMB/CIFS remote mount manager
|
|
# Copyright (C) 2025-2026 CyberMind.fr
|
|
|
|
CONFIG="smbfs"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: smbfsctl <command> [args]
|
|
|
|
Share Management:
|
|
add <name> <server> <mountpoint> Add a new SMB share
|
|
remove <name> Remove a share definition
|
|
enable <name> Enable auto-mount for a share
|
|
disable <name> Disable auto-mount for a share
|
|
credentials <name> <user> <pass> Set credentials for a share
|
|
set <name> <key> <value> Set a share option
|
|
list List all configured shares
|
|
|
|
Mount Operations:
|
|
mount <name> Mount a specific share
|
|
mount-all Mount all enabled shares
|
|
umount <name> Unmount a specific share
|
|
umount-all Unmount all shares
|
|
status Show mount status of all shares
|
|
test <name> Test connectivity to a share
|
|
|
|
Examples:
|
|
smbfsctl add movies //nas/movies /mnt/smb/movies
|
|
smbfsctl credentials movies user mypass
|
|
smbfsctl set movies read_only 1
|
|
smbfsctl mount movies
|
|
smbfsctl enable movies
|
|
EOF
|
|
}
|
|
|
|
require_root() { [ "$(id -u)" -eq 0 ] || { echo "[ERROR] Root required" >&2; exit 1; }; }
|
|
|
|
log_info() { echo "[INFO] $*"; }
|
|
log_warn() { echo "[WARN] $*" >&2; }
|
|
log_error() { echo "[ERROR] $*" >&2; }
|
|
|
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
|
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
|
|
|
# Load global config
|
|
load_global() {
|
|
mount_base="$(uci_get global.mount_base || echo /mnt/smb)"
|
|
default_vers="$(uci_get global.cifs_version || echo 3.0)"
|
|
timeout="$(uci_get global.timeout || echo 10)"
|
|
}
|
|
|
|
# Get list of configured share section names
|
|
get_shares() {
|
|
uci -q show "$CONFIG" | grep "=${CONFIG}\[" | sed "s/.*\.\(.*\)=.*/\1/" | sort -u
|
|
uci -q show "$CONFIG" | grep "=mount" | sed "s/${CONFIG}\.\(.*\)=mount/\1/" | sort -u
|
|
}
|
|
|
|
# Check if share section exists
|
|
share_exists() {
|
|
local type
|
|
type="$(uci -q get ${CONFIG}.$1)"
|
|
[ "$type" = "mount" ]
|
|
}
|
|
|
|
# Build mount options for a share
|
|
build_mount_opts() {
|
|
local name="$1"
|
|
local username password domain vers ro opts
|
|
|
|
username="$(uci_get ${name}.username)"
|
|
password="$(uci_get ${name}._password)"
|
|
domain="$(uci_get ${name}.domain)"
|
|
vers="$(uci_get ${name}.cifs_version || echo "$default_vers")"
|
|
ro="$(uci_get ${name}.read_only)"
|
|
|
|
opts="vers=${vers}"
|
|
|
|
if [ -n "$username" ] && [ "$username" != "guest" ]; then
|
|
opts="${opts},username=${username}"
|
|
[ -n "$password" ] && opts="${opts},password=${password}"
|
|
[ -n "$domain" ] && opts="${opts},domain=${domain}"
|
|
else
|
|
opts="${opts},guest"
|
|
fi
|
|
|
|
[ "$ro" = "1" ] && opts="${opts},ro" || opts="${opts},rw"
|
|
|
|
# Reasonable defaults for network mounts
|
|
opts="${opts},iocharset=utf8,noperm,noserverino"
|
|
|
|
echo "$opts"
|
|
}
|
|
|
|
# Check if a share is currently mounted
|
|
is_mounted() {
|
|
local mountpoint="$1"
|
|
grep -q " ${mountpoint} cifs " /proc/mounts 2>/dev/null
|
|
}
|
|
|
|
# =============================================================================
|
|
# SHARE MANAGEMENT
|
|
# =============================================================================
|
|
|
|
cmd_add() {
|
|
local name="$1" server="$2" mountpoint="$3"
|
|
|
|
[ -z "$name" ] || [ -z "$server" ] || [ -z "$mountpoint" ] && {
|
|
echo "Usage: smbfsctl add <name> <server> <mountpoint>" >&2
|
|
exit 1
|
|
}
|
|
|
|
if share_exists "$name"; then
|
|
log_error "Share '$name' already exists"
|
|
exit 1
|
|
fi
|
|
|
|
load_global
|
|
|
|
uci add ${CONFIG} mount >/dev/null
|
|
# Rename the unnamed section to the given name
|
|
local idx
|
|
idx=$(uci -q show ${CONFIG} | grep "=mount$" | tail -1 | sed "s/${CONFIG}\.\(.*\)=mount/\1/")
|
|
uci rename ${CONFIG}.${idx}="${name}"
|
|
|
|
uci set ${CONFIG}.${name}.enabled='0'
|
|
uci set ${CONFIG}.${name}.server="$server"
|
|
uci set ${CONFIG}.${name}.mountpoint="$mountpoint"
|
|
uci set ${CONFIG}.${name}.username='guest'
|
|
uci set ${CONFIG}.${name}._password=''
|
|
uci set ${CONFIG}.${name}.domain=''
|
|
uci set ${CONFIG}.${name}.cifs_version="$default_vers"
|
|
uci set ${CONFIG}.${name}.read_only='1'
|
|
uci set ${CONFIG}.${name}.auto_mount='0'
|
|
uci set ${CONFIG}.${name}.description=''
|
|
uci commit ${CONFIG}
|
|
|
|
log_info "Share '$name' added: $server -> $mountpoint"
|
|
log_info "Set credentials: smbfsctl credentials $name <user> <pass>"
|
|
}
|
|
|
|
cmd_remove() {
|
|
local name="$1"
|
|
[ -z "$name" ] && { echo "Usage: smbfsctl remove <name>" >&2; exit 1; }
|
|
|
|
if ! share_exists "$name"; then
|
|
log_error "Share '$name' not found"
|
|
exit 1
|
|
fi
|
|
|
|
# Unmount first if mounted
|
|
local mountpoint
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then
|
|
umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null
|
|
fi
|
|
|
|
uci delete ${CONFIG}.${name}
|
|
uci commit ${CONFIG}
|
|
|
|
log_info "Share '$name' removed"
|
|
}
|
|
|
|
cmd_enable() {
|
|
local name="$1"
|
|
[ -z "$name" ] && { echo "Usage: smbfsctl enable <name>" >&2; exit 1; }
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
uci_set ${name}.enabled '1'
|
|
uci_set ${name}.auto_mount '1'
|
|
log_info "Share '$name' enabled for auto-mount"
|
|
}
|
|
|
|
cmd_disable() {
|
|
local name="$1"
|
|
[ -z "$name" ] && { echo "Usage: smbfsctl disable <name>" >&2; exit 1; }
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
uci_set ${name}.enabled '0'
|
|
uci_set ${name}.auto_mount '0'
|
|
log_info "Share '$name' disabled"
|
|
}
|
|
|
|
cmd_credentials() {
|
|
local name="$1" user="$2" pass="$3"
|
|
|
|
[ -z "$name" ] || [ -z "$user" ] && {
|
|
echo "Usage: smbfsctl credentials <name> <user> <pass>" >&2
|
|
exit 1
|
|
}
|
|
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
uci_set ${name}.username "$user"
|
|
uci_set ${name}._password "$pass"
|
|
|
|
log_info "Credentials set for '$name' (user: $user)"
|
|
}
|
|
|
|
cmd_set() {
|
|
local name="$1" key="$2" value="$3"
|
|
|
|
[ -z "$name" ] || [ -z "$key" ] && {
|
|
echo "Usage: smbfsctl set <name> <key> <value>" >&2
|
|
exit 1
|
|
}
|
|
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
uci_set ${name}.${key} "$value"
|
|
log_info "Set ${name}.${key} = $value"
|
|
}
|
|
|
|
cmd_list() {
|
|
load_global
|
|
|
|
local found=0
|
|
local shares
|
|
shares="$(get_shares)"
|
|
|
|
if [ -z "$shares" ]; then
|
|
echo "No SMB shares configured."
|
|
echo "Add one: smbfsctl add <name> //server/share /mnt/smb/name"
|
|
return
|
|
fi
|
|
|
|
printf "%-12s %-8s %-28s %-24s %s\n" "NAME" "STATUS" "SERVER" "MOUNTPOINT" "USER"
|
|
printf "%-12s %-8s %-28s %-24s %s\n" "----" "------" "------" "----------" "----"
|
|
|
|
for name in $shares; do
|
|
local enabled server mountpoint username status
|
|
|
|
enabled="$(uci_get ${name}.enabled)"
|
|
server="$(uci_get ${name}.server)"
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
username="$(uci_get ${name}.username)"
|
|
|
|
if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then
|
|
status="mounted"
|
|
elif [ "$enabled" = "1" ]; then
|
|
status="enabled"
|
|
else
|
|
status="disabled"
|
|
fi
|
|
|
|
printf "%-12s %-8s %-28s %-24s %s\n" "$name" "$status" "$server" "$mountpoint" "${username:-guest}"
|
|
found=1
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# MOUNT OPERATIONS
|
|
# =============================================================================
|
|
|
|
cmd_mount() {
|
|
require_root
|
|
local name="$1"
|
|
[ -z "$name" ] && { echo "Usage: smbfsctl mount <name>" >&2; exit 1; }
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
load_global
|
|
|
|
local server mountpoint
|
|
server="$(uci_get ${name}.server)"
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
|
|
[ -z "$server" ] && { log_error "No server configured for '$name'"; exit 1; }
|
|
[ -z "$mountpoint" ] && { log_error "No mountpoint configured for '$name'"; exit 1; }
|
|
|
|
if is_mounted "$mountpoint"; then
|
|
log_info "'$name' already mounted at $mountpoint"
|
|
return 0
|
|
fi
|
|
|
|
# Create mountpoint
|
|
mkdir -p "$mountpoint"
|
|
|
|
local opts
|
|
opts="$(build_mount_opts "$name")"
|
|
|
|
log_info "Mounting $server -> $mountpoint"
|
|
if mount -t cifs "$server" "$mountpoint" -o "$opts" 2>&1; then
|
|
log_info "Mounted '$name' successfully"
|
|
else
|
|
log_error "Failed to mount '$name'"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
cmd_mount_all() {
|
|
require_root
|
|
load_global
|
|
|
|
local shares count=0 fail=0
|
|
shares="$(get_shares)"
|
|
|
|
for name in $shares; do
|
|
local enabled auto_mount
|
|
enabled="$(uci_get ${name}.enabled)"
|
|
auto_mount="$(uci_get ${name}.auto_mount)"
|
|
|
|
[ "$enabled" = "1" ] && [ "$auto_mount" = "1" ] || continue
|
|
|
|
if cmd_mount_single "$name"; then
|
|
count=$((count + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
done
|
|
|
|
log_info "Mounted $count share(s), $fail failure(s)"
|
|
}
|
|
|
|
# Internal: mount a single share (no arg validation)
|
|
cmd_mount_single() {
|
|
local name="$1"
|
|
local server mountpoint
|
|
|
|
server="$(uci_get ${name}.server)"
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
|
|
[ -z "$server" ] || [ -z "$mountpoint" ] && return 1
|
|
|
|
if is_mounted "$mountpoint"; then
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$mountpoint"
|
|
|
|
local opts
|
|
opts="$(build_mount_opts "$name")"
|
|
|
|
mount -t cifs "$server" "$mountpoint" -o "$opts" 2>/dev/null
|
|
}
|
|
|
|
cmd_umount() {
|
|
require_root
|
|
local name="$1"
|
|
[ -z "$name" ] && { echo "Usage: smbfsctl umount <name>" >&2; exit 1; }
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
local mountpoint
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
|
|
if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then
|
|
umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null
|
|
log_info "Unmounted '$name' from $mountpoint"
|
|
else
|
|
log_info "'$name' is not mounted"
|
|
fi
|
|
}
|
|
|
|
cmd_umount_all() {
|
|
require_root
|
|
load_global
|
|
|
|
local shares
|
|
shares="$(get_shares)"
|
|
|
|
for name in $shares; do
|
|
local mountpoint
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then
|
|
umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null
|
|
log_info "Unmounted '$name'"
|
|
fi
|
|
done
|
|
}
|
|
|
|
cmd_status() {
|
|
load_global
|
|
|
|
echo "=== SMB/CIFS Mount Status ==="
|
|
echo ""
|
|
|
|
local shares
|
|
shares="$(get_shares)"
|
|
|
|
if [ -z "$shares" ]; then
|
|
echo "No shares configured."
|
|
return
|
|
fi
|
|
|
|
for name in $shares; do
|
|
local enabled server mountpoint desc
|
|
|
|
enabled="$(uci_get ${name}.enabled)"
|
|
server="$(uci_get ${name}.server)"
|
|
mountpoint="$(uci_get ${name}.mountpoint)"
|
|
desc="$(uci_get ${name}.description)"
|
|
|
|
printf "Share: %s" "$name"
|
|
[ -n "$desc" ] && printf " (%s)" "$desc"
|
|
echo ""
|
|
|
|
printf " Server: %s\n" "$server"
|
|
printf " Mountpoint: %s\n" "$mountpoint"
|
|
printf " Enabled: %s\n" "$([ "$enabled" = "1" ] && echo "yes" || echo "no")"
|
|
|
|
if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then
|
|
# Get mount stats
|
|
local usage
|
|
usage=$(df -h "$mountpoint" 2>/dev/null | tail -1)
|
|
printf " Status: MOUNTED\n"
|
|
if [ -n "$usage" ]; then
|
|
local size used avail pct
|
|
size=$(echo "$usage" | awk '{print $2}')
|
|
used=$(echo "$usage" | awk '{print $3}')
|
|
avail=$(echo "$usage" | awk '{print $4}')
|
|
pct=$(echo "$usage" | awk '{print $5}')
|
|
printf " Disk: %s used / %s total (%s free, %s)\n" "$used" "$size" "$avail" "$pct"
|
|
fi
|
|
else
|
|
printf " Status: NOT MOUNTED\n"
|
|
fi
|
|
echo ""
|
|
done
|
|
}
|
|
|
|
cmd_test() {
|
|
local name="$1"
|
|
[ -z "$name" ] && { echo "Usage: smbfsctl test <name>" >&2; exit 1; }
|
|
share_exists "$name" || { log_error "Share '$name' not found"; exit 1; }
|
|
|
|
load_global
|
|
|
|
local server
|
|
server="$(uci_get ${name}.server)"
|
|
|
|
# Extract hostname from //host/share
|
|
local host
|
|
host=$(echo "$server" | sed 's|^//||; s|/.*||')
|
|
|
|
log_info "Testing connectivity to $host..."
|
|
|
|
# Test network reachability
|
|
if ping -c 1 -W "$timeout" "$host" >/dev/null 2>&1; then
|
|
log_info "Host $host is reachable"
|
|
else
|
|
log_error "Host $host is not reachable"
|
|
return 1
|
|
fi
|
|
|
|
# Test SMB port (445)
|
|
local smb_ok=0
|
|
if [ -f /proc/net/tcp ]; then
|
|
# Try a TCP connection via shell
|
|
if (echo > /dev/tcp/"$host"/445) 2>/dev/null; then
|
|
smb_ok=1
|
|
fi
|
|
fi
|
|
|
|
# Fallback: try netstat or just attempt mount
|
|
if [ "$smb_ok" = "1" ]; then
|
|
log_info "SMB port 445 is open on $host"
|
|
else
|
|
log_warn "Could not verify SMB port 445 (will attempt mount anyway)"
|
|
fi
|
|
|
|
# Try a test mount
|
|
require_root
|
|
local mountpoint="/tmp/smbfs-test-$$"
|
|
mkdir -p "$mountpoint"
|
|
|
|
local opts
|
|
opts="$(build_mount_opts "$name")"
|
|
|
|
if mount -t cifs "$server" "$mountpoint" -o "$opts" 2>&1; then
|
|
log_info "Test mount successful — share is accessible"
|
|
local count
|
|
count=$(ls -1 "$mountpoint" 2>/dev/null | wc -l)
|
|
log_info "Contents: $count items visible"
|
|
umount "$mountpoint" 2>/dev/null
|
|
else
|
|
log_error "Test mount failed — check server, credentials, or share name"
|
|
rmdir "$mountpoint" 2>/dev/null
|
|
return 1
|
|
fi
|
|
|
|
rmdir "$mountpoint" 2>/dev/null
|
|
log_info "Test complete: share '$name' is working"
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN
|
|
# =============================================================================
|
|
|
|
case "${1:-}" in
|
|
add) shift; cmd_add "$@" ;;
|
|
remove) shift; cmd_remove "$@" ;;
|
|
enable) shift; cmd_enable "$@" ;;
|
|
disable) shift; cmd_disable "$@" ;;
|
|
credentials) shift; cmd_credentials "$@" ;;
|
|
set) shift; cmd_set "$@" ;;
|
|
list) shift; cmd_list "$@" ;;
|
|
mount) shift; cmd_mount "$@" ;;
|
|
mount-all) shift; cmd_mount_all "$@" ;;
|
|
umount) shift; cmd_umount "$@" ;;
|
|
umount-all) shift; cmd_umount_all "$@" ;;
|
|
status) shift; cmd_status "$@" ;;
|
|
test) shift; cmd_test "$@" ;;
|
|
help|--help|-h|'') usage ;;
|
|
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|