mirror of
https://github.com/hansjone/netx.git
synced 2026-10-12 04:10:47 +08:00
Parse onsite UME TopoNode FDN names when nodeId is missing.
Live TopoNodes use MD=...;ME=<uuid> / SBN=<uuid> without nodeId; extract those uuids as stable PKs and parent refs so reconcile drops old name:hash rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cd7d1fc6e7
commit
5d705fa7bb
2 changed files with 133 additions and 4 deletions
|
|
@ -26,18 +26,34 @@ _TOPOLOGY_SYNC_LOCK = threading.Lock()
|
||||||
_UUID_RE = re.compile(
|
_UUID_RE = re.compile(
|
||||||
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
||||||
)
|
)
|
||||||
|
# Onsite FDN: MD=ZTE/UME(BN);ME=0ab0b408-... or ;SBN=47173499-...
|
||||||
|
_ME_EQ_RE = re.compile(
|
||||||
|
r"(?:^|[;,/])ME=([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_SBN_EQ_RE = re.compile(
|
||||||
|
r"(?:^|[;,/])SBN=([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
_ME_BRACE_RE = re.compile(
|
_ME_BRACE_RE = re.compile(
|
||||||
r"ME\{([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\}",
|
r"ME\{([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\}",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
_SBN_BRACE_RE = re.compile(
|
||||||
|
r"SBN\{([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\}",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
_PTP_RE = re.compile(r"PTP=\{([^}]*)\}", re.IGNORECASE)
|
_PTP_RE = re.compile(r"PTP=\{([^}]*)\}", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def extract_me_uuid(text: str) -> str:
|
def extract_me_uuid(text: str) -> str:
|
||||||
"""Extract managed-element uuid from TP ref or TOPO_NODE_ME* name."""
|
"""Extract managed-element uuid from FDN / TP ref / TOPO_NODE_ME* name."""
|
||||||
s = str(text or "").strip()
|
s = str(text or "").strip()
|
||||||
if not s:
|
if not s:
|
||||||
return ""
|
return ""
|
||||||
|
m = _ME_EQ_RE.search(s)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
m = _ME_BRACE_RE.search(s)
|
m = _ME_BRACE_RE.search(s)
|
||||||
if m:
|
if m:
|
||||||
return m.group(1)
|
return m.group(1)
|
||||||
|
|
@ -48,6 +64,26 @@ def extract_me_uuid(text: str) -> str:
|
||||||
m2 = _UUID_RE.search(rest)
|
m2 = _UUID_RE.search(rest)
|
||||||
if m2:
|
if m2:
|
||||||
return m2.group(0)
|
return m2.group(0)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def extract_topo_object_uuid(text: str) -> str:
|
||||||
|
"""Extract ME/SBN uuid from onsite FDN name (preferred stable node_id)."""
|
||||||
|
s = str(text or "").strip()
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
for cre in (_ME_EQ_RE, _SBN_EQ_RE, _ME_BRACE_RE, _SBN_BRACE_RE):
|
||||||
|
m = cre.search(s)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
low = s.upper()
|
||||||
|
for prefix in ("TOPO_NODE_ME", "TOPO_NODE_SBN"):
|
||||||
|
if prefix in low:
|
||||||
|
idx = low.find(prefix)
|
||||||
|
rest = s[idx + len(prefix) :]
|
||||||
|
m2 = _UUID_RE.search(rest)
|
||||||
|
if m2:
|
||||||
|
return m2.group(0)
|
||||||
m3 = _UUID_RE.search(s)
|
m3 = _UUID_RE.search(s)
|
||||||
return m3.group(0) if m3 else ""
|
return m3.group(0) if m3 else ""
|
||||||
|
|
||||||
|
|
@ -86,17 +122,27 @@ def _link_id_from_row(row: dict[str, Any]) -> str:
|
||||||
name = _s(_pick(row, "name"))
|
name = _s(_pick(row, "name"))
|
||||||
if not name:
|
if not name:
|
||||||
return ""
|
return ""
|
||||||
|
# Prefer endpoint ME uuids embedded in TL name when linkId is absent.
|
||||||
|
obj = extract_topo_object_uuid(name)
|
||||||
|
if obj:
|
||||||
|
# Keep uniqueness for nameless links by hashing full name but avoid bare name: when possible
|
||||||
|
digest = hashlib.sha1(name.encode("utf-8", errors="replace")).hexdigest()[:40]
|
||||||
|
return f"tl:{digest}"[:128]
|
||||||
digest = hashlib.sha1(name.encode("utf-8", errors="replace")).hexdigest()[:40]
|
digest = hashlib.sha1(name.encode("utf-8", errors="replace")).hexdigest()[:40]
|
||||||
return f"name:{digest}"[:128]
|
return f"name:{digest}"[:128]
|
||||||
|
|
||||||
|
|
||||||
def _node_id_from_row(row: dict[str, Any]) -> str:
|
def _node_id_from_row(row: dict[str, Any]) -> str:
|
||||||
|
"""Stable PK: nodeId if present, else ME=/SBN= uuid from onsite FDN ``name``."""
|
||||||
nid = _s(_pick(row, "nodeId", "node-id", "node_id", "id"))
|
nid = _s(_pick(row, "nodeId", "node-id", "node_id", "id"))
|
||||||
if nid:
|
if nid:
|
||||||
return nid[:128]
|
return nid[:128]
|
||||||
name = _s(_pick(row, "name"))
|
name = _s(_pick(row, "name"))
|
||||||
if not name:
|
if not name:
|
||||||
return ""
|
return ""
|
||||||
|
obj = extract_topo_object_uuid(name)
|
||||||
|
if obj:
|
||||||
|
return obj[:128]
|
||||||
digest = hashlib.sha1(name.encode("utf-8", errors="replace")).hexdigest()[:40]
|
digest = hashlib.sha1(name.encode("utf-8", errors="replace")).hexdigest()[:40]
|
||||||
return f"name:{digest}"[:128]
|
return f"name:{digest}"[:128]
|
||||||
|
|
||||||
|
|
@ -105,7 +151,16 @@ def _ume_ne_id_for_topo_node(*, node_type: str, name: str) -> str:
|
||||||
nt = str(node_type or "").strip().upper()
|
nt = str(node_type or "").strip().upper()
|
||||||
if nt != "TOPO_NODE_ME":
|
if nt != "TOPO_NODE_ME":
|
||||||
return ""
|
return ""
|
||||||
return extract_me_uuid(name)
|
return extract_me_uuid(name) or extract_topo_object_uuid(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_parent_node(raw: str) -> str:
|
||||||
|
"""Store parent as ME/SBN uuid when parentNode is an onsite FDN string."""
|
||||||
|
s = _s(raw)
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
obj = extract_topo_object_uuid(s)
|
||||||
|
return (obj or s)[:512]
|
||||||
|
|
||||||
|
|
||||||
def _stale_running_sec() -> int:
|
def _stale_running_sec() -> int:
|
||||||
|
|
@ -263,7 +318,9 @@ def _upsert_topo_node(work: Session, node_id: str, row: dict[str, Any], *, now)
|
||||||
existing.node_type = node_type[:64]
|
existing.node_type = node_type[:64]
|
||||||
existing.user_label = _s(_pick(row, "userLabel", "user-label", "user_label"))[:512]
|
existing.user_label = _s(_pick(row, "userLabel", "user-label", "user_label"))[:512]
|
||||||
existing.owner = _s(_pick(row, "owner"))[:64]
|
existing.owner = _s(_pick(row, "owner"))[:64]
|
||||||
existing.parent_node = _s(_pick(row, "parentNode", "parent-node", "parent_node"))[:512]
|
existing.parent_node = _normalize_parent_node(
|
||||||
|
_s(_pick(row, "parentNode", "parent-node", "parent_node"))
|
||||||
|
)
|
||||||
existing.x_pos = _as_optional_int(_pick(row, "xPos", "x-pos", "x_pos"))
|
existing.x_pos = _as_optional_int(_pick(row, "xPos", "x-pos", "x_pos"))
|
||||||
existing.y_pos = _as_optional_int(_pick(row, "yPos", "y-pos", "y_pos"))
|
existing.y_pos = _as_optional_int(_pick(row, "yPos", "y-pos", "y_pos"))
|
||||||
existing.ume_ne_id = _ume_ne_id_for_topo_node(node_type=node_type, name=name)[:128]
|
existing.ume_ne_id = _ume_ne_id_for_topo_node(node_type=node_type, name=name)[:128]
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,12 @@ from netx_api.ume_sync_service import (
|
||||||
sync_inventory_full,
|
sync_inventory_full,
|
||||||
sync_topology_full,
|
sync_topology_full,
|
||||||
)
|
)
|
||||||
from netx_api.ume_sync_topology import extract_me_uuid, extract_ptp
|
from netx_api.ume_sync_topology import (
|
||||||
|
extract_me_uuid,
|
||||||
|
extract_ptp,
|
||||||
|
extract_topo_object_uuid,
|
||||||
|
_node_id_from_row,
|
||||||
|
)
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1109,6 +1114,73 @@ class UmeTopologySyncTests(unittest.TestCase):
|
||||||
"7e8ac1c7-9d34-42d3-adfb-b1031e7c145a",
|
"7e8ac1c7-9d34-42d3-adfb-b1031e7c145a",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_onsite_fdn_name_without_node_id(self):
|
||||||
|
"""Live UME often omits nodeId; name is MD=...;ME=<uuid> / SBN=<uuid>."""
|
||||||
|
me_name = "MD=ZTE/UME(BN);ME=0ab0b408-0910-483e-94ca-c3dde489d848"
|
||||||
|
sbn_name = "MD=ZTE/UME(BN);SBN=47173499-c8bb-4a51-bef3-aea04ecf5c4c"
|
||||||
|
self.assertEqual(extract_me_uuid(me_name), "0ab0b408-0910-483e-94ca-c3dde489d848")
|
||||||
|
self.assertEqual(extract_me_uuid(sbn_name), "")
|
||||||
|
self.assertEqual(extract_topo_object_uuid(me_name), "0ab0b408-0910-483e-94ca-c3dde489d848")
|
||||||
|
self.assertEqual(extract_topo_object_uuid(sbn_name), "47173499-c8bb-4a51-bef3-aea04ecf5c4c")
|
||||||
|
row = {
|
||||||
|
"xPos": -2082,
|
||||||
|
"parentNode": sbn_name,
|
||||||
|
"owner": "ZTE",
|
||||||
|
"yPos": -1521,
|
||||||
|
"userLabel": "KND-RAEA-EN1-Z20HS",
|
||||||
|
"name": me_name,
|
||||||
|
"nodeType": "TOPO_NODE_ME",
|
||||||
|
}
|
||||||
|
self.assertEqual(_node_id_from_row(row), "0ab0b408-0910-483e-94ca-c3dde489d848")
|
||||||
|
|
||||||
|
def test_sync_topology_onsite_fdn_payload(self):
|
||||||
|
class _Diag:
|
||||||
|
latency_ms = 1
|
||||||
|
|
||||||
|
me_uuid = "0ab0b408-0910-483e-94ca-c3dde489d848"
|
||||||
|
sbn_uuid = "47173499-c8bb-4a51-bef3-aea04ecf5c4c"
|
||||||
|
|
||||||
|
class _C:
|
||||||
|
def get_topo_nodes(self):
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": f"MD=ZTE/UME(BN);SBN={sbn_uuid}",
|
||||||
|
"nodeType": "TOPO_NODE_SBN",
|
||||||
|
"owner": "ZTE",
|
||||||
|
"userLabel": "subnet",
|
||||||
|
"xPos": 1,
|
||||||
|
"yPos": 2,
|
||||||
|
"parentNode": "topLevel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"xPos": -2082,
|
||||||
|
"parentNode": f"MD=ZTE/UME(BN);SBN={sbn_uuid}",
|
||||||
|
"owner": "ZTE",
|
||||||
|
"yPos": -1521,
|
||||||
|
"userLabel": "KND-RAEA-EN1-Z20HS",
|
||||||
|
"name": f"MD=ZTE/UME(BN);ME={me_uuid}",
|
||||||
|
"nodeType": "TOPO_NODE_ME",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_Diag(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_topological_links(self):
|
||||||
|
return ([], _Diag())
|
||||||
|
|
||||||
|
job = sync_topology_full(self.db, _C(), trigger_mode="manual")
|
||||||
|
self.assertEqual(job.status, "done")
|
||||||
|
self.assertEqual(job.inserted_count, 2)
|
||||||
|
sbn = self.db.get(UmeTopoNode, sbn_uuid)
|
||||||
|
me = self.db.get(UmeTopoNode, me_uuid)
|
||||||
|
self.assertIsNotNone(sbn)
|
||||||
|
self.assertIsNotNone(me)
|
||||||
|
self.assertEqual(me.ume_ne_id, me_uuid)
|
||||||
|
self.assertEqual(me.parent_node, sbn_uuid)
|
||||||
|
self.assertEqual(me.x_pos, -2082)
|
||||||
|
self.assertEqual(me.user_label, "KND-RAEA-EN1-Z20HS")
|
||||||
|
|
||||||
def test_sync_topology_upsert_and_reconcile(self):
|
def test_sync_topology_upsert_and_reconcile(self):
|
||||||
class _Diag:
|
class _Diag:
|
||||||
latency_ms = 1
|
latency_ms = 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue