mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 17:20:44 +08:00
Skip LLDP self-loop edges instead of failing the whole discover apply.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
925726002f
commit
8991bb31d1
4 changed files with 364 additions and 287 deletions
|
|
@ -222,6 +222,9 @@ def _apply_discover_hits(
|
||||||
source="lldp",
|
source="lldp",
|
||||||
now=now,
|
now=now,
|
||||||
)
|
)
|
||||||
|
if edge is None or action == "skipped_self_loop":
|
||||||
|
# Device advertising itself (or peer resolved to same fabric node).
|
||||||
|
continue
|
||||||
touched.append(edge.id)
|
touched.append(edge.id)
|
||||||
replaced.extend(
|
replaced.extend(
|
||||||
_mark_replaced_port_peers(
|
_mark_replaced_port_peers(
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,17 @@ def upsert_fabric_edge(
|
||||||
source: str = "lldp",
|
source: str = "lldp",
|
||||||
layer: str = "physical",
|
layer: str = "physical",
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
) -> tuple[TopoFabricEdge, str]:
|
) -> tuple[TopoFabricEdge | None, str]:
|
||||||
"""Return (edge, action) where action is added|updated|kept_manual."""
|
"""Return (edge, action) where action is added|updated|kept_manual|skipped_self_loop.
|
||||||
|
|
||||||
|
Self-loops are skipped (``(None, \"skipped_self_loop\")``) so LLDP discovery can
|
||||||
|
ignore a device advertising itself without aborting the rest of the scan.
|
||||||
|
Manual edge APIs should treat that action as a client error.
|
||||||
|
"""
|
||||||
now = now or _utcnow()
|
now = now or _utcnow()
|
||||||
a, b, ap, bp = _normalize_endpoints(a_node_id, b_node_id, a_port, b_port)
|
a, b, ap, bp = _normalize_endpoints(a_node_id, b_node_id, a_port, b_port)
|
||||||
if a == b:
|
if a == b:
|
||||||
raise HTTPException(status_code=400, detail="edge_self_loop")
|
return None, "skipped_self_loop"
|
||||||
layer_v = str(layer or "physical").strip() or "physical"
|
layer_v = str(layer or "physical").strip() or "physical"
|
||||||
src = str(source or "lldp").strip().lower() or "lldp"
|
src = str(source or "lldp").strip().lower() or "lldp"
|
||||||
if src == "stale":
|
if src == "stale":
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .db import get_db
|
from .db import get_db
|
||||||
|
|
@ -154,6 +154,8 @@ def api_fabric_manual_edge(
|
||||||
b_port=body.b_port,
|
b_port=body.b_port,
|
||||||
source="manual",
|
source="manual",
|
||||||
)
|
)
|
||||||
|
if edge is None or action == "skipped_self_loop":
|
||||||
|
raise HTTPException(status_code=400, detail="edge_self_loop")
|
||||||
db.commit()
|
db.commit()
|
||||||
refresh_fabric_stats(db)
|
refresh_fabric_stats(db)
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,73 @@ class FabricTopologyTests(unittest.TestCase):
|
||||||
self.assertEqual(calls["n"], 2)
|
self.assertEqual(calls["n"], 2)
|
||||||
self.assertGreaterEqual(int(out.get("edges_added") or 0), 1)
|
self.assertGreaterEqual(int(out.get("edges_added") or 0), 1)
|
||||||
|
|
||||||
|
def test_apply_discover_hits_skips_self_loop(self) -> None:
|
||||||
|
"""LLDP advertising itself must not fail the whole apply; other peers still count."""
|
||||||
|
suffix = uuid4().hex[:8]
|
||||||
|
ne = ManagedNE(
|
||||||
|
id=f"nea-{suffix}",
|
||||||
|
name=f"R-self-{suffix}",
|
||||||
|
vendor="Cisco",
|
||||||
|
device_type="cisco_ios",
|
||||||
|
ip_address=f"203.0.113.{(int(suffix[:2], 16) % 80) + 20}",
|
||||||
|
)
|
||||||
|
peer_ne = ManagedNE(
|
||||||
|
id=f"neb-{suffix}",
|
||||||
|
name=f"r-peer-{suffix}",
|
||||||
|
vendor="Cisco",
|
||||||
|
device_type="cisco_ios",
|
||||||
|
ip_address=f"203.0.113.{(int(suffix[2:4], 16) % 80) + 120}",
|
||||||
|
)
|
||||||
|
self.db.add(ne)
|
||||||
|
self.db.add(peer_ne)
|
||||||
|
self.db.commit()
|
||||||
|
node = svc.ensure_fabric_node_for_managed(self.db, ne)
|
||||||
|
peer = svc.ensure_fabric_node_for_managed(self.db, peer_ne)
|
||||||
|
self.db.commit()
|
||||||
|
hits = [
|
||||||
|
lldp.NeighborHit(
|
||||||
|
remote_name=ne.name,
|
||||||
|
remote_ip=ne.ip_address,
|
||||||
|
local_port="Gi0/0",
|
||||||
|
remote_port="Gi0/0",
|
||||||
|
),
|
||||||
|
lldp.NeighborHit(
|
||||||
|
remote_name=peer_ne.name,
|
||||||
|
remote_ip=peer_ne.ip_address,
|
||||||
|
local_port="Gi0/1",
|
||||||
|
remote_port="Ethernet1/0/1",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
out = svc._apply_discover_hits(
|
||||||
|
self.db,
|
||||||
|
fabric_node_id=node.id,
|
||||||
|
hits=hits,
|
||||||
|
auto_add_unmatched=False,
|
||||||
|
)
|
||||||
|
self.assertTrue(out.get("ok"), out)
|
||||||
|
self.assertEqual(int(out.get("edges_added") or 0), 1)
|
||||||
|
edges = (
|
||||||
|
self.db.query(TopoFabricEdge)
|
||||||
|
.filter(
|
||||||
|
(TopoFabricEdge.a_node_id == node.id) | (TopoFabricEdge.b_node_id == node.id)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
self.assertEqual(len(edges), 1)
|
||||||
|
ids = {edges[0].a_node_id, edges[0].b_node_id}
|
||||||
|
self.assertEqual(ids, {node.id, peer.id})
|
||||||
|
|
||||||
|
skipped, action = svc.upsert_fabric_edge(
|
||||||
|
self.db,
|
||||||
|
a_node_id=node.id,
|
||||||
|
b_node_id=node.id,
|
||||||
|
a_port="Lo0",
|
||||||
|
b_port="Lo0",
|
||||||
|
source="lldp",
|
||||||
|
)
|
||||||
|
self.assertIsNone(skipped)
|
||||||
|
self.assertEqual(action, "skipped_self_loop")
|
||||||
|
|
||||||
def _region(self, name: str = "Test-Region") -> str:
|
def _region(self, name: str = "Test-Region") -> str:
|
||||||
return svc.create_folder(
|
return svc.create_folder(
|
||||||
self.db, TopologyFolderCreate(name=name, kind="region")
|
self.db, TopologyFolderCreate(name=name, kind="region")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue