Files
cifs-watch/cifs-watch.sh
2025-10-19 14:28:54 -05:00

230 lines
6.3 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Monitor CIFS mounts from /etc/fstab and (re)mount if needed.
# Designed for cron/systemd timer. Requires root.
set -Eeuo pipefail
IFS=$'\n\t'
# ---------------------------
# Config (edit if desired)
# ---------------------------
LOGFILE="/var/log/cifs-remount.log" # Set empty ("") to disable file logging.
PING_COUNT=1
PING_TIMEOUT=1 # seconds
TCP_TIMEOUT=2 # seconds for port 445 check
PROBE_TIMEOUT=2 # seconds to test mount health (listing)
REMOUNT_RETRY=2 # attempts
SLEEP_BETWEEN=1 # seconds between retries
SYSLOG_TAG="cifs-watch"
DRY_RUN=0
VERBOSE=0
# ---------------------------
# Helpers
# ---------------------------
log() {
local level="$1"; shift
local msg="$*"
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
if [[ "$VERBOSE" -eq 1 || "$level" != "DEBUG" ]]; then
echo "[$ts] [$level] $msg"
fi
logger -t "$SYSLOG_TAG[$$]" -p "user.$(tr '[:upper:]' '[:lower:]' <<<"$level")" -- "$msg" || true
if [[ -n "$LOGFILE" ]]; then
( umask 0077; echo "[$ts] [$level] $msg" >> "$LOGFILE" ) || true
fi
}
vdbg() { [[ "$VERBOSE" -eq 1 ]] && log "DEBUG" "$*"; }
fail() { log "ERROR" "$*"; exit 1; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
tcp_open_445() {
local host="$1"
if have_cmd nc; then
nc -z -w "$TCP_TIMEOUT" "$host" 445 >/dev/null 2>&1
return $?
else
timeout "$TCP_TIMEOUT" bash -c "cat < /dev/null > /dev/tcp/$host/445" 2>/dev/null
return $?
fi
}
host_reachable() {
local host="$1"
if ! getent ahosts "$host" >/dev/null 2>&1; then
vdbg "DNS resolution failed for $host"
return 1
fi
if have_cmd ping; then
ping -c "$PING_COUNT" -W "$PING_TIMEOUT" "$host" >/dev/null 2>&1 || vdbg "Ping to $host failed"
fi
if tcp_open_445 "$host"; then
return 0
else
vdbg "TCP/445 closed on $host"
return 1
fi
}
is_cifs_mounted() {
local mnt="$1"
if findmnt -no FSTYPE -T "$mnt" 2>/dev/null | grep -qi '^cifs$'; then
return 0
fi
return 1
}
mount_healthy() {
local mnt="$1"
timeout "$PROBE_TIMEOUT" bash -c 'ls -1A -- "$0" >/dev/null 2>&1' "$mnt"
}
repair_mount() {
local mnt="$1"
local attempt=1
while (( attempt <= REMOUNT_RETRY )); do
vdbg "Attempt $attempt: remounting $mnt"
if (( DRY_RUN )); then
log "INFO" "DRY-RUN: would remount $mnt"
return 0
fi
if mount -o remount "$mnt" >/dev/null 2>&1; then
if mount_healthy "$mnt"; then
log "INFO" "Remounted healthy: $mnt"
return 0
fi
vdbg "Remount completed but health probe failed: $mnt"
fi
sleep "$SLEEP_BETWEEN"
(( attempt++ ))
done
log "WARN" "Remount failed/unhealthy for $mnt; trying forced unmount + clean mount"
if (( DRY_RUN )); then
log "INFO" "DRY-RUN: would umount -f $mnt && mount $mnt"
return 0
fi
if umount -f "$mnt" >/dev/null 2>&1 || umount -l "$mnt" >/dev/null 2>&1; then
:
else
log "WARN" "Unable to unmount $mnt; will still attempt a mount"
fi
if mount "$mnt" >/dev/null 2>&1; then
if mount_healthy "$mnt"; then
log "INFO" "Mounted healthy: $mnt"
return 0
else
log "WARN" "Mounted but health probe failed: $mnt"
return 1
fi
else
log "ERROR" "Mount failed for $mnt"
return 1
fi
}
usage() {
cat <<'USAGE'
cifs-watch.sh [-n|--dry-run] [-v|--verbose] [--logfile PATH]
Monitors CIFS entries in /etc/fstab, checks server reachability, and (re)mounts as needed.
- Processes uncommented lines with type "cifs".
- Accepts fstab lines with 46 fields.
- Skips entries containing "noauto".
Options:
-n, --dry-run Show actions without changing anything
-v, --verbose More detailed output
--logfile P Override logfile path (empty to disable file logging)
USAGE
}
# ---------------------------
# Parse args
# ---------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--dry-run) DRY_RUN=1; shift ;;
-v|--verbose) VERBOSE=1; shift ;;
--logfile) LOGFILE="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1"; usage; exit 2 ;;
esac
done
# Ensure tools present
for bin in findmnt mount umount awk grep sed timeout; do
have_cmd "$bin" || fail "Required command not found: $bin"
done
# ---------------------------
# Main: parse /etc/fstab
# ---------------------------
mapfile -t CIFS_LINES < <(awk '
$0 !~ /^[[:space:]]*#/ && NF>=4 && tolower($3)=="cifs" { print }
' /etc/fstab)
if [[ ${#CIFS_LINES[@]} -eq 0 ]]; then
log "INFO" "No CIFS entries found in /etc/fstab. Nothing to do."
exit 0
fi
overall_rc=0
for line in "${CIFS_LINES[@]}"; do
# fields: fs_spec mountpoint fstype options [dump] [pass]
fs_spec=$(awk '{print $1}' <<<"$line")
mnt_point=$(awk '{print $2}' <<<"$line")
fstype=$(awk '{print tolower($3)}' <<<"$line")
options=$(awk '{print $4}' <<<"$line")
dumpv=$(awk 'NF>=5{print $5}' <<<"$line")
passv=$(awk 'NF>=6{print $6}' <<<"$line")
# Skip noauto entries
if grep -qi '(^|,)noauto(,|$)' <<<",$options,"; then
vdbg "Skipping noauto CIFS entry: $mnt_point ($fs_spec)"
continue
fi
# Parse server from //server/share
if [[ "$fs_spec" =~ ^//([^/]+)/.+$ ]]; then
server="${BASH_REMATCH[1]}"
else
log "WARN" "Could not parse server from fs_spec: $fs_spec (skipping)"
continue
fi
log "INFO" "Checking CIFS mount: $mnt_point (server: $server)"
if is_cifs_mounted "$mnt_point"; then
if mount_healthy "$mnt_point"; then
vdbg "Healthy: $mnt_point"
continue
else
log "WARN" "Mounted but unhealthy: $mnt_point"
fi
else
log "WARN" "Not mounted: $mnt_point"
fi
if host_reachable "$server"; then
log "INFO" "Server reachable: $server — attempting repair for $mnt_point"
else
log "ERROR" "Server NOT reachable: $server — skipping $mnt_point for now"
overall_rc=1
continue
fi
if ! repair_mount "$mnt_point"; then
log "ERROR" "Repair failed: $mnt_point"
overall_rc=1
fi
done
exit "$overall_rc"