mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Wire ZTE show arp through TextFSM dual-input pipeline.
Add zte_zxros_show_arp rule + prefer_fsm mapping using the lab one-line sample. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
02b8a7073b
commit
0f235133ed
5 changed files with 195 additions and 51 deletions
|
|
@ -1,13 +1,21 @@
|
|||
"""ZTE: show arp [| one-line]."""
|
||||
"""ZTE: show arp [| one-line].
|
||||
|
||||
Pipeline: TextFSM rule ``zte_zxros_show_arp`` → map rows → hand fallback.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Mapping
|
||||
|
||||
from ....lldp_shared import resolve_vendor_key
|
||||
from ....ntc_parse import apply_rules, resolve_cli_platform, row_get
|
||||
from ..common.pipeline import prefer_fsm
|
||||
|
||||
RULE_KEYS = ("zte_zxros_show_arp",)
|
||||
|
||||
_ARP_AGE_TIME_RE = re.compile(r"^\d{1,2}:\d{2}:\d{2}$")
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
_IP_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$")
|
||||
|
||||
|
||||
def is_valid_arp_age(age: str) -> bool:
|
||||
|
|
@ -15,19 +23,62 @@ def is_valid_arp_age(age: str) -> bool:
|
|||
return bool(_ARP_AGE_TIME_RE.match(str(age or "").strip()))
|
||||
|
||||
|
||||
def normalize_arp(
|
||||
def _row_from_fields(
|
||||
*,
|
||||
raw_text: str,
|
||||
fsm_tables=None,
|
||||
vendor: str = "",
|
||||
device_type: str = "",
|
||||
command: str = "",
|
||||
params: dict[str, str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
_ = (vendor, device_type, command, params, fsm_tables)
|
||||
ip: str,
|
||||
age: str,
|
||||
mac: str,
|
||||
iface: str,
|
||||
exter: str = "",
|
||||
inter: str = "",
|
||||
sub: str = "",
|
||||
) -> dict[str, Any] | None:
|
||||
ip = str(ip or "").strip()
|
||||
iface = str(iface or "").strip()
|
||||
if not ip or not iface:
|
||||
return None
|
||||
age = str(age or "").strip()
|
||||
dynamic = is_valid_arp_age(age)
|
||||
return {
|
||||
"ip": ip[:64],
|
||||
"age": age[:32],
|
||||
"mac": str(mac or "").strip()[:64],
|
||||
"interface": iface[:128],
|
||||
"exter_vlan": str(exter or "").strip()[:32],
|
||||
"inter_vlan": str(inter or "").strip()[:32],
|
||||
"sub_interface": str(sub or "").strip()[:128],
|
||||
"entry_type": "dynamic" if dynamic else "static",
|
||||
}
|
||||
|
||||
|
||||
def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for r in rows:
|
||||
ip = row_get(r, "IP", "ip")
|
||||
iface = row_get(r, "INTERFACE", "interface")
|
||||
key = (ip, iface)
|
||||
if not ip or not iface or key in seen:
|
||||
continue
|
||||
mapped = _row_from_fields(
|
||||
ip=ip,
|
||||
age=row_get(r, "AGE", "age"),
|
||||
mac=row_get(r, "MAC", "mac", "HARDWARE", "hardware"),
|
||||
iface=iface,
|
||||
exter=row_get(r, "EXTER_VLAN", "exter_vlan"),
|
||||
inter=row_get(r, "INTER_VLAN", "inter_vlan"),
|
||||
sub=row_get(r, "SUB_INTERFACE", "sub_interface"),
|
||||
)
|
||||
if not mapped:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(mapped)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
ip_re = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$")
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("---") or line.lower().startswith("arp protect"):
|
||||
|
|
@ -37,32 +88,60 @@ def normalize_arp(
|
|||
if "hardware" in line.lower() and "address" in line.lower():
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 4 or not ip_re.match(parts[0]):
|
||||
if len(parts) < 4 or not _IP_RE.match(parts[0]):
|
||||
continue
|
||||
ip = parts[0]
|
||||
age = parts[1]
|
||||
mac = parts[2]
|
||||
iface = parts[3]
|
||||
exter = parts[4] if len(parts) > 4 else ""
|
||||
inter = parts[5] if len(parts) > 5 else ""
|
||||
sub = parts[6] if len(parts) > 6 else ""
|
||||
ip, age, mac, iface = parts[0], parts[1], parts[2], parts[3]
|
||||
key = (ip, iface)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
dynamic = is_valid_arp_age(age)
|
||||
out.append(
|
||||
{
|
||||
"ip": ip[:64],
|
||||
"age": age[:32],
|
||||
"mac": mac[:64],
|
||||
"interface": iface[:128],
|
||||
"exter_vlan": exter[:32],
|
||||
"inter_vlan": inter[:32],
|
||||
"sub_interface": sub[:128],
|
||||
"entry_type": "dynamic" if dynamic else "static",
|
||||
}
|
||||
mapped = _row_from_fields(
|
||||
ip=ip,
|
||||
age=age,
|
||||
mac=mac,
|
||||
iface=iface,
|
||||
exter=parts[4] if len(parts) > 4 else "",
|
||||
inter=parts[5] if len(parts) > 5 else "",
|
||||
sub=parts[6] if len(parts) > 6 else "",
|
||||
)
|
||||
if not mapped:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(mapped)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_arp(
|
||||
*,
|
||||
raw_text: str,
|
||||
fsm_tables: Mapping[str, list[dict[str, Any]]] | None = None,
|
||||
vendor: str = "",
|
||||
device_type: str = "",
|
||||
command: str = "",
|
||||
params: dict[str, str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
_ = params
|
||||
tables = dict(fsm_tables or {})
|
||||
if not any(tables.get(k) for k in RULE_KEYS):
|
||||
platform = resolve_cli_platform(
|
||||
vendor=vendor,
|
||||
device_type=device_type,
|
||||
vendor_key=resolve_vendor_key(vendor, device_type),
|
||||
)
|
||||
cmd = str(command or "show arp").strip() or "show arp"
|
||||
if platform:
|
||||
tables = apply_rules(
|
||||
platform=platform, text=raw_text, rule_keys=RULE_KEYS, command=cmd
|
||||
)
|
||||
return prefer_fsm(
|
||||
tables,
|
||||
RULE_KEYS,
|
||||
_map_fsm_rows,
|
||||
_hand_parse,
|
||||
raw_text=raw_text,
|
||||
vendor=vendor,
|
||||
device_type=device_type,
|
||||
command=command,
|
||||
)
|
||||
|
||||
|
||||
normalize_arp.RULE_KEYS = RULE_KEYS
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ zte/zte_zxros_show_lldp_neighbor_brief.textfsm, .*, zte_zxros, sh[[ow]] lld[[p]]
|
|||
zte/zte_zxros_show_interface_brief.textfsm, .*, zte_zxros, sh[[ow]] int[[erface]] br[[ief]]
|
||||
zte/zte_zxros_show_interface.textfsm, .*, zte_zxros, sh[[ow]] int[[erface]] .+
|
||||
zte/zte_zxros_show_ip_vrf.textfsm, .*, zte_zxros, sh[[ow]] ip vrf$
|
||||
zte/zte_zxros_show_arp.textfsm, .*, zte_zxros, sh[[ow]] arp
|
||||
|
||||
# --- cisco ---
|
||||
cisco/cisco_ios_show_interfaces.textfsm, .*, cisco_ios, sh[[ow]] int[[erfaces]] .+
|
||||
|
|
|
|||
32
netx_api/cli_templates/zte/zte_zxros_show_arp.textfsm
Normal file
32
netx_api/cli_templates/zte/zte_zxros_show_arp.textfsm
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
Value IP (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})
|
||||
Value AGE (\S+)
|
||||
Value MAC ([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4})
|
||||
Value INTERFACE (\S+)
|
||||
Value EXTER_VLAN (\S+)
|
||||
Value INTER_VLAN (\S+)
|
||||
Value SUB_INTERFACE (\S+)
|
||||
|
||||
Start
|
||||
^IP\s+Hardware -> Header
|
||||
^Address\s+Age\s+Address -> Entries
|
||||
^-+ -> Entries
|
||||
^\s*$$
|
||||
^.* -> Start
|
||||
|
||||
Header
|
||||
^Address\s+Age\s+Address -> Entries
|
||||
^-+ -> Entries
|
||||
^\s*$$
|
||||
^.* -> Header
|
||||
|
||||
Entries
|
||||
^${IP}\s+${AGE}\s+${MAC}\s+${INTERFACE}\s+${EXTER_VLAN}\s+${INTER_VLAN}\s+${SUB_INTERFACE}\s*$$ -> Record
|
||||
^${IP}\s+${AGE}\s+${MAC}\s+${INTERFACE}\s+${EXTER_VLAN}\s+${INTER_VLAN}\s*$$ -> Record
|
||||
^${IP}\s+${AGE}\s+${MAC}\s+${INTERFACE}\s*$$ -> Record
|
||||
^-+
|
||||
^The\s+count
|
||||
^Arp\s+protect
|
||||
^\s*$$
|
||||
^\S+\s*#\s*$$ -> End
|
||||
^.*#\s*$$ -> End
|
||||
^.* -> Entries
|
||||
Loading…
Add table
Add a link
Reference in a new issue