Files
ubuntu-post-install/tools/mount-network-drive.sh
T
Claude 9e886ff9a7 Fix CIFS mount error(79) caused by missing nls_utf8 kernel module
The keyutils fix alone didn't resolve it — confirmed live with keyutils
already installed, the same error persisted. Root cause: the hardcoded
iocharset=utf8 mount option requires the kernel's nls_utf8 module, which
some kernels don't ship at all (confirmed live: `modprobe nls_utf8` on a
stock Ubuntu 6.8.0-137-generic VPS kernel returns "FATAL: Module
nls_utf8 not found" — not loadable, not built in). Every such mount
fails with errno 79 (ELIBACC) regardless of credentials, which is why
this recurred identically after the keyutils fix.

Both vpn-data-mount.sh and mount-network-drive.sh now probe with a
harmless `modprobe nls_utf8` before adding the option, and mount without
it (falling back to the kernel's build-time nls_default) with a clear
warning if the module isn't available, instead of hard-failing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn
2026-08-10 20:11:05 +00:00

470 lines
18 KiB
Bash

#!/usr/bin/env bash
# mount-network-drive.sh — Mount a network share (SMB/CIFS or NFS) and add
# it to /etc/fstab so it survives reboots.
#
# Usage:
# sudo bash mount-network-drive.sh
#
# Supports:
# - SMB/CIFS (Windows shares, Samba, NAS)
# - NFS (Linux/NAS exports)
set -euo pipefail
# ── Helpers ───────────────────────────────────────────────────────────────────
info() { printf '\033[0;34m[INFO]\033[0m %s\n' "$*"; }
ok() { printf '\033[0;32m[OK]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
err() { printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2; }
[[ "$(id -u)" == "0" ]] || { err "Run with sudo: sudo bash $0"; exit 1; }
# Actual user who invoked sudo
ACTUAL_USER="${SUDO_USER:-$USER}"
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" | cut -d: -f6)"
ACTUAL_UID="$(id -u "$ACTUAL_USER")"
ACTUAL_GID="$(id -g "$ACTUAL_USER")"
# ── Install packages ──────────────────────────────────────────────────────────
ensure_packages() {
local pkgs=()
case "$1" in
smb)
command -v mount.cifs &>/dev/null || pkgs+=(cifs-utils)
# keyutils explicitly, not left to cifs-utils' Recommends — on
# images with install-recommends disabled, missing keyutils
# makes every mount.cifs call fail with "mount error(79): Can
# not access a needed shared library" regardless of credentials.
dpkg -s keyutils &>/dev/null || pkgs+=(keyutils)
;;
nfs) command -v mount.nfs &>/dev/null || pkgs+=(nfs-common) ;;
esac
if [[ ${#pkgs[@]} -gt 0 ]]; then
info "Installing: ${pkgs[*]}"
apt-get install -y "${pkgs[@]}" -qq
fi
}
# ── Already in fstab? ─────────────────────────────────────────────────────────
fstab_has() {
grep -qs "$1" /etc/fstab
}
# ── Show existing network mounts ──────────────────────────────────────────────
show_existing() {
local entries
entries=$(grep -E 'cifs|nfs' /etc/fstab 2>/dev/null || true)
if [[ -n "$entries" ]]; then
info "Existing network mounts in /etc/fstab:"
echo "$entries" | sed 's/^/ /'
echo ""
fi
}
# ── SMB/CIFS ──────────────────────────────────────────────────────────────────
mount_smb() {
ensure_packages smb
echo ""
echo " Enter the share details."
echo " Format: //server/share (e.g. //192.168.1.10/media or //nas/homes)"
echo ""
local share=""
while [[ -z "$share" ]]; do
read -r -p " Share path (//server/share): " share
done
local default_mount
default_mount="/mnt/$(echo "$share" | sed 's|//[^/]*/||; s|/|-|g')"
local mount_point=""
read -r -p " Mount point [$default_mount]: " mount_point
mount_point="${mount_point:-$default_mount}"
# Credentials
echo ""
local use_creds=""
read -r -p " Does this share require a username/password? [Y/n]: " use_creds
local creds_file="" creds_opt=""
if [[ "${use_creds,,}" != "n" ]]; then
local smb_user="" smb_pass="" smb_domain=""
read -r -p " Username: " smb_user
read -r -s -p " Password: " smb_pass; echo ""
read -r -p " Domain (leave blank if none): " smb_domain
# Store credentials file — root-owned, root-readable only
creds_file="/etc/samba/credentials.$(echo "$share" | sed 's|[^a-zA-Z0-9]|_|g')"
mkdir -p /etc/samba
cat > "$creds_file" << CREDS
username=${smb_user}
password=${smb_pass}
${smb_domain:+domain=${smb_domain}}
CREDS
chmod 600 "$creds_file"
chown root:root "$creds_file"
ok "Credentials saved to $creds_file (root-only, 600)"
creds_opt="credentials=${creds_file}"
else
creds_opt="guest"
fi
# SMB version
echo ""
echo " SMB version (leave blank to let the system negotiate):"
echo " 3.0 — modern NAS, Windows 2012+, Samba 4+"
echo " 2.1 — older NAS, Windows 7/2008 R2"
echo " 1.0 — legacy only (insecure, not recommended)"
local smb_ver=""
read -r -p " SMB version [auto]: " smb_ver
local ver_opt=""
[[ -n "$smb_ver" ]] && ver_opt=",vers=${smb_ver}"
local opts="uid=${ACTUAL_UID},gid=${ACTUAL_GID},${creds_opt}${ver_opt},nofail,_netdev"
# iocharset=utf8 only if the kernel can actually load nls_utf8 — some
# kernels (confirmed live: a stock Ubuntu 6.8.0-137-generic VPS) don't
# ship that module at all, and mount.cifs fails every such mount with
# "mount error(79): Can not access a needed shared library" regardless
# of credentials. modprobe on an already-loaded/built-in module is a
# harmless no-op, so this check is safe to run unconditionally.
if modprobe nls_utf8 >/dev/null 2>&1; then
opts="${opts},iocharset=utf8"
else
warn "This kernel ($(uname -r)) has no nls_utf8 module — mounting without iocharset=utf8. Non-ASCII filenames may not display correctly; try 'sudo apt-get install --reinstall linux-modules-$(uname -r)' to see if it restores the module."
fi
_do_mount "cifs" "$share" "$mount_point" "$opts"
}
# ── NFS ───────────────────────────────────────────────────────────────────────
mount_nfs() {
ensure_packages nfs
echo ""
echo " Format: server:/export (e.g. 192.168.1.10:/mnt/data or nas:/homes)"
echo ""
local share=""
while [[ -z "$share" ]]; do
read -r -p " NFS export (server:/path): " share
done
local default_mount
default_mount="/mnt/$(echo "$share" | sed 's|[:/]|-|g; s|^-||')"
local mount_point=""
read -r -p " Mount point [$default_mount]: " mount_point
mount_point="${mount_point:-$default_mount}"
echo ""
echo " NFS version:"
echo " 4 — recommended (NFSv4, supports Kerberos, ACLs)"
echo " 3 — older servers/NAS"
local nfs_ver=""
read -r -p " NFS version [4]: " nfs_ver
nfs_ver="${nfs_ver:-4}"
local opts="nfsvers=${nfs_ver},rw,soft,intr,nofail,_netdev"
_do_mount "nfs" "$share" "$mount_point" "$opts"
}
# ── Common mount + fstab logic ────────────────────────────────────────────────
_do_mount() {
local fstype="$1" share="$2" mount_point="$3" opts="$4"
# Create mount point
if [[ ! -d "$mount_point" ]]; then
mkdir -p "$mount_point"
ok "Created mount point: $mount_point"
fi
# Test mount
echo ""
info "Testing mount..."
if mount -t "$fstype" -o "$opts" "$share" "$mount_point"; then
ok "Mounted successfully at $mount_point"
else
err "Mount failed. Check the share path, credentials, and network connectivity."
rmdir "$mount_point" 2>/dev/null || true
return 1
fi
# Show what's there
echo ""
info "Contents of $mount_point:"
ls "$mount_point" | head -20 | sed 's/^/ /' || true
echo ""
# fstab
local add_fstab=""
read -r -p " Add to /etc/fstab so it mounts on boot? [Y/n]: " add_fstab
if [[ "${add_fstab,,}" != "n" ]]; then
if fstab_has "$mount_point"; then
warn "$mount_point already in /etc/fstab — skipping."
else
# Backup fstab
local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
cp /etc/fstab "$bk"
info "Backed up /etc/fstab → $bk"
printf '\n# %s — added by mount-network-drive.sh %s\n' \
"$share" "$(date +%Y-%m-%d)" >> /etc/fstab
printf '%-40s %-25s %-6s %s 0 0\n' \
"$share" "$mount_point" "$fstype" "$opts" >> /etc/fstab
ok "Added to /etc/fstab"
info "Verify with: sudo mount -a"
fi
fi
# Ownership hint
echo ""
info "To make this drive writable by $ACTUAL_USER:"
echo " sudo chown -R ${ACTUAL_USER}:${ACTUAL_USER} $mount_point"
echo ""
ok "Done. $share is mounted at $mount_point"
}
# ── Edit a mount ─────────────────────────────────────────────────────────────
edit_mount() {
echo ""
local entries=()
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
entries+=("$line")
done < <(grep -E '\bcifs\b|\bnfs\b' /etc/fstab 2>/dev/null || true)
if [[ ${#entries[@]} -eq 0 ]]; then
warn "No network mounts found in /etc/fstab."
return 0
fi
echo " Network mounts in /etc/fstab:"
echo ""
local i=1
for e in "${entries[@]}"; do
local mp; mp=$(echo "$e" | awk '{print $2}')
local src; src=$(echo "$e" | awk '{print $1}')
printf " %2d) %-30s %s\n" "$i" "$src" "$mp"
i=$(( i + 1 ))
done
echo ""
local choice=""
read -r -p " Edit entry number [cancel]: " choice
[[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]] && { info "Cancelled."; return 0; }
local idx=$(( choice - 1 ))
[[ "$idx" -lt 0 || "$idx" -ge ${#entries[@]} ]] && { warn "Invalid selection."; return 0; }
local target_entry="${entries[$idx]}"
local target_src; target_src=$(echo "$target_entry" | awk '{print $1}')
local target_mp; target_mp=$(echo "$target_entry" | awk '{print $2}')
local target_fs; target_fs=$(echo "$target_entry" | awk '{print $3}')
local target_opts; target_opts=$(echo "$target_entry" | awk '{print $4}')
echo ""
echo " Current values:"
printf " Share : %s\n" "$target_src"
printf " Mount point: %s\n" "$target_mp"
printf " Type : %s\n" "$target_fs"
printf " Options : %s\n" "$target_opts"
echo ""
echo " Press Enter to keep the current value."
echo ""
# Share path
local new_src=""
read -r -p " Share path [$target_src]: " new_src
new_src="${new_src:-$target_src}"
# Mount point
local new_mp=""
read -r -p " Mount point [$target_mp]: " new_mp
new_mp="${new_mp:-$target_mp}"
# Options — offer guided or raw edit
echo ""
echo " Options — edit raw fstab options string, or press Enter to keep."
local new_opts=""
read -r -p " Options [$target_opts]: " new_opts
new_opts="${new_opts:-$target_opts}"
# Credentials update (CIFS only)
local creds_file="/etc/samba/credentials.$(echo "$target_src" | sed 's|[^a-zA-Z0-9]|_|g')"
if [[ "$target_fs" == "cifs" && -f "$creds_file" ]]; then
echo ""
local update_creds=""
read -r -p " Update credentials (username/password)? [y/N]: " update_creds
if [[ "${update_creds,,}" == "y" ]]; then
local smb_user="" smb_pass="" smb_domain=""
read -r -p " Username: " smb_user
read -r -s -p " Password: " smb_pass; echo ""
read -r -p " Domain (leave blank if none): " smb_domain
local bk_creds="${creds_file}.backup.$(date +%Y%m%d-%H%M%S)"
cp "$creds_file" "$bk_creds"
cat > "$creds_file" << CREDS
username=${smb_user}
password=${smb_pass}
${smb_domain:+domain=${smb_domain}}
CREDS
chmod 600 "$creds_file"
chown root:root "$creds_file"
ok "Credentials updated (old saved to $(basename "$bk_creds"))"
fi
fi
# Write changes
local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
cp /etc/fstab "$bk"
info "Backed up /etc/fstab → $bk"
# Replace the old line with the new one
local new_line
new_line=$(printf '%-40s %-25s %-6s %s 0 0' "$new_src" "$new_mp" "$target_fs" "$new_opts")
# Escape for sed
local escaped_old escaped_new
escaped_old=$(printf '%s\n' "$target_entry" | sed 's/[[\.*^$()+?{|]/\\&/g')
escaped_new=$(printf '%s\n' "$new_line" | sed 's/[[\.*^$()+?{|]/\\&/g; s|/|\\/|g')
sed -i "s|${escaped_old}|${new_line}|" /etc/fstab
ok "fstab updated"
# Remount if mount point changed or options changed
if mountpoint -q "$target_mp" 2>/dev/null; then
echo ""
local remount=""
read -r -p " Remount now to apply changes? [Y/n]: " remount
if [[ "${remount,,}" != "n" ]]; then
umount "$target_mp" 2>/dev/null || warn "Could not unmount cleanly — may still be in use"
[[ "$new_mp" != "$target_mp" ]] && mkdir -p "$new_mp"
if mount -t "$target_fs" -o "$new_opts" "$new_src" "$new_mp"; then
ok "Remounted at $new_mp"
else
err "Remount failed — check options and connectivity."
info "Manual retry: sudo mount -t $target_fs -o $new_opts $new_src $new_mp"
fi
fi
fi
}
# ── Remove a mount ────────────────────────────────────────────────────────────
remove_mount() {
echo ""
local entries=()
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
entries+=("$line")
done < <(grep -E '\bcifs\b|\bnfs\b' /etc/fstab 2>/dev/null || true)
if [[ ${#entries[@]} -eq 0 ]]; then
warn "No network mounts found in /etc/fstab."
return 0
fi
echo " Network mounts in /etc/fstab:"
echo ""
local i=1
for e in "${entries[@]}"; do
local mp; mp=$(echo "$e" | awk '{print $2}')
local src; src=$(echo "$e" | awk '{print $1}')
printf " %2d) %-30s %s\n" "$i" "$src" "$mp"
i=$(( i + 1 ))
done
echo ""
local choice=""
read -r -p " Remove entry number [cancel]: " choice
[[ -z "$choice" || ! "$choice" =~ ^[0-9]+$ ]] && { info "Cancelled."; return 0; }
local idx=$(( choice - 1 ))
[[ "$idx" -lt 0 || "$idx" -ge ${#entries[@]} ]] && { warn "Invalid selection."; return 0; }
local target_entry="${entries[$idx]}"
local target_mp; target_mp=$(echo "$target_entry" | awk '{print $2}')
local target_src; target_src=$(echo "$target_entry" | awk '{print $1}')
warn "Will unmount $target_mp and remove from /etc/fstab."
local confirm=""
read -r -p " Confirm? [y/N]: " confirm
[[ "${confirm,,}" == "y" ]] || { info "Cancelled."; return 0; }
# Unmount
if mountpoint -q "$target_mp" 2>/dev/null; then
umount "$target_mp" && ok "Unmounted $target_mp" || warn "umount failed — may still be in use"
fi
# Remove from fstab (match the mount point field)
local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
cp /etc/fstab "$bk"
info "Backed up /etc/fstab → $bk"
# Remove the entry line and its preceding comment if added by this script
sed -i "\|${target_src}|d" /etc/fstab
sed -i "\|# ${target_src} — added by mount-network-drive.sh|d" /etc/fstab
ok "Removed from /etc/fstab"
# Remove credentials file if it exists
local creds_file
creds_file="/etc/samba/credentials.$(echo "$target_src" | sed 's|[^a-zA-Z0-9]|_|g')"
if [[ -f "$creds_file" ]]; then
local rm_creds=""
read -r -p " Remove credentials file $creds_file? [Y/n]: " rm_creds
[[ "${rm_creds,,}" != "n" ]] && rm -f "$creds_file" && ok "Removed $creds_file"
fi
}
# ── Show current mounts ───────────────────────────────────────────────────────
show_mounts() {
echo ""
echo " Currently mounted network shares:"
echo ""
mount | grep -E 'type (cifs|nfs)' | awk '{print " " $1 " → " $3}' || echo " (none)"
echo ""
echo " /etc/fstab network entries:"
echo ""
grep -E '\bcifs\b|\bnfs\b' /etc/fstab | sed 's/^/ /' || echo " (none)"
echo ""
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
echo ""
echo "┌──────────────────────────────────────────────────────────┐"
echo "│ Network Drive Mount Manager │"
echo "│ SMB/CIFS (Windows/Samba) and NFS shares │"
echo "└──────────────────────────────────────────────────────────┘"
echo ""
show_existing
while true; do
echo " What would you like to do?"
echo " 1) Mount an SMB/CIFS share (Windows, Samba, NAS)"
echo " 2) Mount an NFS share"
echo " 3) Show current network mounts"
echo " 4) Edit a mount"
echo " 5) Remove a mount"
echo " 0) Quit"
echo ""
read -r -p " Choice [1]: " action
action="${action:-1}"
echo ""
case "$action" in
1) mount_smb ;;
2) mount_nfs ;;
3) show_mounts ;;
4) edit_mount ;;
5) remove_mount ;;
0|q|Q) break ;;
*) warn "Invalid choice." ;;
esac
echo ""
done
ok "Done."
}
main "$@"