Harden UME/LLDP link ports and canvas styles around UME authority.

Resolve ports from EQ+PTP first, enrich from LLDP when compatible, miss only on valid LLDP evidence, and paint UME solid blue / LLDP dashed / missing red. Also localize new root maps as Root map in English.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-07 16:33:25 +08:00
parent 77832b271d
commit 137597cd25
16 changed files with 736 additions and 152 deletions

View file

@ -491,6 +491,26 @@ class FabricTopologyTests(unittest.TestCase):
self.assertEqual(len(rm.views), 2)
self.assertTrue(all(not getattr(v, "children", None) for v in rm.views))
def test_manual_root_map_name_follows_locale(self) -> None:
"""English UI creates Root map; zh (default) keeps 根图."""
en_root = svc.create_folder(
self.db, TopologyFolderCreate(name="West-EN", kind="region", locale="en")
)
tree = svc.get_topology_tree(self.db)
assert tree.root is not None
west = next(c for c in tree.root.children if c.id == en_root.id)
self.assertEqual(len(west.children), 1)
self.assertEqual(west.children[0].name, "Root map")
self.assertEqual(west.children[0].views[0].name, "Root map")
zh_root = svc.create_folder(
self.db, TopologyFolderCreate(name="East-ZH", kind="region", locale="zh")
)
tree2 = svc.get_topology_tree(self.db)
assert tree2.root is not None
east = next(c for c in tree2.root.children if c.id == zh_root.id)
self.assertEqual(east.children[0].name, "根图")
def test_site_physical_and_custom_flat(self) -> None:
top = svc.create_folder(
self.db, TopologyFolderCreate(name="Site-R", kind="region")
@ -1774,6 +1794,76 @@ Management Addresses:
self.db.delete(ne_b)
self.db.commit()
def test_ume_edge_not_marked_missing_by_lldp_miss(self) -> None:
"""Valid LLDP scan on endpoint must not red-line UME-authority edges."""
suffix = uuid4().hex[:8]
fa, fb, ne_a, ne_b = self._pair_nodes(suffix)
edge, _ = svc.upsert_fabric_edge(
self.db,
a_node_id=fa.id,
b_node_id=fb.id,
a_port="xxvgei-1/1/0/1",
b_port="xxvgei-1/1/0/2",
source="ume",
)
self.db.commit()
self.assertEqual(edge.source, "ume")
newly, purged = svc._apply_missing_and_purge(
self.db,
scanned_ok={fa.id},
touched_edge_ids=set(),
)
self.db.commit()
self.assertEqual(newly, 0)
self.assertEqual(purged, 0)
self.db.refresh(edge)
self.assertEqual(edge.status, "active")
self.assertEqual(edge.source, "ume")
# Dual provenance still UME-protected; LLDP mark is cleared.
edge.attrs = {"sources": ["lldp", "ume"]}
edge.source = "ume"
self.db.commit()
newly, purged = svc._apply_missing_and_purge(
self.db,
scanned_ok={fa.id},
touched_edge_ids=set(),
)
self.db.commit()
self.assertEqual(newly, 0)
self.db.refresh(edge)
self.assertEqual(edge.status, "active")
self.assertEqual(edge.source, "ume")
self.assertNotIn("lldp", (edge.attrs or {}).get("sources", []))
self.assertIn("ume", (edge.attrs or {}).get("sources", []))
# Pure LLDP on same node still miss-eligible.
lldp_edge, _ = svc.upsert_fabric_edge(
self.db,
a_node_id=fa.id,
b_node_id=fb.id,
a_port="Gi0/0",
b_port="Gi0/1",
source="lldp",
)
self.db.commit()
newly, purged = svc._apply_missing_and_purge(
self.db,
scanned_ok={fa.id},
touched_edge_ids=set(),
)
self.db.commit()
self.assertEqual(newly, 1)
self.db.refresh(lldp_edge)
self.assertEqual(lldp_edge.status, "missing")
self.db.refresh(edge)
self.assertEqual(edge.status, "active")
self.db.delete(ne_a)
self.db.delete(ne_b)
self.db.commit()
def test_delete_managed_purges_orphan_fabric_and_edges(self) -> None:
"""Managed-only delete → detach then purge fabric node + incident edges."""
from netx_api import ne_service
@ -1879,7 +1969,7 @@ Management Addresses:
b_node_id=peer.id,
a_port="Gi1/0",
b_port="Gi1/1",
source="lldp",
source="ume",
)
self.db.commit()
edge_id = str(edge.id)
@ -1927,7 +2017,7 @@ Management Addresses:
b_node_id=peer.id,
a_port="Eth1",
b_port="Eth2",
source="lldp",
source="ume",
)
self.db.commit()
edge_id = str(edge.id)

