Posted on
Artificial Intelligence

Artificial Intelligence RArtificial IntelligenceD Monitoring

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

AI‑Assisted RAID Monitoring on Linux (with Bash)

Your RAID array is “healthy”… until it isn’t. SMART says “PASSED”… until a drive starts accumulating pending sectors and your rebuild window becomes a nail‑biter. Traditional threshold alarms can miss weak signals. Let’s use a tiny dose of AI to learn what “normal” looks like on your disks and flag drift before your array degrades.

This post shows how to glue together Bash, smartctl, mdadm, and a lightweight anomaly detection model to surface early‑warning signals. You’ll get install commands, collection scripts, a minimal Python model, and automation via cron.

Why AI for RAID monitoring?

  • Disks age differently. Thresholds that work for one batch fail for another. Anomaly detection adapts to your hardware’s baseline.

  • SMART “PASSED” can be misleading. Subtle metric drift often precedes failure (e.g., growing pending sectors, rising media errors, increasing temperature).

  • RAID masks pain until it breaks. Catching weak signals lets you replace a drive during business hours instead of during a rebuild.

  • It’s cheap and local. No cloud dependency; just Bash, smartctl/mdadm, and a small Python model.

What we’ll build: 1) A Bash collector that snapshots SMART and RAID state. 2) A Python IsolationForest model that learns your baseline and flags anomalies. 3) Cron jobs to collect data and alert daily.


1) Install prerequisites

Run these as root (or with sudo). Choose the commands for your distro.

  • Debian/Ubuntu (apt):
apt update
apt install -y smartmontools mdadm nvme-cli jq python3 python3-venv python3-pip mailutils cron
systemctl enable --now cron
  • RHEL/CentOS/Fedora (dnf):
dnf install -y smartmontools mdadm nvme-cli jq python3 python3-pip python3-virtualenv mailx cronie
systemctl enable --now crond
  • openSUSE/SLES (zypper):
zypper refresh
zypper install -y smartmontools mdadm nvme-cli jq python3 python3-pip python3-virtualenv s-nail cron
systemctl enable --now cron

Set up a Python virtual environment for the small AI model:

mkdir -p /opt/raid-ai
python3 -m venv /opt/raid-ai/venv
/opt/raid-ai/venv/bin/pip install --upgrade pip
/opt/raid-ai/venv/bin/pip install numpy pandas scikit-learn

2) Collect telemetry: SMART + RAID

This script:

  • Enumerates disks via lsblk.

  • Captures detailed SMART JSON per device (SATA/SAS/NVMe via smartctl).

  • Captures RAID status (/proc/mdstat + mdadm --detail).

  • Appends a JSON line to /var/log/raid-ai/metrics.jsonl

Save as /usr/local/sbin/raid_ai_collect.sh and make it executable.

#!/usr/bin/env bash
set -euo pipefail

LOG_DIR=/var/log/raid-ai
LOG_FILE="$LOG_DIR/metrics.jsonl"
mkdir -p "$LOG_DIR"
chmod 750 "$LOG_DIR"

ts=$(date -Iseconds)
host=$(hostname -f 2>/dev/null || hostname)

# Capture RAID status snapshots
mdstat=$(cat /proc/mdstat || true)
md_details_json="[]"
for a in /dev/md*; do
  [ -e "$a" ] || continue
  det=$(mdadm --detail "$a" 2>/dev/null || true)
  md_details_json=$(jq --arg arr "$a" --arg det "$det" '. + [{array: $arr, detail: $det}]' <<<"$md_details_json")
done

# Enumerate block devices
mapfile -t devs < <(lsblk -dn -o NAME,TYPE | awk '$2=="disk"{print "/dev/"$1}')

for dev in "${devs[@]}"; do
  smart_json=$(smartctl -x -j "$dev" 2>/dev/null || true)
  [ -n "$smart_json" ] || continue
  jq -n \
    --arg ts "$ts" \
    --arg host "$host" \
    --arg dev "$dev" \
    --arg mdstat "$mdstat" \
    --argjson mdadm "$md_details_json" \
    --argjson smart "$smart_json" \
    '{ts:$ts, host:$host, dev:$dev, mdstat:$mdstat, mdadm:$mdadm, smart:$smart}' \
    >> "$LOG_FILE"
done

Permissions:

chmod +x /usr/local/sbin/raid_ai_collect.sh

Notes:

  • smartctl -x -j works across ATA/NVMe, returning structured JSON we can parse later.

  • This script is read‑only, but SMART may spin up idle disks. Schedule sensibly.


3) A minimal anomaly detector

We’ll use IsolationForest (unsupervised) to learn your disks’ baseline and score the latest snapshot per device. It combines features like temperature, power‑on hours, and key SMART attributes (reallocated, pending, uncorrectable sectors; NVMe media errors; percentage used), plus a simple RAID degraded indicator.

