mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 06:50:49 +08:00
Split ZTE L2VPN MAC outgoing into pw/AC/SID fields and skip separator rows.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4b38427679
commit
0dc63bb96a
4 changed files with 173 additions and 22 deletions
|
|
@ -16,6 +16,84 @@ _MAC_RE = re.compile(
|
||||||
r"(?P<vpn>\S+)\s+(?P<vlan>\S+)\s+(?P<out>.+?)\s+(?P<attr>\S+)\s*$"
|
r"(?P<vpn>\S+)\s+(?P<vlan>\S+)\s+(?P<out>.+?)\s+(?P<attr>\S+)\s*$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_SID_OUT_RE = re.compile(
|
||||||
|
r"(?i)^SID\s+(?P<vpn_sid>\S+)\s*,\s*(?P<neighbor_sid>\S+)\s*$"
|
||||||
|
)
|
||||||
|
_AC_OUT_RE = re.compile(
|
||||||
|
r"^(?P<ac_port>\S+)\s*,\s*[EeIi]:(?P<exter_vlan>\S+)\s*$"
|
||||||
|
)
|
||||||
|
_PW_OUT_RE = re.compile(r"^(?P<pw>\S+)\s*,\s*(?P<neighbor>\S+)\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_outgoing(raw: str = "") -> dict[str, str]:
|
||||||
|
text = str(raw or "").strip()
|
||||||
|
return {
|
||||||
|
"outgoing": text[:256],
|
||||||
|
"pw": "",
|
||||||
|
"neighbor": "",
|
||||||
|
"ac_port": "",
|
||||||
|
"exter_vlan": "",
|
||||||
|
"vpn_sid": "",
|
||||||
|
"neighbor_sid": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_outgoing(raw: str) -> dict[str, str]:
|
||||||
|
"""Split Outgoing Information into typed fields; keep raw in outgoing."""
|
||||||
|
out = _empty_outgoing(raw)
|
||||||
|
text = out["outgoing"]
|
||||||
|
if not text:
|
||||||
|
return out
|
||||||
|
|
||||||
|
m = _SID_OUT_RE.match(text)
|
||||||
|
if m:
|
||||||
|
out["vpn_sid"] = m.group("vpn_sid")[:128]
|
||||||
|
out["neighbor_sid"] = m.group("neighbor_sid")[:128]
|
||||||
|
return out
|
||||||
|
|
||||||
|
m = _AC_OUT_RE.match(text)
|
||||||
|
if m:
|
||||||
|
out["ac_port"] = m.group("ac_port")[:128]
|
||||||
|
out["exter_vlan"] = m.group("exter_vlan")[:32]
|
||||||
|
return out
|
||||||
|
|
||||||
|
# ESI / other opaque forms stay in outgoing only.
|
||||||
|
if text.upper().startswith("ESI:"):
|
||||||
|
return out
|
||||||
|
|
||||||
|
m = _PW_OUT_RE.match(text)
|
||||||
|
if m:
|
||||||
|
out["pw"] = m.group("pw")[:128]
|
||||||
|
out["neighbor"] = m.group("neighbor")[:128]
|
||||||
|
return out
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _is_mac(token: str) -> bool:
|
||||||
|
t = str(token or "").strip()
|
||||||
|
return bool(re.fullmatch(r"[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}", t))
|
||||||
|
|
||||||
|
|
||||||
|
def _row(
|
||||||
|
*,
|
||||||
|
mac: str,
|
||||||
|
vpn: str,
|
||||||
|
vlan: str,
|
||||||
|
outgoing: str,
|
||||||
|
attribute: str,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
if not _is_mac(mac) or not vpn or vpn.lower() == "vpn":
|
||||||
|
return None
|
||||||
|
row = {
|
||||||
|
"mac": mac[:64],
|
||||||
|
"vpn": vpn[:128],
|
||||||
|
"vlan": str(vlan or "").strip()[:32],
|
||||||
|
"attribute": str(attribute or "").strip()[:64],
|
||||||
|
}
|
||||||
|
row.update(_parse_outgoing(outgoing))
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
|
|
@ -23,21 +101,20 @@ def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
for r in rows:
|
for r in rows:
|
||||||
mac = row_get(r, "MAC", "mac")
|
mac = row_get(r, "MAC", "mac")
|
||||||
vpn = row_get(r, "VPN", "vpn")
|
vpn = row_get(r, "VPN", "vpn")
|
||||||
if not mac or mac.lower() == "mac":
|
|
||||||
continue
|
|
||||||
key = (mac, vpn)
|
key = (mac, vpn)
|
||||||
if key in seen:
|
if key in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(key)
|
mapped = _row(
|
||||||
out.append(
|
mac=mac,
|
||||||
{
|
vpn=vpn,
|
||||||
"mac": mac[:64],
|
vlan=row_get(r, "VLAN", "vlan"),
|
||||||
"vpn": vpn[:128],
|
outgoing=row_get(r, "OUTGOING", "outgoing"),
|
||||||
"vlan": row_get(r, "VLAN", "vlan")[:32],
|
attribute=row_get(r, "ATTRIBUTE", "attribute"),
|
||||||
"outgoing": row_get(r, "OUTGOING", "outgoing")[:256],
|
|
||||||
"attribute": row_get(r, "ATTRIBUTE", "attribute")[:64],
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
if not mapped:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(mapped)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,16 +131,17 @@ def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||||
key = (m.group("mac"), m.group("vpn"))
|
key = (m.group("mac"), m.group("vpn"))
|
||||||
if key in seen:
|
if key in seen:
|
||||||
continue
|
continue
|
||||||
seen.add(key)
|
mapped = _row(
|
||||||
out.append(
|
mac=m.group("mac"),
|
||||||
{
|
vpn=m.group("vpn"),
|
||||||
"mac": m.group("mac")[:64],
|
vlan=m.group("vlan"),
|
||||||
"vpn": m.group("vpn")[:128],
|
outgoing=m.group("out").strip(),
|
||||||
"vlan": m.group("vlan")[:32],
|
attribute=m.group("attr"),
|
||||||
"outgoing": m.group("out").strip()[:256],
|
|
||||||
"attribute": m.group("attr")[:64],
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
if not mapped:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(mapped)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,13 @@ _L2VPN_MAC_FIELDS: list[FieldDef] = [
|
||||||
FieldDef("mac", length=64, indexed=True, is_key=True, display_name="MAC"),
|
FieldDef("mac", length=64, indexed=True, is_key=True, display_name="MAC"),
|
||||||
FieldDef("vpn", length=128, indexed=True, is_key=True, display_name="VPN"),
|
FieldDef("vpn", length=128, indexed=True, is_key=True, display_name="VPN"),
|
||||||
FieldDef("vlan", length=32, role="meta", display_name="VLAN"),
|
FieldDef("vlan", length=32, role="meta", display_name="VLAN"),
|
||||||
FieldDef("outgoing", length=256, role="state", display_name="Outgoing"),
|
FieldDef("pw", length=128, role="state", display_name="PW"),
|
||||||
|
FieldDef("neighbor", length=128, role="state", display_name="Neighbor"),
|
||||||
|
FieldDef("ac_port", length=128, role="state", display_name="AC Port"),
|
||||||
|
FieldDef("exter_vlan", length=32, role="state", display_name="Exter VLAN"),
|
||||||
|
FieldDef("vpn_sid", length=128, role="state", display_name="VPN SID"),
|
||||||
|
FieldDef("neighbor_sid", length=128, role="state", display_name="Neighbor SID"),
|
||||||
|
FieldDef("outgoing", length=256, role="meta", display_name="Outgoing"),
|
||||||
FieldDef("attribute", length=64, role="meta", display_name="Attribute"),
|
FieldDef("attribute", length=64, role="meta", display_name="Attribute"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Value MAC (\S+)
|
Value MAC ([0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4})
|
||||||
Value VPN (\S+)
|
Value VPN (\S+)
|
||||||
Value VLAN (\S+)
|
Value VLAN (\S+)
|
||||||
Value OUTGOING (.*)
|
Value OUTGOING (.*)
|
||||||
|
|
@ -13,6 +13,7 @@ Start
|
||||||
|
|
||||||
Entries
|
Entries
|
||||||
^${MAC}\s+${VPN}\s+${VLAN}\s+${OUTGOING}\s+${ATTRIBUTE}\s*$$ -> Record
|
^${MAC}\s+${VPN}\s+${VLAN}\s+${OUTGOING}\s+${ATTRIBUTE}\s*$$ -> Record
|
||||||
|
^-+(\s+-+)*\s*$$
|
||||||
^\s*$$
|
^\s*$$
|
||||||
^\S+\s*#\s*$$ -> End
|
^\S+\s*#\s*$$ -> End
|
||||||
^.*#\s*$$ -> End
|
^.*#\s*$$ -> End
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from netx_api.biz_state.parsers.zte import (
|
||||||
normalize_bgp_route,
|
normalize_bgp_route,
|
||||||
normalize_ip_route,
|
normalize_ip_route,
|
||||||
normalize_ipv6_route,
|
normalize_ipv6_route,
|
||||||
|
normalize_l2vpn_mac,
|
||||||
normalize_l2vpn_pw,
|
normalize_l2vpn_pw,
|
||||||
normalize_l2vpn_pw_detail,
|
normalize_l2vpn_pw_detail,
|
||||||
normalize_optical_brief,
|
normalize_optical_brief,
|
||||||
|
|
@ -129,6 +130,23 @@ pw2 10.0.0.2 128 Ethernet S DOWN - - L:VPN_B
|
||||||
pw3 10.0.0.3 128 Ethernet UP 101 201 W:100
|
pw3 10.0.0.3 128 Ethernet UP 101 201 W:100
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_L2VPN_MAC = """
|
||||||
|
Total MAC Entries: 502
|
||||||
|
|
||||||
|
Headers: Src--Source filter, Dst--Destination filter
|
||||||
|
E--Exter-VLAN ID I--Inter-VLAN ID
|
||||||
|
|
||||||
|
MAC VPN VLAN Outgoing Information Attribute
|
||||||
|
-------------- -------------- ---- -------------------------------- ------------
|
||||||
|
0000.9999.0002 EVPNvpls501 0 SID fc00:a000:1005::567b:0, 2400:9800:7000:1005:: Static
|
||||||
|
0011.2232.42e3 mxy-bvi-ag4-ag3 0 SID fc00:a000:1002::3469:0, 2400:9800:7000:1002:: Dynamic
|
||||||
|
0099.2232.7788 css-irb-ag3-1 0 ESI:0:109501000010000001 Static
|
||||||
|
004b.0100.0001 css-irb-ag3-1 0 smartgroup11.31, E:31 Dynamic
|
||||||
|
0011.2232.42e4 mxy-kompella-2 0 auto_pw3, 24.11.100.5 Dynamic
|
||||||
|
0011.2232.42e4 mxy-kompella-1 0 PW1,1.1.1.1 Dynamic
|
||||||
|
0000.2200.0001 EVPNvpls501 0 smartgroup2345.2501, E:2501 Dynamic
|
||||||
|
"""
|
||||||
|
|
||||||
_L2VPN_PW_DETAIL = """
|
_L2VPN_PW_DETAIL = """
|
||||||
Service type and instance name:[VPLS qualified demo-vpls-1]
|
Service type and instance name:[VPLS qualified demo-vpls-1]
|
||||||
Peer IP address : 10.0.0.6 VCID : 19001
|
Peer IP address : 10.0.0.6 VCID : 19001
|
||||||
|
|
@ -276,6 +294,54 @@ class ZteExtendedParserTests(unittest.TestCase):
|
||||||
self.assertEqual(by["cgei-0/1/0/2:1"]["tx_power"], "2.3,2.4,2.3,2.2")
|
self.assertEqual(by["cgei-0/1/0/2:1"]["tx_power"], "2.3,2.4,2.3,2.2")
|
||||||
self.assertEqual(by["xgei-0/2/0/1"]["rx_power"], "-2.4")
|
self.assertEqual(by["xgei-0/2/0/1"]["rx_power"], "-2.4")
|
||||||
|
|
||||||
|
def test_l2vpn_mac_outgoing_split(self) -> None:
|
||||||
|
fsm_rows = apply_rule(
|
||||||
|
platform="zte_zxros",
|
||||||
|
rule_key="zte_zxros_show_mac_l2vpn",
|
||||||
|
text=_L2VPN_MAC,
|
||||||
|
command="show mac l2vpn",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(fsm_rows), 7)
|
||||||
|
self.assertTrue(all("----" not in r.get("MAC", "") for r in fsm_rows))
|
||||||
|
|
||||||
|
rows = normalize_l2vpn_mac(
|
||||||
|
raw_text=_L2VPN_MAC,
|
||||||
|
vendor="zte",
|
||||||
|
device_type="zte_zxros",
|
||||||
|
command="show mac l2vpn | one-line",
|
||||||
|
)
|
||||||
|
self.assertEqual(len(rows), 7)
|
||||||
|
by = {(r["mac"], r["vpn"]): r for r in rows}
|
||||||
|
|
||||||
|
sid = by[("0000.9999.0002", "EVPNvpls501")]
|
||||||
|
self.assertEqual(sid["vpn_sid"], "fc00:a000:1005::567b:0")
|
||||||
|
self.assertEqual(sid["neighbor_sid"], "2400:9800:7000:1005::")
|
||||||
|
self.assertEqual(sid["pw"], "")
|
||||||
|
self.assertEqual(sid["ac_port"], "")
|
||||||
|
self.assertTrue(sid["outgoing"].startswith("SID "))
|
||||||
|
|
||||||
|
ac = by[("004b.0100.0001", "css-irb-ag3-1")]
|
||||||
|
self.assertEqual(ac["ac_port"], "smartgroup11.31")
|
||||||
|
self.assertEqual(ac["exter_vlan"], "31")
|
||||||
|
self.assertEqual(ac["pw"], "")
|
||||||
|
|
||||||
|
pw = by[("0011.2232.42e4", "mxy-kompella-2")]
|
||||||
|
self.assertEqual(pw["pw"], "auto_pw3")
|
||||||
|
self.assertEqual(pw["neighbor"], "24.11.100.5")
|
||||||
|
self.assertEqual(pw["ac_port"], "")
|
||||||
|
|
||||||
|
pw2 = by[("0011.2232.42e4", "mxy-kompella-1")]
|
||||||
|
self.assertEqual(pw2["pw"], "PW1")
|
||||||
|
self.assertEqual(pw2["neighbor"], "1.1.1.1")
|
||||||
|
|
||||||
|
esi = by[("0099.2232.7788", "css-irb-ag3-1")]
|
||||||
|
self.assertEqual(esi["outgoing"], "ESI:0:109501000010000001")
|
||||||
|
self.assertEqual(esi["pw"], "")
|
||||||
|
self.assertEqual(esi["vpn_sid"], "")
|
||||||
|
|
||||||
|
fields = {f.name for f in metric_field_map()["l2vpn_mac"]}
|
||||||
|
self.assertTrue({"pw", "neighbor", "ac_port", "exter_vlan", "vpn_sid", "neighbor_sid"} <= fields)
|
||||||
|
|
||||||
def test_bgp_route_and_aux_render(self) -> None:
|
def test_bgp_route_and_aux_render(self) -> None:
|
||||||
routes = normalize_bgp_route(
|
routes = normalize_bgp_route(
|
||||||
raw_text=_BGP_ROUTE_IN,
|
raw_text=_BGP_ROUTE_IN,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue