From 8bc8e0125dd5e591c8ad835f11d7bc654f68e85e Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 19 Oct 2025 14:28:54 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Initial=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 8 ++ cifs-watch.service.sh | 8 ++ cifs-watch.sh | 229 ++++++++++++++++++++++++++++++++++++++++++ cifs-watch.timer.txt | 12 +++ 4 files changed, 257 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 cifs-watch.service.sh create mode 100644 cifs-watch.sh create mode 100644 cifs-watch.timer.txt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fbe7b7e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "workbench.colorCustomizations": { + "tree.indentGuidesStroke": "#3d92ec", + "activityBar.background": "#5C0A2F", + "titleBar.activeBackground": "#800E42", + "titleBar.activeForeground": "#FFFCFD" + } +} diff --git a/cifs-watch.service.sh b/cifs-watch.service.sh new file mode 100644 index 0000000..44b7666 --- /dev/null +++ b/cifs-watch.service.sh @@ -0,0 +1,8 @@ +# /etc/systemd/system/cifs-watch.service +[Unit] +Description=Monitor and repair CIFS mounts + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/cifs-watch +Nice=10 diff --git a/cifs-watch.sh b/cifs-watch.sh new file mode 100644 index 0000000..1e177ac --- /dev/null +++ b/cifs-watch.sh @@ -0,0 +1,229 @@ +#!/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" diff --git a/cifs-watch.timer.txt b/cifs-watch.timer.txt new file mode 100644 index 0000000..1c75757 --- /dev/null +++ b/cifs-watch.timer.txt @@ -0,0 +1,12 @@ +# /etc/systemd/system/cifs-watch.timer +[Unit] +Description=Run cifs-watch periodically + +[Timer] +OnBootSec=2min +OnUnitActiveSec=5min +AccuracySec=30s +Unit=cifs-watch.service + +[Install] +WantedBy=timers.target