Save as /usr/local/sbin/raid_ai_score.py:

#!/usr/bin/env python3
import json, sys, os, math, datetime
from collections import defaultdict

import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest

LOGFILE = "/var/log/raid-ai/metrics.jsonl"

def parse_time(s):
    try:
        return datetime.datetime.fromisoformat(s.replace("Z", "+00:00"))
    except Exception:
        return None

def ata_attr(smart, attr_id, default=0.0):
    try:
        for a in smart.get("ata_smart_attributes", {}).get("table", []):
            if a.get("id") == attr_id:
                return float(a.get("raw", {}).get("value", default))
    except Exception:
        pass
    return float(default)

def pick_temp_c(smart):
    # smartctl tries to normalize to Celsius under 'temperature.current'
    t = None
    try:
        t = smart.get("temperature", {}).get("current", None)
        if t is not None:
            return float(t)
    except Exception:
        pass
    # NVMe block (fallbacks)
    try:
        t = smart.get("nvme_smart_health_information_log", {}).get("temperature", None)
        if t is not None:
            return float(t)
    except Exception:
        pass
    return np.nan

def md_degraded(rec):
    degraded = False
    for s in [rec.get("mdstat","")] + [d.get("detail","") for d in rec.get("mdadm", [])]:
        s_low = (s or "").lower()
        if "degraded" in s_low or "[u_" in s_low or "[_u" in s_low:
            degraded = True
    return 1.0 if degraded else 0.0

def extract_features(rec):
    smart = rec.get("smart", {}) or {}
    feats = {}
    feats["temp_c"] = pick_temp_c(smart)
    feats["po_hours"] = float(smart.get("power_on_time", {}).get("hours", 0.0) or 0.0)
    feats["smart_pass"] = 1.0 if smart.get("smart_status", {}).get("passed", True) else 0.0

    # ATA attributes
    feats["realloc"] = ata_attr(smart, 5, 0.0)
    feats["pending"] = ata_attr(smart, 197, 0.0)
    feats["uncorr"]  = ata_attr(smart, 198, 0.0)

    # NVMe attributes
    nvme = smart.get("nvme_smart_health_information_log", {}) or {}
    feats["nvme_media_err"] = float(nvme.get("media_errors", 0.0) or 0.0)
    feats["nvme_pct_used"]  = float(nvme.get("percentage_used", 0.0) or 0.0)

    feats["raid_degraded"] = md_degraded(rec)
    return feats

def load_records(path):
    rows = []
    if not os.path.exists(path):
        return rows
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                rec = json.loads(line)
                rec["ts_dt"] = parse_time(rec.get("ts",""))
                rows.append(rec)
            except Exception:
                continue
    return rows

def main():
    recs = load_records(LOGFILE)
    if not recs:
        print("No data yet. Run the collector and wait for a few snapshots.")
        return

    # Build feature frame
    data = []
    for r in recs:
        feats = extract_features(r)
        feats["dev"] = r.get("dev", "unknown")
        feats["ts"]  = r.get("ts", "")
        feats["ts_dt"] = r.get("ts_dt", None)
        data.append(feats)
    df = pd.DataFrame(data)
    df = df.replace([np.inf, -np.inf], np.nan)

    # Keep rows with at least one signal
    feature_cols = ["temp_c","po_hours","smart_pass","realloc","pending","uncorr","nvme_media_err","nvme_pct_used","raid_degraded"]
    if df[feature_cols].dropna(how="all").empty:
        print("Not enough usable SMART/RAID features found. Check smartctl output and permissions.")
        return

    # Impute NaNs to 0 for robust tree-based model; log1p for heavy-tailed counts
    for c in ["realloc","pending","uncorr","nvme_media_err","po_hours"]:
        df[c] = np.log1p(df[c].fillna(0.0))
    for c in ["temp_c","smart_pass","nvme_pct_used","raid_degraded"]:
        df[c] = df[c].fillna(0.0)

    # Train on historical data (unsupervised)
    hist = df.dropna(subset=feature_cols)
    if len(hist) < 20:
        print("Collected {} samples; IsolationForest needs more history. Let it run for ~1 day.".format(len(hist)))
        return

    X = hist[feature_cols].values
    iforest = IsolationForest(
        n_estimators=200,
        contamination=0.05,      # Top 5% anomalies flagged
        random_state=42
    )
    iforest.fit(X)

    # Latest per device
    latest_idx = hist.groupby("dev")["ts_dt"].idxmax()
    latest = hist.loc[latest_idx].copy()
    X_latest = latest[feature_cols].values
    pred = iforest.predict(X_latest)          # 1 normal, -1 anomaly
    score = -iforest.score_samples(X_latest)  # Higher = more anomalous

    latest["anomaly_score"] = score
    latest["label"] = np.where(pred==-1, "ALERT", "OK")

    # Pretty print
    lines = []
    for _, row in latest.sort_values(by="anomaly_score", ascending=False).iterrows():
        msg = (
            f"{row['label']}: {row['dev']} score={row['anomaly_score']:.3f} "
            f"(temp_c={row['temp_c']:.1f}, po_h=exp({row['po_hours']:.2f})-1, "
            f"realloc=exp({row['realloc']:.2f})-1, pending=exp({row['pending']:.2f})-1, "
            f"uncorr=exp({row['uncorr']:.2f})-1, nvme_media=exp({row['nvme_media_err']:.2f})-1, "
            f"nvme_pct_used={row['nvme_pct_used']:.1f}, raid_degraded={int(row['raid_degraded'])})"
        )
        lines.append(msg)

    print("\n".join(lines))

