Add live SNMP walk server, UI controls, and viewer enhancements
- Add nid-server.py: Python web server (stdlib) with live walk API, SSE progress streaming, and clear/archive endpoints - Add snmp-walk.sh: CLI wrapper for walk pipeline with .env config - Add walk control card to viewer UI with IP input, mode selector, walk/clear buttons, and real-time progress bar - Make cards collapsible, add management IP to header - Add dynamic port type rendering (SFP vs RJ45 from connector table) - Add SFF-8024 connector type labels for SFP detail cards - Fix ifOperStatus numeric vs text comparisons for live walk data - Add alarm config-only fallback when device lacks status table - Use snmpbulkwalk for faster walks with parallel subtree execution - Add .env/.env.example for secrets and config, gitignore walks/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dfdbd85bf7
commit
2b10edbb7b
26
.env.example
Normal file
26
.env.example
Normal file
@ -0,0 +1,26 @@
|
||||
# ── SNMP NID Viewer – Environment Configuration ──
|
||||
# Copy to .env and edit as needed. This file is gitignored.
|
||||
|
||||
# Target device IP (override via CLI: ./snmp-walk.sh 10.0.0.1)
|
||||
SNMP_TARGET=10.13.60.102
|
||||
|
||||
# ── SNMPv2c ──
|
||||
SNMP_VERSION=2c
|
||||
SNMP_COMMUNITY=public
|
||||
|
||||
# ── SNMPv3 (future – uncomment and set when needed) ──
|
||||
# SNMP_VERSION=3
|
||||
# SNMP_V3_USER=
|
||||
# SNMP_V3_AUTH_PROTO=SHA # MD5 | SHA | SHA-256 | SHA-512
|
||||
# SNMP_V3_AUTH_PASS=
|
||||
# SNMP_V3_PRIV_PROTO=AES # DES | AES | AES-256
|
||||
# SNMP_V3_PRIV_PASS=
|
||||
# SNMP_V3_SEC_LEVEL=authPriv # noAuthNoPriv | authNoPriv | authPriv
|
||||
|
||||
# ── Walk mode ──
|
||||
# "full" = walk entire .1 tree (captures everything, ~27% larger)
|
||||
# "targeted" = walk only subtrees used by the viewer (faster)
|
||||
SNMP_WALK_MODE=targeted
|
||||
|
||||
# ── Server ──
|
||||
SERVER_PORT=5525
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Environment secrets
|
||||
.env
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
||||
# Walk data (device-specific, generated)
|
||||
walks/
|
||||
File diff suppressed because it is too large
Load Diff
374
nid-server.py
Normal file
374
nid-server.py
Normal file
@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SNMP NID Viewer — Web Server
|
||||
|
||||
Serves the NID viewer HTML and provides API endpoints to trigger
|
||||
live SNMP walks from the browser UI.
|
||||
|
||||
Routes:
|
||||
GET / — Viewer page (built from latest monitoring JSON)
|
||||
POST /api/walk — Trigger a walk { "target": "x.x.x.x", "mode": "targeted"|"full" }
|
||||
GET /api/status — SSE stream of walk progress
|
||||
|
||||
Usage:
|
||||
python3 nid-server.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
WALKS_DIR = SCRIPT_DIR / "walks"
|
||||
|
||||
# ── OID subtrees for targeted walk (mirrors snmp-walk.sh) ────────────
|
||||
TARGETED_OIDS = [
|
||||
(".1.3.6.1.2.1.1", "System"),
|
||||
(".1.3.6.1.2.1.2", "IF-MIB"),
|
||||
(".1.3.6.1.2.1.4", "IP-MIB"),
|
||||
(".1.3.6.1.2.1.31", "IF-MIB-X"),
|
||||
(".1.3.6.1.2.1.55", "IPv6-MIB"),
|
||||
(".1.3.111.2.802.1.1.13", "LLDP-MIB"),
|
||||
(".1.3.6.1.4.1.22420.1.1", "ACD-DESC-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.1", "ACD-ALARM-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.2", "ACD-FILTER-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.3", "ACD-POLICY-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.4", "ACD-SFP-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.6", "ACD-REGULATOR-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.8", "ACD-SMAP-MIB"),
|
||||
(".1.3.6.1.4.1.22420.2.9", "ACD-PORT-MIB"),
|
||||
]
|
||||
|
||||
# ── Environment / config ─────────────────────────────────────────────
|
||||
|
||||
def load_env(path: Path) -> dict:
|
||||
"""Parse a .env file into a dict (simple key=value, skip comments)."""
|
||||
env = {}
|
||||
if not path.is_file():
|
||||
return env
|
||||
for line in path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, _, val = line.partition("=")
|
||||
env[key.strip()] = val.strip().strip('"').strip("'")
|
||||
return env
|
||||
|
||||
|
||||
ENV = load_env(SCRIPT_DIR / ".env")
|
||||
|
||||
SERVER_PORT = int(ENV.get("SERVER_PORT", "5525"))
|
||||
SNMP_VERSION = ENV.get("SNMP_VERSION", "2c")
|
||||
SNMP_COMMUNITY = ENV.get("SNMP_COMMUNITY", "public")
|
||||
# SNMPv3 fields (future)
|
||||
SNMP_V3_USER = ENV.get("SNMP_V3_USER", "")
|
||||
SNMP_V3_AUTH_PROTO = ENV.get("SNMP_V3_AUTH_PROTO", "SHA")
|
||||
SNMP_V3_AUTH_PASS = ENV.get("SNMP_V3_AUTH_PASS", "")
|
||||
SNMP_V3_PRIV_PROTO = ENV.get("SNMP_V3_PRIV_PROTO", "AES")
|
||||
SNMP_V3_PRIV_PASS = ENV.get("SNMP_V3_PRIV_PASS", "")
|
||||
SNMP_V3_SEC_LEVEL = ENV.get("SNMP_V3_SEC_LEVEL", "authPriv")
|
||||
|
||||
# ── Walk state (shared across threads) ───────────────────────────────
|
||||
|
||||
walk_lock = threading.Lock()
|
||||
walk_status = {
|
||||
"state": "idle", # idle | walking | parsing | building | complete | error
|
||||
"message": "",
|
||||
"progress": 0, # 0-100
|
||||
"timestamp": None,
|
||||
"lines": 0,
|
||||
"elapsed": 0,
|
||||
}
|
||||
# Monotonically increasing version so SSE clients know when state changed
|
||||
walk_version = 0
|
||||
# Path to latest monitoring JSON (set after successful walk)
|
||||
latest_json = None
|
||||
|
||||
|
||||
def set_status(state, message="", progress=0, **extra):
|
||||
global walk_version
|
||||
with walk_lock:
|
||||
walk_status["state"] = state
|
||||
walk_status["message"] = message
|
||||
walk_status["progress"] = progress
|
||||
walk_status.update(extra)
|
||||
walk_version += 1
|
||||
|
||||
|
||||
def get_status():
|
||||
with walk_lock:
|
||||
return dict(walk_status), walk_version
|
||||
|
||||
|
||||
# ── SNMP walk execution ──────────────────────────────────────────────
|
||||
|
||||
def build_snmp_auth() -> list:
|
||||
"""Build snmpwalk authentication flags from env config."""
|
||||
if SNMP_VERSION == "3":
|
||||
args = ["-v3", "-u", SNMP_V3_USER, "-l", SNMP_V3_SEC_LEVEL]
|
||||
if SNMP_V3_SEC_LEVEL != "noAuthNoPriv":
|
||||
args += ["-a", SNMP_V3_AUTH_PROTO, "-A", SNMP_V3_AUTH_PASS]
|
||||
if SNMP_V3_SEC_LEVEL == "authPriv":
|
||||
args += ["-x", SNMP_V3_PRIV_PROTO, "-X", SNMP_V3_PRIV_PASS]
|
||||
return args
|
||||
return ["-v", SNMP_VERSION, "-c", SNMP_COMMUNITY]
|
||||
|
||||
|
||||
def run_walk(target: str, mode: str):
|
||||
"""Execute the full walk pipeline in a background thread."""
|
||||
global latest_json
|
||||
|
||||
ip_re = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$")
|
||||
if not ip_re.match(target):
|
||||
set_status("error", message=f"Invalid IP address: {target}")
|
||||
return
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
safe_ip = target.replace(".", "-")
|
||||
walk_file = WALKS_DIR / f"{safe_ip}_{timestamp}_walk.txt"
|
||||
WALKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
auth = build_snmp_auth()
|
||||
t_start = time.time()
|
||||
|
||||
try:
|
||||
# ── Step 1: snmpwalk ──────────────────────────────────────
|
||||
# Use snmpbulkwalk (GETBULK PDUs) when available — much faster
|
||||
walk_cmd = "snmpbulkwalk" if shutil.which("snmpbulkwalk") else "snmpwalk"
|
||||
bulk_args = [] # use snmpbulkwalk default (-Cr10); higher values truncate on some devices
|
||||
|
||||
if mode == "full":
|
||||
set_status("walking", message="Walking full OID tree (.1)", progress=5)
|
||||
result = subprocess.run(
|
||||
[walk_cmd, "-On", "-OQ"] + bulk_args + auth + [target, ".1"],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
walk_file.write_text(result.stdout)
|
||||
else:
|
||||
# Walk subtrees in parallel for speed
|
||||
total = len(TARGETED_OIDS)
|
||||
completed = [0] # mutable counter for progress
|
||||
results_map = {}
|
||||
|
||||
def walk_subtree(idx, oid, label):
|
||||
res = subprocess.run(
|
||||
[walk_cmd, "-On", "-OQ"] + bulk_args + auth + [target, oid],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
completed[0] += 1
|
||||
pct = int((completed[0] / total) * 70)
|
||||
set_status("walking", message=f"Walking subtrees ({completed[0]}/{total})", progress=pct)
|
||||
return idx, res.stdout
|
||||
|
||||
set_status("walking", message=f"Walking {total} subtrees in parallel", progress=5)
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
futures = [
|
||||
pool.submit(walk_subtree, i, oid, label)
|
||||
for i, (oid, label) in enumerate(TARGETED_OIDS)
|
||||
]
|
||||
for fut in as_completed(futures):
|
||||
idx, output = fut.result()
|
||||
if output.strip():
|
||||
results_map[idx] = output
|
||||
|
||||
# Reassemble in OID order
|
||||
output_lines = [results_map[i] for i in sorted(results_map)]
|
||||
walk_file.write_text("\n".join(output_lines))
|
||||
|
||||
line_count = sum(1 for _ in walk_file.open())
|
||||
elapsed = round(time.time() - t_start, 1)
|
||||
set_status("walking", message=f"Walk complete: {line_count:,} lines in {elapsed}s",
|
||||
progress=72, lines=line_count)
|
||||
|
||||
if line_count == 0:
|
||||
set_status("error", message=f"Walk returned no data — check reachability and credentials",
|
||||
elapsed=elapsed)
|
||||
return
|
||||
|
||||
# ── Step 2: snmp-parse.py ─────────────────────────────────
|
||||
set_status("parsing", message="Parsing SNMP data", progress=78)
|
||||
parse_result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT_DIR / "snmp-parse.py"), str(walk_file)],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
if parse_result.returncode != 0:
|
||||
set_status("error", message=f"Parse failed: {parse_result.stderr[:200]}")
|
||||
return
|
||||
|
||||
monitoring_json = walk_file.with_name(walk_file.stem + "_monitoring.json")
|
||||
if not monitoring_json.is_file():
|
||||
set_status("error", message="Parser did not produce monitoring JSON")
|
||||
return
|
||||
|
||||
latest_json = monitoring_json
|
||||
elapsed = round(time.time() - t_start, 1)
|
||||
set_status("complete",
|
||||
message=f"Done — {line_count:,} lines in {elapsed}s",
|
||||
progress=100, lines=line_count, elapsed=elapsed,
|
||||
timestamp=datetime.now().isoformat())
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
set_status("error", message="Walk timed out — device may be unreachable")
|
||||
except Exception as e:
|
||||
set_status("error", message=str(e)[:300])
|
||||
|
||||
|
||||
# ── Find latest monitoring JSON ──────────────────────────────────────
|
||||
|
||||
def find_latest_json() -> Path | None:
|
||||
"""Return the most recent *_monitoring.json in walks/."""
|
||||
if latest_json and latest_json.is_file():
|
||||
return latest_json
|
||||
candidates = sorted(WALKS_DIR.glob("*_monitoring.json"), key=lambda p: p.stat().st_mtime)
|
||||
return candidates[-1] if candidates else None
|
||||
|
||||
|
||||
# ── HTTP handler ─────────────────────────────────────────────────────
|
||||
|
||||
class NIDHandler(BaseHTTPRequestHandler):
|
||||
"""Serve viewer and handle walk API."""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# Cleaner logging
|
||||
print(f"[{self.log_date_time_string()}] {format % args}")
|
||||
|
||||
def _send(self, code, content_type, body):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _send_json(self, code, obj):
|
||||
self._send(code, "application/json", json.dumps(obj).encode())
|
||||
|
||||
# ── GET ────────────────────────────────────────────────────────
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/":
|
||||
self._serve_viewer()
|
||||
elif self.path == "/api/status":
|
||||
self._serve_sse()
|
||||
else:
|
||||
self._send(404, "text/plain", b"Not found")
|
||||
|
||||
def _serve_viewer(self):
|
||||
"""Build and serve the viewer HTML from latest data."""
|
||||
# Import build_html lazily to avoid circular issues at module level
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from build_nid_viewer import build_html
|
||||
|
||||
json_path = find_latest_json()
|
||||
if json_path:
|
||||
with json_path.open() as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
data = {}
|
||||
|
||||
html = build_html(data)
|
||||
self._send(200, "text/html; charset=utf-8", html.encode())
|
||||
|
||||
def _serve_sse(self):
|
||||
"""Stream walk status as Server-Sent Events."""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Connection", "keep-alive")
|
||||
self.end_headers()
|
||||
|
||||
last_version = -1
|
||||
try:
|
||||
while True:
|
||||
status, version = get_status()
|
||||
if version != last_version:
|
||||
msg = f"data: {json.dumps(status)}\n\n"
|
||||
self.wfile.write(msg.encode())
|
||||
self.wfile.flush()
|
||||
last_version = version
|
||||
# Close SSE after terminal states so client triggers reload
|
||||
if status["state"] in ("complete", "error"):
|
||||
break
|
||||
time.sleep(0.3)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
|
||||
# ── POST ───────────────────────────────────────────────────────
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/api/walk":
|
||||
self._handle_walk()
|
||||
elif self.path == "/api/clear":
|
||||
self._handle_clear()
|
||||
else:
|
||||
self._send(404, "text/plain", b"Not found")
|
||||
|
||||
def _handle_walk(self):
|
||||
"""Start a walk in a background thread."""
|
||||
current, _ = get_status()
|
||||
if current["state"] in ("walking", "parsing"):
|
||||
self._send_json(409, {"error": "Walk already in progress"})
|
||||
return
|
||||
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length)) if length else {}
|
||||
target = body.get("target", "").strip()
|
||||
mode = body.get("mode", "targeted").strip()
|
||||
|
||||
if not target:
|
||||
self._send_json(400, {"error": "target IP required"})
|
||||
return
|
||||
if mode not in ("targeted", "full"):
|
||||
self._send_json(400, {"error": "mode must be 'targeted' or 'full'"})
|
||||
return
|
||||
|
||||
set_status("walking", message="Starting walk...", progress=1)
|
||||
thread = threading.Thread(target=run_walk, args=(target, mode), daemon=True)
|
||||
thread.start()
|
||||
self._send_json(200, {"status": "started", "target": target, "mode": mode})
|
||||
|
||||
def _handle_clear(self):
|
||||
"""Move all walk data to walks/archive/ and reset state."""
|
||||
global latest_json
|
||||
archive = WALKS_DIR / "archive"
|
||||
archive.mkdir(parents=True, exist_ok=True)
|
||||
moved = 0
|
||||
for f in WALKS_DIR.iterdir():
|
||||
if f.is_file():
|
||||
f.rename(archive / f.name)
|
||||
moved += 1
|
||||
latest_json = None
|
||||
set_status("idle")
|
||||
self._send_json(200, {"status": "cleared", "files_archived": moved})
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
server = ThreadingHTTPServer(("0.0.0.0", SERVER_PORT), NIDHandler)
|
||||
print(f"╔═══════════════════════════════════════════════════╗")
|
||||
print(f"║ SNMP NID Viewer — Web Server ║")
|
||||
print(f"║ http://localhost:{SERVER_PORT:<5} ║")
|
||||
print(f"╚═══════════════════════════════════════════════════╝")
|
||||
print(f" SNMPv{SNMP_VERSION} | Press Ctrl+C to stop")
|
||||
print()
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down.")
|
||||
server.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -995,6 +995,18 @@ def build_monitoring_output(walk_data: dict, resolver: OIDResolver) -> dict:
|
||||
if ip_addresses:
|
||||
output["ip_addresses"] = ip_addresses
|
||||
|
||||
# Back-fill interfaces for any ifIndex referenced by IP addresses
|
||||
# but missing from the IF-MIB walk (common on Accedian virtual/internal interfaces).
|
||||
# Mark them as synthetic so the viewer can distinguish them from real interfaces.
|
||||
for ip_entry in ip_addresses.values():
|
||||
if_idx = ip_entry.get("ifIndex", "")
|
||||
if if_idx and if_idx not in interfaces:
|
||||
interfaces[if_idx] = OrderedDict([
|
||||
("ifDescr", f"Virtual ({if_idx})"),
|
||||
("ifName", f"Virtual ({if_idx})"),
|
||||
("synthetic", "1"),
|
||||
])
|
||||
|
||||
# ── 23. LLDP Neighbors (structured) ──
|
||||
LLDP_PREFIX = ".1.3.111.2.802.1.1.13"
|
||||
LLDP_REM_TABLE = LLDP_PREFIX + ".1.4.1.1." # lldpRemTable columns
|
||||
|
||||
175
snmp-walk.sh
Executable file
175
snmp-walk.sh
Executable file
@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env bash
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# snmp-walk.sh — Live SNMP walk → parse → HTML viewer pipeline
|
||||
#
|
||||
# Usage:
|
||||
# ./snmp-walk.sh # uses SNMP_TARGET from .env
|
||||
# ./snmp-walk.sh 10.0.0.1 # override target IP
|
||||
# ./snmp-walk.sh 10.0.0.1 full # override target + walk mode
|
||||
#
|
||||
# Reads configuration from .env (community, version, walk mode).
|
||||
# Produces: walks/{IP}_{TIMESTAMP}_walk.txt
|
||||
# walks/{IP}_{TIMESTAMP}_walk_monitoring.json
|
||||
# walks/nid-viewer.html
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||
|
||||
# ── Load .env ────────────────────────────────────────────────────────
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
else
|
||||
echo "Warning: ${ENV_FILE} not found — using defaults" >&2
|
||||
fi
|
||||
|
||||
# ── Resolve parameters (CLI overrides .env) ──────────────────────────
|
||||
TARGET="${1:-${SNMP_TARGET:-}}"
|
||||
WALK_MODE="${2:-${SNMP_WALK_MODE:-targeted}}"
|
||||
VERSION="${SNMP_VERSION:-2c}"
|
||||
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
echo "Error: No target IP specified."
|
||||
echo " Set SNMP_TARGET in .env or pass as argument: $0 <ip>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Validate prerequisites ───────────────────────────────────────────
|
||||
for cmd in snmpwalk python3; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
echo "Error: '$cmd' not found. Please install it." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Prefer snmpbulkwalk (GETBULK PDUs — much faster) over snmpwalk (GETNEXT)
|
||||
if command -v snmpbulkwalk &>/dev/null; then
|
||||
WALK_CMD=snmpbulkwalk
|
||||
BULK_ARGS=() # use default -Cr10; higher values truncate on some devices
|
||||
else
|
||||
WALK_CMD=snmpwalk
|
||||
BULK_ARGS=()
|
||||
fi
|
||||
|
||||
# ── Build snmpwalk auth flags ────────────────────────────────────────
|
||||
SNMP_AUTH=()
|
||||
if [[ "$VERSION" == "3" ]]; then
|
||||
SNMP_AUTH+=(-v3)
|
||||
SNMP_AUTH+=(-u "${SNMP_V3_USER:?SNMP_V3_USER required for v3}")
|
||||
SNMP_AUTH+=(-l "${SNMP_V3_SEC_LEVEL:-authPriv}")
|
||||
if [[ "${SNMP_V3_SEC_LEVEL:-authPriv}" != "noAuthNoPriv" ]]; then
|
||||
SNMP_AUTH+=(-a "${SNMP_V3_AUTH_PROTO:-SHA}")
|
||||
SNMP_AUTH+=(-A "${SNMP_V3_AUTH_PASS:?SNMP_V3_AUTH_PASS required}")
|
||||
fi
|
||||
if [[ "${SNMP_V3_SEC_LEVEL:-authPriv}" == "authPriv" ]]; then
|
||||
SNMP_AUTH+=(-x "${SNMP_V3_PRIV_PROTO:-AES}")
|
||||
SNMP_AUTH+=(-X "${SNMP_V3_PRIV_PASS:?SNMP_V3_PRIV_PASS required}")
|
||||
fi
|
||||
else
|
||||
SNMP_AUTH+=(-v "${VERSION}" -c "${SNMP_COMMUNITY:-public}")
|
||||
fi
|
||||
|
||||
# ── Define OID subtrees ──────────────────────────────────────────────
|
||||
# Targeted: only what the viewer/parser needs
|
||||
TARGETED_OIDS=(
|
||||
.1.3.6.1.2.1.1 # System (sysDescr, sysName, sysUpTime, …)
|
||||
.1.3.6.1.2.1.2 # IF-MIB (interface table)
|
||||
.1.3.6.1.2.1.4 # IP-MIB (addresses, routes)
|
||||
.1.3.6.1.2.1.31 # IF-MIB extensions (ifName, ifAlias, 64-bit counters)
|
||||
.1.3.6.1.2.1.55 # IPv6-MIB
|
||||
.1.3.111.2.802.1.1.13 # LLDP-MIB (neighbors, stats)
|
||||
.1.3.6.1.4.1.22420.1.1 # ACD-DESC-MIB (device identity, connectors, sensors)
|
||||
.1.3.6.1.4.1.22420.2.1 # ACD-ALARM-MIB
|
||||
.1.3.6.1.4.1.22420.2.2 # ACD-FILTER-MIB
|
||||
.1.3.6.1.4.1.22420.2.3 # ACD-POLICY-MIB (L2 policies, stats)
|
||||
.1.3.6.1.4.1.22420.2.4 # ACD-SFP-MIB (transceiver info/diag)
|
||||
.1.3.6.1.4.1.22420.2.6 # ACD-REGULATOR-MIB
|
||||
.1.3.6.1.4.1.22420.2.8 # ACD-SMAP-MIB (CoS profiles)
|
||||
.1.3.6.1.4.1.22420.2.9 # ACD-PORT-MIB (port config/status)
|
||||
)
|
||||
|
||||
# ── Prepare output paths ─────────────────────────────────────────────
|
||||
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
|
||||
SAFE_IP="${TARGET//./-}"
|
||||
WALKS_DIR="${SCRIPT_DIR}/walks"
|
||||
WALK_FILE="${WALKS_DIR}/${SAFE_IP}_${TIMESTAMP}_walk.txt"
|
||||
ERROR_FILE="${WALKS_DIR}/${SAFE_IP}_${TIMESTAMP}_errors.txt"
|
||||
|
||||
mkdir -p "$WALKS_DIR"
|
||||
|
||||
# ── Execute SNMP walk ─────────────────────────────────────────────────
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
echo " SNMP NID Viewer — Live Walk"
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
echo " Target: ${TARGET}"
|
||||
echo " Version: SNMPv${VERSION}"
|
||||
echo " Mode: ${WALK_MODE}"
|
||||
echo " Walker: ${WALK_CMD}${BULK_ARGS:+ (max-rep=${BULK_ARGS[1]#-Cr})}"
|
||||
echo " Output: ${WALK_FILE}"
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
WALK_START="$(date +%s)"
|
||||
|
||||
if [[ "$WALK_MODE" == "full" ]]; then
|
||||
echo "[1/3] Walking full OID tree (.1) ..."
|
||||
"$WALK_CMD" -On -OQ "${BULK_ARGS[@]}" "${SNMP_AUTH[@]}" "$TARGET" .1 \
|
||||
> "$WALK_FILE" 2> "$ERROR_FILE" || true
|
||||
else
|
||||
echo "[1/3] Walking ${#TARGETED_OIDS[@]} targeted subtrees ..."
|
||||
: > "$WALK_FILE"
|
||||
: > "$ERROR_FILE"
|
||||
for oid in "${TARGETED_OIDS[@]}"; do
|
||||
echo " ↳ ${oid}"
|
||||
"$WALK_CMD" -On -OQ "${BULK_ARGS[@]}" "${SNMP_AUTH[@]}" "$TARGET" "$oid" \
|
||||
>> "$WALK_FILE" 2>> "$ERROR_FILE" || true
|
||||
done
|
||||
fi
|
||||
|
||||
WALK_END="$(date +%s)"
|
||||
WALK_LINES="$(wc -l < "$WALK_FILE")"
|
||||
WALK_SECS=$(( WALK_END - WALK_START ))
|
||||
echo " ✓ Walk complete: ${WALK_LINES} lines in ${WALK_SECS}s"
|
||||
|
||||
# Remove empty error file
|
||||
if [[ ! -s "$ERROR_FILE" ]]; then
|
||||
rm -f "$ERROR_FILE"
|
||||
fi
|
||||
|
||||
if [[ "$WALK_LINES" -eq 0 ]]; then
|
||||
echo ""
|
||||
echo "Error: Walk returned no data. Check:"
|
||||
echo " - Device reachability (ping ${TARGET})"
|
||||
echo " - SNMP community/credentials in .env"
|
||||
echo " - Firewall rules (UDP 161)"
|
||||
[[ -s "$ERROR_FILE" ]] && echo "" && cat "$ERROR_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Parse walk → monitoring JSON ──────────────────────────────────────
|
||||
echo ""
|
||||
echo "[2/3] Parsing walk data ..."
|
||||
python3 "${SCRIPT_DIR}/snmp-parse.py" "$WALK_FILE"
|
||||
|
||||
MONITORING_JSON="${WALK_FILE%.txt}_monitoring.json"
|
||||
if [[ ! -f "$MONITORING_JSON" ]]; then
|
||||
echo "Error: snmp-parse.py did not produce ${MONITORING_JSON}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ Monitoring JSON: ${MONITORING_JSON}"
|
||||
|
||||
# ── Build HTML viewer ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[3/3] Building HTML viewer ..."
|
||||
python3 "${SCRIPT_DIR}/build_nid_viewer.py" "$MONITORING_JSON"
|
||||
echo " ✓ Viewer: ${WALKS_DIR}/nid-viewer.html"
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
echo " Done! Open walks/nid-viewer.html in a browser."
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
@ -1 +0,0 @@
|
||||
Unknown flag passed to -C: r
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user