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 <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-20 11:07:18 +08:00
parent f279431c70
commit a6ea67a8bf
4 changed files with 83 additions and 3 deletions

View file

@ -18,6 +18,9 @@ _IF_HDR_RE = re.compile(
) )
_DESC_RE = re.compile(r"^\s*Description:\s*(?P<desc>.*?)\s*$", re.I) _DESC_RE = re.compile(r"^\s*Description:\s*(?P<desc>.*?)\s*$", re.I)
_BW_RE = re.compile(r"^\s*BW\s+(?P<bw>.+?)\s*$", re.I) _BW_RE = re.compile(r"^\s*BW\s+(?P<bw>.+?)\s*$", re.I)
_IP_MTU_RE = re.compile(r"^\s*IP\s+MTU\s+(?P<mtu>\d+)\s+bytes", re.I)
_IPV6_MTU_RE = re.compile(r"^\s*IPv6\s+MTU\s+(?P<mtu>\d+)\s+bytes", re.I)
_MPLS_MTU_RE = re.compile(r"^\s*MPLS\s+MTU\s+(?P<mtu>\d+)\s+bytes", re.I)
_MTU_RE = re.compile(r"^\s*MTU\s+(?P<mtu>\d+)\s+bytes", re.I) _MTU_RE = re.compile(r"^\s*MTU\s+(?P<mtu>\d+)\s+bytes", re.I)
_MEDIA_RE = re.compile(r"^\s*The\s+port\s+is\s+(?P<media>\S+)", re.I) _MEDIA_RE = re.compile(r"^\s*The\s+port\s+is\s+(?P<media>\S+)", re.I)
_NEG_RE = re.compile(r"^\s*Negotiation\s+(?P<neg>\S+)", re.I) _NEG_RE = re.compile(r"^\s*Negotiation\s+(?P<neg>\S+)", re.I)
@ -39,7 +42,10 @@ def _empty_row(iface: str) -> dict[str, Any]:
"port_media": "", "port_media": "",
"negotiation": "", "negotiation": "",
"bw": "", "bw": "",
"ip_mtu": "",
"mtu": "", "mtu": "",
"mpls_mtu": "",
"ipv6_mtu": "",
"rate_period": "", "rate_period": "",
"input_bps": "", "input_bps": "",
"output_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], "port_media": row_get(r, "PORT_MEDIA", "port_media")[:32],
"negotiation": row_get(r, "NEGOTIATION", "negotiation")[:32], "negotiation": row_get(r, "NEGOTIATION", "negotiation")[:32],
"bw": row_get(r, "BW_RAW", "bw")[:64], "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], "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], "rate_period": row_get(r, "RATE_PERIOD", "rate_period")[:16],
"input_bps": row_get(r, "INPUT_BPS", "input_bps")[:32], "input_bps": row_get(r, "INPUT_BPS", "input_bps")[:32],
"output_bps": row_get(r, "OUTPUT_BPS", "output_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: if m:
cur["bw"] = m.group("bw").strip()[:64] cur["bw"] = m.group("bw").strip()[:64]
continue 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) m = _MTU_RE.match(line)
if m: if m:
cur["mtu"] = m.group("mtu")[:16] cur["mtu"] = m.group("mtu")[:16]

View file