if __name__ == "__main__":
    main()

Make it executable:

chmod +x /usr/local/sbin/raid_ai_score.py

What the model does:

  • Learns your disks’ “normal” feature distribution over time.

  • Scores the newest snapshot per device.

  • Flags the top ~5% most unusual devices as ALERT.

Tip: Let it collect at least a day of data before you rely on alerts.


4) Automate and alert

Create a tiny wrapper that runs the model and emails root if anything is ALERT.

Save as /usr/local/sbin/raid_ai_score.sh:

#!/usr/bin/env bash
set -euo pipefail

VENV=/opt/raid-ai/venv
PY=$VENV/bin/python
SCRIPT=/usr/local/sbin/raid_ai_score.py
LOG=/var/log/raid-ai/alerts.log

out="$($PY "$SCRIPT" 2>&1 || true)"
ts=$(date -Iseconds)
if [ -n "$out" ]; then
  echo "[$ts]" >> "$LOG"
  echo "$out" | tee -a "$LOG"
  if echo "$out" | grep -q "ALERT"; then
    subject="RAID-AI alerts on $(hostname)"
    if command -v mailx >/dev/null 2>&1; then
      echo "$out" | mailx -s "$subject" root
    elif command -v mail >/dev/null 2>&1; then
      echo "$out" | mail -s "$subject" root
    else
      logger -p daemon.warn "$subject: $out"
    fi
  fi
fi

Permissions:

chmod +x /usr/local/sbin/raid_ai_score.sh

Cron scheduling:

tee /etc/cron.d/raid-ai >/dev/null <<'EOF'
# Collect SMART/RAID metrics every 15 minutes
*/15 * * * * root /usr/local/sbin/raid_ai_collect.sh
# Score anomalies daily at 03:17
17 3 * * * root /usr/local/sbin/raid_ai_score.sh
EOF

Ensure cron is active (see install section). Logs:

  • Raw metrics: /var/log/raid-ai/metrics.jsonl

  • Alerts: /var/log/raid-ai/alerts.log

  • Email to root if configured.


5) Validate and tune

  • Dry run the collector:
/usr/local/sbin/raid_ai_collect.sh && tail -1 /var/log/raid-ai/metrics.jsonl | jq .
  • After 24h, test the scorer:
/usr/local/sbin/raid_ai_score.sh
  • Tune sensitivity:

    • Increase/decrease contamination in raid_ai_score.py (e.g., 0.02 for stricter alerts).
    • Add/remove features as your fleet dictates (e.g., add UDMA CRC errors, wear-leveling counts).
  • Integrate with your stack:

    • Replace mail with your notifier (Slack, webhook, systemd-notify, Prometheus pushgateway).
    • Ship metrics.jsonl to your TSDB and chart top features per device.

Real‑world examples

  • Rising pending sectors: A drive shows a slow, steady rise in attribute 197 (Current_Pending_Sector) with otherwise “PASSED” SMART. The model flags it as anomalous 2 days before the array reports degraded, letting you replace the disk during business hours.

  • NVMe wear surge: percentage_used climbs unusually fast for one NVMe compared to its peers. The anomaly score spikes, prompting workload rebalancing and avoiding an abrupt capacity loss.

  • Silent heat issue: A server with a partly obstructed intake runs 7–10°C hotter than its siblings. The model flags temperature drift before thermal throttling impacts performance.


Conclusion and next steps

AI doesn’t replace good ops hygiene or backups—but it’s a sharp tool for early detection. With a few Bash lines and a small IsolationForest, you can teach your monitoring to “listen” to your disks and RAID arrays.

Your next steps: 1) Install the prerequisites and deploy the collector today. 2) Let it run for a week to learn your baseline. 3) Turn on daily scoring and route ALERTs into your normal paging path. 4) Iterate: add features that matter to your environment and tune sensitivity.

If you found this useful, try extending it with Prometheus/Grafana panels or a systemd timer, and share your improvements back to the team. Your future 2 a.m. self will thank you.