230 lines
6.3 KiB
Bash
230 lines
6.3 KiB
Bash
#!/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 4–6 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"
|