#!/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"