View file

@ -22,6 +22,7 @@ from netx_api.ume_port_normalize import (
extract_ifnames_from_user_label,
port_keys_compatible,
port_suffix_from_tp_ref,
prefer_richer_ifname,
resolve_link_ifnames,
)
from netx_api.ume_topology_apply import (
@ -69,10 +70,78 @@ class UmePortNormalizeTests(unittest.TestCase):
self.assertEqual(a, "1/1/0/28")
self.assertEqual(z, "1/1/0/28")
def test_prefer_richer_ifname(self):
from netx_api.ume_port_normalize import prefer_richer_ifname
self.assertEqual(
prefer_richer_ifname("1/1/0/32", "xxvgei-1/1/0/32"),
"xxvgei-1/1/0/32",
)
self.assertEqual(
prefer_richer_ifname("xxvgei-1/1/0/32", "1/1/0/32"),
"xxvgei-1/1/0/32",
)
self.assertEqual(
prefer_richer_ifname("1/1/0/32", "xxvgei-1/1/0/28"),
"1/1/0/32",
)
def test_port_compatible(self):
self.assertTrue(port_keys_compatible("xxvgei-1/1/0/32", "1/1/0/32"))
self.assertTrue(port_keys_compatible("xxvgei-1/1/0/32", "xxvgei-1/1/0/32"))
self.assertFalse(port_keys_compatible("xxvgei-1/1/0/32", "xxvgei-1/1/0/28"))
# Must not false-match via endswith (11/… vs 1/…).
self.assertFalse(port_keys_compatible("11/1/0/1", "1/1/0/1"))
self.assertFalse(port_keys_compatible("xxvgei-11/1/0/1", "1/1/0/1"))
def test_no_tp_does_not_invent_port_pair(self):
a, z = resolve_link_ifnames(
a_end_tp_ref="ME{a}",
z_end_tp_ref="ME{b}",
user_label="NE-A_xxvgei-1/1/0/32_NE-B_xxvgei-1/1/0/28",
)
self.assertEqual(a, "")
self.assertEqual(z, "")
def test_tp_first_ignores_swapped_neighbor_ports_in_label(self):
"""Real UME label embeds the *peer* port beside each end — order swaps A/Z.
TP pins local ports (A=14, Z=16); media prefix is taken from whichever
token matches that numeric tail (even if it appears under the other end).
"""
label = (
"RSFRL22-RMP01-SMGD[0-1-1]-25GE:14(NNI | RSFRLR1-RMP01 xxvgei-1/1/0/16 "
"| CLARO | FO | RSCSL15-RSNHO06-01)_RSFRLR1-RMP01-SMGD[0-1-1]-25GE:16"
"(NNI | RSFRL22-RMP01 xxvgei-1/1/0/14 | CLARO | FO | RSCSL15-RSNHO06-01)"
)
a_tp = (
"ME{4e598e5d-fe42-4c79-9f62-7d3e5d4eb5b2},EQ={/r=0/sh=1/sl=1},PTP={/p=1_14}"
)
z_tp = (
"ME{b1215371-491a-4641-a1fd-9d59c8002e77},EQ={/r=0/sh=1/sl=1},PTP={/p=1_16}"
)
tokens = extract_ifnames_from_user_label(label)
# Label order is neighbor-first (16 then 14) — must not drive A/Z.
self.assertEqual(tokens[0], "xxvgei-1/1/0/16")
self.assertEqual(tokens[1], "xxvgei-1/1/0/14")
self.assertEqual(port_suffix_from_tp_ref(a_tp), "1/1/0/14")
self.assertEqual(port_suffix_from_tp_ref(z_tp), "1/1/0/16")
a, z = resolve_link_ifnames(
a_end_tp_ref=a_tp, z_end_tp_ref=z_tp, user_label=label
)
self.assertEqual(a, "xxvgei-1/1/0/14")
self.assertEqual(z, "xxvgei-1/1/0/16")
def test_tp_suffix_without_matching_media_stays_bare(self):
a, z = resolve_link_ifnames(
a_end_tp_ref="ME{a},EQ={/r=0/sh=1/sl=1},PTP={/p=1_14}",
z_end_tp_ref="ME{b},EQ={/r=0/sh=1/sl=1},PTP={/p=1_16}",
user_label="no-cli-tokens-here",
)
self.assertEqual(a, "1/1/0/14")
self.assertEqual(z, "1/1/0/16")
self.assertTrue(port_keys_compatible(a, "xxvgei-1/1/0/14"))
self.assertTrue(port_keys_compatible(z, "cgei-1/1/0/16"))
class UmeFabricApplyTests(unittest.TestCase):
@ -241,6 +310,99 @@ class UmeFabricApplyTests(unittest.TestCase):
self.assertEqual(len(manuals), 1)
self.assertEqual(manuals[0].status, "active")
def test_apply_reresolves_and_upgrades_fabric_ports(self):
"""Apply always re-resolves dock ifnames and upgrades compatible Fabric ports."""
from netx_api.ume_port_normalize import numeric_port_tail
self._seed_ume()
apply_ume_topology_to_fabric(self.db)
link = self.db.query(UmeTopoLink).one()
edge = self.db.query(TopoFabricEdge).one()
# Simulate stale bare dock + fabric ports after an older resolve.
link.a_ifname = "1/1/0/32"
link.z_ifname = "1/1/0/28"
edge.a_port = numeric_port_tail(edge.a_port)
edge.b_port = numeric_port_tail(edge.b_port)
self.db.commit()
apply_ume_topology_to_fabric(self.db)
self.db.refresh(link)
self.db.refresh(edge)
self.assertEqual({link.a_ifname, link.z_ifname}, {"xxvgei-1/1/0/32", "xxvgei-1/1/0/28"})
self.assertEqual({edge.a_port, edge.b_port}, {"xxvgei-1/1/0/32", "xxvgei-1/1/0/28"})
def test_lldp_upgrades_bare_ume_ports_and_keeps_ume_primary(self):
from netx_api.ume_port_normalize import numeric_port_tail
self._seed_ume()
# Apply creates bare-capable UME edge with media from label.
apply_ume_topology_to_fabric(self.db)
edge = self.db.query(TopoFabricEdge).one()
# Force bare ports as if UME had no media prefix (keep A/B assignment).
rich_a, rich_b = edge.a_port, edge.b_port
edge.a_port = numeric_port_tail(edge.a_port)
edge.b_port = numeric_port_tail(edge.b_port)
edge.source = "ume"
edge.attrs = {"sources": ["ume"], "ume_link_id": "link-1"}
self.db.commit()
a = (
self.db.query(TopoFabricNode)
.filter(TopoFabricNode.ume_ne_id == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
.one()
)
b = (
self.db.query(TopoFabricNode)
.filter(TopoFabricNode.ume_ne_id == "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
.one()
)
edge2, action = upsert_fabric_edge(
self.db,
a_node_id=a.id,
b_node_id=b.id,
a_port=rich_a,
b_port=rich_b,
source="lldp",
)
self.db.commit()
self.assertEqual(action, "updated")
self.assertIsNotNone(edge2)
assert edge2 is not None
self.assertEqual(edge2.id, edge.id)
self.assertEqual(edge2.source, "ume")
self.assertIn("lldp", (edge2.attrs or {}).get("sources", []))
ports = {edge2.a_port, edge2.b_port}
self.assertEqual(ports, {"xxvgei-1/1/0/32", "xxvgei-1/1/0/28"})
def test_lldp_can_add_edge_between_ume_nes_when_ume_has_no_link(self):
"""UME dump may omit a physical link — LLDP may still create it (dashed)."""
self._seed_ume()
apply_ume_topology_to_fabric(self.db)
a = (
self.db.query(TopoFabricNode)
.filter(TopoFabricNode.ume_ne_id == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
.one()
)
b = (
self.db.query(TopoFabricNode)
.filter(TopoFabricNode.ume_ne_id == "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
.one()
)
before = self.db.query(TopoFabricEdge).count()
edge, action = upsert_fabric_edge(
self.db,
a_node_id=a.id,
b_node_id=b.id,
a_port="xxvgei-1/1/0/99",
b_port="xxvgei-1/1/0/98",
source="lldp",
)
self.db.commit()
self.assertEqual(action, "added")
self.assertIsNotNone(edge)
assert edge is not None
self.assertEqual(edge.source, "lldp")
self.assertEqual(self.db.query(TopoFabricEdge).count(), before + 1)
def test_gap_needs_apply_when_dock_only(self):
self._seed_ume()
self.db.commit()