@ -223,7 +223,10 @@ _IFACE_DETAIL_FIELDS: list[FieldDef] = [
FieldDef("in_util", length=16, role="counter", display_name="In Util%"), FieldDef("in_util", length=16, role="counter", display_name="In Util%"),
FieldDef("out_util", length=16, role="counter", display_name="Out Util%"), FieldDef("out_util", length=16, role="counter", display_name="Out Util%"),
FieldDef("bw", length=64, role="meta", display_name="BW"), 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("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("ifindex", length=32, role="meta", display_name="ifindex"),
FieldDef("description", length=256, role="meta", display_name="描述"), FieldDef("description", length=256, role="meta", display_name="描述"),
FieldDef("port_media", length=32, role="meta", display_name="Media"), FieldDef("port_media", length=32, role="meta", display_name="Media"),

View file

@ -5,7 +5,10 @@ Value Filldown DESCRIPTION (.+?)
Value Filldown PORT_MEDIA (\S+) Value Filldown PORT_MEDIA (\S+)
Value Filldown NEGOTIATION (\S+) Value Filldown NEGOTIATION (\S+)
Value Filldown BW_RAW (\d+(?:\.\d+)?\s*[kKmMgGtT]?\s*(?:G?bit|bit)/s(?:ec)?) 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 MTU (\d+)
Value Filldown MPLS_MTU (\d+)
Value Filldown IPV6_MTU (\d+)
Value Filldown RATE_PERIOD (\d+) Value Filldown RATE_PERIOD (\d+)
Value Filldown INPUT_BPS ([\d.]+) Value Filldown INPUT_BPS ([\d.]+)
Value Filldown OUTPUT_BPS ([\d.]+) Value Filldown OUTPUT_BPS ([\d.]+)
@ -13,11 +16,15 @@ Value IN_UTIL ([\d.]+)
Value OUT_UTIL ([\d.]+) Value OUT_UTIL ([\d.]+)
Start Start
^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? -> Continue.Clearall
^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})? ^${IFNAME}\s+is\s+${ADMIN_STATE}(?:,\s*ifindex:\s*${IFINDEX})?
^\s*Description:\s*${DESCRIPTION}\s*$$ ^\s*Description:\s*${DESCRIPTION}\s*$$
^\s*The\s+port\s+is\s+${PORT_MEDIA} ^\s*The\s+port\s+is\s+${PORT_MEDIA}
^\s*Negotiation\s+${NEGOTIATION} ^\s*Negotiation\s+${NEGOTIATION}
^\s*BW\s+${BW_RAW} ^\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*MTU\s+${MTU}\s+bytes
^\s*Rate\s+period\s*:\s*${RATE_PERIOD}\s*s -> Rates ^\s*Rate\s+period\s*:\s*${RATE_PERIOD}\s*s -> Rates
^\s*$$ ^\s*$$
@ -28,12 +35,14 @@ Rates
^\s*Output\s*:\s*${OUTPUT_BPS}\s*bit/s ^\s*Output\s*:\s*${OUTPUT_BPS}\s*bit/s
^\s*Peak\s+rate: -> AfterPeak ^\s*Peak\s+rate: -> AfterPeak
^\s*Intf\s+utilization:\s*input\s*${IN_UTIL}%\s+output\s*${OUT_UTIL}% -> Record Start ^\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*$$ ^\s*$$
^.* -> Rates ^.* -> Rates
AfterPeak AfterPeak
^\s*Intf\s+utilization:\s*input\s*${IN_UTIL}%\s+output\s*${OUT_UTIL}% -> Record Start ^\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*$$ ^\s*$$
^.* -> AfterPeak ^.* -> AfterPeak

View file

@ -265,7 +265,9 @@ gei-0/0/0/1 is up, ifindex: 100
The port is optical The port is optical
Negotiation force Negotiation force
BW 1 Gbit/s BW 1 Gbit/s
IP MTU 1500 bytes
MTU 1600 bytes MTU 1600 bytes
MPLS MTU 1550 bytes
Rate period : 120 s Rate period : 120 s
Input : 100 bit/s 1 packet/s Input : 100 bit/s 1 packet/s
Output : 200 bit/s 2 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 gei-0/0/0/2 is administratively down, ifindex: 101
Description: spare Description: spare
BW 1 Gbit/s 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 Rate period : 120 s
Input : 0 bit/s 0 packet/s Input : 0 bit/s 0 packet/s
Output : 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]["admin"], "up")
self.assertEqual(rows[0]["input_bps"], "100") self.assertEqual(rows[0]["input_bps"], "100")
self.assertEqual(rows[0]["out_util"], "2") 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]["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 = ( cmd = (
"show interface | include ifindex|BW|The port is|MTU|Negotiation|" "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) self.assertIsNotNone(hit)
assert hit is not None assert hit is not None
self.assertEqual(hit.profile.profile_id, "zte.interface_detail") 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( hit6 = match_command(
vendor_key="zte", vendor_key="zte",