From a6ea67a8bf5aca512162fa26cc6d42cf5121280e Mon Sep 17 00:00:00 2001 From: oliver Date: Sun, 20 Sep 2026 11:07:18 +0800 Subject: [PATCH] Extract IP/MPLS/IPv6 MTU from ZTE interface detail. Plain MTU alone missed sibling values in filtered show interface output; also clear Filldown between interfaces so optional IPv6 MTU does not leak. Co-authored-by: Cursor --- .../biz_state/parsers/zte/interface_detail.py | 21 ++++++++ netx_api/biz_state/profiles.py | 3 ++ .../zte/zte_zxros_show_interface.textfsm | 13 ++++- tests/test_zte_extended_parsers.py | 49 ++++++++++++++++++- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/netx_api/biz_state/parsers/zte/interface_detail.py b/netx_api/biz_state/parsers/zte/interface_detail.py index 4b990ee..e81af9b 100644 --- a/netx_api/biz_state/parsers/zte/interface_detail.py +++ b/netx_api/biz_state/parsers/zte/interface_detail.py @@ -18,6 +18,9 @@ _IF_HDR_RE = re.compile( ) _DESC_RE = re.compile(r"^\s*Description:\s*(?P.*?)\s*$", re.I) _BW_RE = re.compile(r"^\s*BW\s+(?P.+?)\s*$", re.I) +_IP_MTU_RE = re.compile(r"^\s*IP\s+MTU\s+(?P\d+)\s+bytes", re.I) +_IPV6_MTU_RE = re.compile(r"^\s*IPv6\s+MTU\s+(?P\d+)\s+bytes", re.I) +_MPLS_MTU_RE = re.compile(r"^\s*MPLS\s+MTU\s+(?P\d+)\s+bytes", re.I) _MTU_RE = re.compile(r"^\s*MTU\s+(?P\d+)\s+bytes", re.I) _MEDIA_RE = re.compile(r"^\s*The\s+port\s+is\s+(?P\S+)", re.I) _NEG_RE = re.compile(r"^\s*Negotiation\s+(?P\S+)", re.I) @@ -39,7 +42,10 @@ def _empty_row(iface: str) -> dict[str, Any]: "port_media": "", "negotiation": "", "bw": "", + "ip_mtu": "", "mtu": "", + "mpls_mtu": "", + "ipv6_mtu": "", "rate_period": "", "input_bps": "", "output_bps": "", @@ -82,7 +88,10 @@ def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: "port_media": row_get(r, "PORT_MEDIA", "port_media")[:32], "negotiation": row_get(r, "NEGOTIATION", "negotiation")[:32], "bw": row_get(r, "BW_RAW", "bw")[:64], + "ip_mtu": row_get(r, "IP_MTU", "ip_mtu")[:16], "mtu": row_get(r, "MTU", "mtu")[:16], + "mpls_mtu": row_get(r, "MPLS_MTU", "mpls_mtu")[:16], + "ipv6_mtu": row_get(r, "IPV6_MTU", "ipv6_mtu")[:16], "rate_period": row_get(r, "RATE_PERIOD", "rate_period")[:16], "input_bps": row_get(r, "INPUT_BPS", "input_bps")[:32], "output_bps": row_get(r, "OUTPUT_BPS", "output_bps")[:32], @@ -136,6 +145,18 @@ def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]: if m: cur["bw"] = m.group("bw").strip()[:64] continue + m = _IP_MTU_RE.match(line) + if m: + cur["ip_mtu"] = m.group("mtu")[:16] + continue + m = _IPV6_MTU_RE.match(line) + if m: + cur["ipv6_mtu"] = m.group("mtu")[:16] + continue + m = _MPLS_MTU_RE.match(line) + if m: + cur["mpls_mtu"] = m.group("mtu")[:16] + continue m = _MTU_RE.match(line) if m: cur["mtu"] = m.group("mtu")[:16] diff --git a/netx_api/biz_state/profiles.py b/netx_api/biz_state/profiles.py index 7d4ccb2..53d2ac6 100644 --- a/netx_api/biz_state/profiles.py +++ b/netx_api/biz_state/profiles.py @@ -223,7 +223,10 @@ _IFACE_DETAIL_FIELDS: list[FieldDef] = [ FieldDef("in_util", length=16, role="counter", display_name="In Util%"), FieldDef("out_util", length=16, role="counter", display_name="Out Util%"), FieldDef("bw", length=64, role="meta", display_name="BW"), + FieldDef("ip_mtu", length=16, role="meta", display_name="IP MTU"), FieldDef("mtu", length=16, role="meta", display_name="MTU"), + FieldDef("mpls_mtu", length=16, role="meta", display_name="MPLS MTU"), + FieldDef("ipv6_mtu", length=16, role="meta", display_name="IPv6 MTU"), FieldDef("ifindex", length=32, role="meta", display_name="ifindex"), FieldDef("description", length=256, role="meta", display_name="描述"), FieldDef("port_media", length=32, role="meta", display_name="Media"), diff --git a/netx_api/cli_templates/zte/zte_zxros_show_interface.textfsm b/netx_api/cli_templates/zte/zte_zxros_show_interface.textfsm index 0427dd4..5e0f60c 100644 --- a/netx_api/cli_templates/zte/zte_zxros_show_interface.textfsm +++ b/netx_api/cli_templates/zte/zte_zxros_show_interface.textfsm @@ -5,7 +5,10 @@ Value Filldown DESCRIPTION (.+?) Value Filldown PORT_MEDIA (\S+) Value Filldown NEGOTIATION (\S+) Value Filldown BW_RAW (\d+(?:\.\d+)?\s*[kKmMgGtT]?\s*(?:G?bit|bit)/s(?:ec)?) +Value Filldown IP_MTU (\d+) Value Filldown MTU (\d+) +Value Filldown MPLS_MTU (\d+) +Value Filldown IPV6_MTU (\d+) Value Filldown RATE_PERIOD (\d+) Value Filldown INPUT_BPS ([\d.]+) Value Filldown OUTPUT_BPS ([\d.]+) @@ -13,11 +16,15 @@ Value IN_UTIL ([\d.]+) Value OUT_UTIL ([\d.]+) Start + ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Continue.Clearall ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? ^\s*Description:\s*${DESCRIPTION}\s*$$ ^\s*The\s+port\s+is\s+${PORT_MEDIA} ^\s*Negotiation\s+${NEGOTIATION} ^\s*BW\s+${BW_RAW} + ^\s*IP\s+MTU\s+${IP_MTU}\s+bytes + ^\s*IPv6\s+MTU\s+${IPV6_MTU}\s+bytes + ^\s*MPLS\s+MTU\s+${MPLS_MTU}\s+bytes ^\s*MTU\s+${MTU}\s+bytes ^\s*Rate\s+period\s*:\s*${RATE_PERIOD}\s*s -> Rates ^\s*$$ @@ -28,12 +35,14 @@ Rates ^\s*Output\s*:\s*${OUTPUT_BPS}\s*bit/s ^\s*Peak\s+rate: -> AfterPeak ^\s*Intf\s+utilization:\s*input\s*${IN_UTIL}%\s+output\s*${OUT_UTIL}% -> Record Start - ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Clearall Start + ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Continue.Clearall + ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Start ^\s*$$ ^.* -> Rates AfterPeak ^\s*Intf\s+utilization:\s*input\s*${IN_UTIL}%\s+output\s*${OUT_UTIL}% -> Record Start - ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Clearall Start + ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Continue.Clearall + ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Start ^\s*$$ ^.* -> AfterPeak diff --git a/tests/test_zte_extended_parsers.py b/tests/test_zte_extended_parsers.py index d2ed3e8..894fef6 100644 --- a/tests/test_zte_extended_parsers.py +++ b/tests/test_zte_extended_parsers.py @@ -265,7 +265,9 @@ gei-0/0/0/1 is up, ifindex: 100 The port is optical Negotiation force BW 1 Gbit/s + IP MTU 1500 bytes MTU 1600 bytes + MPLS MTU 1550 bytes Rate period : 120 s Input : 100 bit/s 1 packet/s Output : 200 bit/s 2 packet/s @@ -276,7 +278,10 @@ gei-0/0/0/1 is up, ifindex: 100 gei-0/0/0/2 is administratively down, ifindex: 101 Description: spare BW 1 Gbit/s - MTU 1500 bytes + IP MTU 8978 bytes + MTU 9000 bytes + IPv6 MTU 8978 bytes + MPLS MTU 8978 bytes Rate period : 120 s Input : 0 bit/s 0 packet/s Output : 0 bit/s 0 packet/s @@ -292,7 +297,47 @@ gei-0/0/0/2 is administratively down, ifindex: 101 self.assertEqual(rows[0]["admin"], "up") self.assertEqual(rows[0]["input_bps"], "100") self.assertEqual(rows[0]["out_util"], "2") + self.assertEqual(rows[0]["ip_mtu"], "1500") + self.assertEqual(rows[0]["mtu"], "1600") + self.assertEqual(rows[0]["mpls_mtu"], "1550") + self.assertEqual(rows[0]["ipv6_mtu"], "") self.assertEqual(rows[1]["admin"], "admin-down") + self.assertEqual(rows[1]["ip_mtu"], "8978") + self.assertEqual(rows[1]["mtu"], "9000") + self.assertEqual(rows[1]["ipv6_mtu"], "8978") + self.assertEqual(rows[1]["mpls_mtu"], "8978") + + # No IPv6 MTU on second iface — must not inherit from previous Filldown. + leak_sample = """ +gei-0/0/0/1 is up, ifindex: 1 + IP MTU 1500 bytes + MTU 1600 bytes + IPv6 MTU 8978 bytes + MPLS MTU 1550 bytes + Rate period : 120 s + Input : 1 bit/s + Output : 2 bit/s + Intf utilization: input 1% output 2% +gei-0/0/0/2 is up, ifindex: 2 + IP MTU 1500 bytes + MTU 1600 bytes + MPLS MTU 1550 bytes + Rate period : 120 s + Input : 0 bit/s + Output : 0 bit/s + Intf utilization: input 0% output 0% +""" + leak_rows = normalize_interface_detail( + raw_text=leak_sample, + vendor="zte", + device_type="zte_zxros", + command="show interface", + ) + self.assertEqual(len(leak_rows), 2) + self.assertEqual(leak_rows[0]["ipv6_mtu"], "8978") + self.assertEqual(leak_rows[1]["ipv6_mtu"], "") + self.assertEqual(leak_rows[1]["ip_mtu"], "1500") + self.assertEqual(leak_rows[1]["mpls_mtu"], "1550") cmd = ( "show interface | include ifindex|BW|The port is|MTU|Negotiation|" @@ -302,6 +347,8 @@ gei-0/0/0/2 is administratively down, ifindex: 101 self.assertIsNotNone(hit) assert hit is not None self.assertEqual(hit.profile.profile_id, "zte.interface_detail") + field_names = {f.name for f in hit.profile.fields} + self.assertTrue({"ip_mtu", "mtu", "mpls_mtu", "ipv6_mtu"} <= field_names) hit6 = match_command( vendor_key="zte",