+ colsHtml += `
`;
+ colsHtml += `
+
+
-
-
Management
-
-
${{esc(mgmtNbr.remPortId || '?')}}
-
-
-
${{esc(shortName)}}
-
${{esc(modelLine)}}
-
MAC ${{esc(mgmtNbr.chassisId || '?')}}
- ${{mgmtNbr.mgmtIPv4 ? `
${{esc(mgmtNbr.mgmtIPv4)}}
` : ''}}
-
+
Management
+
+
${{esc(mgmtNbr.remPortId || '?')}}
+ ${{buildNeighborCard(mgmtNbr, 'lldp-col-remote')}}
`;
}}
- // Build per-port LLDP stats table
- let statsRows = '';
- for (const [port, s] of Object.entries(stats).sort((a,b) => parseInt(a[0]) - parseInt(b[0]))) {{
- const tx = parseInt(s.txFrames || '0').toLocaleString();
- const rx = parseInt(s.rxFrames || '0').toLocaleString();
- const nb = s.neighborsLearned || '0';
- const name = (ifaces[port] || {{}}).ifName || `Port ${{port}}`;
- const hasActive = !!neighborByPort[port];
- const activeMarker = hasActive ? '
(active)' : '';
- statsRows += `
- | ${{parseInt(port) <= 4 ? 'SFP-' + port : 'MGMT'}} |
- ${{esc(name)}} |
- ${{tx}} |
- ${{rx}} |
- ${{nb}}${{activeMarker}} |
-
`;
- }}
-
document.getElementById('sec-lldp').innerHTML = `
${{esc(localName)}} — ${{esc(localModel)}}
- ${{rowsHtml}}
-
- ${{Object.keys(stats).length ? `
-
-
- Per-Port LLDP Statistics
-
-
-
- | Port | Interface | TX Frames |
- RX Frames | Neighbors |
-
- ${{statsRows}}
-
+
+ ${{colsHtml}}
- ` : ''}}
+
`;
}}
@@ -2031,6 +2107,7 @@ function renderPortCmp() {{
// ── Render all sections ──────────────────────────────
renderWalkControl();
renderHeader();
+renderMap();
renderPanel();
renderLldp();
renderInterfaces();
diff --git a/nid-server.py b/nid-server.py
index 254731c..eab23d4 100644
--- a/nid-server.py
+++ b/nid-server.py
@@ -41,7 +41,6 @@ TARGETED_OIDS = [
(".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"),
@@ -79,6 +78,10 @@ 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")
+# Conditionally include heavy policy MIB (~73% of all OIDs)
+if ENV.get("SNMP_WALK_POLICIES", "true").lower() == "true":
+ TARGETED_OIDS.append((".1.3.6.1.4.1.22420.2.3", "ACD-POLICY-MIB"))
+
# ── Walk state (shared across threads) ───────────────────────────────
walk_lock = threading.Lock()
@@ -125,7 +128,7 @@ def build_snmp_auth() -> list:
return ["-v", SNMP_VERSION, "-c", SNMP_COMMUNITY]
-def run_walk(target: str, mode: str):
+def run_walk(target: str, mode: str, policies: bool = True):
"""Execute the full walk pipeline in a background thread."""
global latest_json
@@ -142,6 +145,11 @@ def run_walk(target: str, mode: str):
auth = build_snmp_auth()
t_start = time.time()
+ # Build OID list for this walk — optionally exclude heavy policy MIB
+ walk_oids = list(TARGETED_OIDS)
+ if not policies:
+ walk_oids = [(oid, lbl) for oid, lbl in walk_oids if lbl != "ACD-POLICY-MIB"]
+
try:
# ── Step 1: snmpwalk ──────────────────────────────────────
# Use snmpbulkwalk (GETBULK PDUs) when available — much faster
@@ -157,7 +165,7 @@ def run_walk(target: str, mode: str):
walk_file.write_text(result.stdout)
else:
# Walk subtrees in parallel for speed
- total = len(TARGETED_OIDS)
+ total = len(walk_oids)
completed = [0] # mutable counter for progress
results_map = {}
@@ -175,7 +183,7 @@ def run_walk(target: str, mode: str):
with ThreadPoolExecutor(max_workers=4) as pool:
futures = [
pool.submit(walk_subtree, i, oid, label)
- for i, (oid, label) in enumerate(TARGETED_OIDS)
+ for i, (oid, label) in enumerate(walk_oids)
]
for fut in as_completed(futures):
idx, output = fut.result()
@@ -309,11 +317,39 @@ class NIDHandler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path == "/api/walk":
self._handle_walk()
+ elif self.path == "/api/ping":
+ self._handle_ping()
elif self.path == "/api/clear":
self._handle_clear()
else:
self._send(404, "text/plain", b"Not found")
+ def _handle_ping(self):
+ """Ping a target IP to check reachability."""
+ length = int(self.headers.get("Content-Length", 0))
+ body = json.loads(self.rfile.read(length)) if length else {}
+ target = body.get("target", "").strip()
+
+ if not target:
+ self._send_json(400, {"error": "target IP required"})
+ return
+
+ ip_re = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$")
+ if not ip_re.match(target):
+ self._send_json(400, {"error": f"Invalid IP address: {target}"})
+ return
+
+ try:
+ result = subprocess.run(
+ ["ping", "-c", "1", "-W", "2", target],
+ capture_output=True, text=True, timeout=5,
+ )
+ reachable = result.returncode == 0
+ except (subprocess.TimeoutExpired, FileNotFoundError):
+ reachable = False
+
+ self._send_json(200, {"reachable": reachable, "target": target})
+
def _handle_walk(self):
"""Start a walk in a background thread."""
current, _ = get_status()
@@ -325,6 +361,7 @@ class NIDHandler(BaseHTTPRequestHandler):
body = json.loads(self.rfile.read(length)) if length else {}
target = body.get("target", "").strip()
mode = body.get("mode", "targeted").strip()
+ policies = body.get("policies", True)
if not target:
self._send_json(400, {"error": "target IP required"})
@@ -334,7 +371,7 @@ class NIDHandler(BaseHTTPRequestHandler):
return
set_status("walking", message="Starting walk...", progress=1)
- thread = threading.Thread(target=run_walk, args=(target, mode), daemon=True)
+ thread = threading.Thread(target=run_walk, args=(target, mode, policies), daemon=True)
thread.start()
self._send_json(200, {"status": "started", "target": target, "mode": mode})
diff --git a/snmp-walk.sh b/snmp-walk.sh
index 21f8f96..3228312 100755
--- a/snmp-walk.sh
+++ b/snmp-walk.sh
@@ -75,6 +75,8 @@ fi
# ── Define OID subtrees ──────────────────────────────────────────────
# Targeted: only what the viewer/parser needs
+WALK_POLICIES="${SNMP_WALK_POLICIES:-true}"
+
TARGETED_OIDS=(
.1.3.6.1.2.1.1 # System (sysDescr, sysName, sysUpTime, …)
.1.3.6.1.2.1.2 # IF-MIB (interface table)
@@ -85,13 +87,16 @@ TARGETED_OIDS=(
.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)
)
+if [[ "$WALK_POLICIES" == "true" ]]; then
+ TARGETED_OIDS+=(.1.3.6.1.4.1.22420.2.3) # ACD-POLICY-MIB (~73% of all OIDs)
+fi
+
# ── Prepare output paths ─────────────────────────────────────────────
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
SAFE_IP="${TARGET//./-}"