Add TextFSM rule tables as dual input to biz-state parsers.

Collectors apply RULE_KEYS (or index stems) into fsm_tables before normalize; parsers prefer FSM or hand-parse.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-17 22:18:21 +08:00
parent 24a1a853af
commit 02b8a7073b
25 changed files with 714 additions and 69 deletions

View file

@ -7,6 +7,8 @@ from typing import Any
_ARP_AGE_TIME_RE = re.compile(r"^\d{1,2}:\d{2}:\d{2}$")
RULE_KEYS: tuple[str, ...] = ()
def is_valid_arp_age(age: str) -> bool:
"""True when Age looks like a dynamic timer (HH:MM:SS), not static flags like H."""
@ -16,12 +18,13 @@ def is_valid_arp_age(age: str) -> bool:
def normalize_arp(
*,
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)
_ = (vendor, device_type, command, params, fsm_tables)
out: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
ip_re = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$")
@ -61,3 +64,5 @@ def normalize_arp(
}
)
return out
normalize_arp.RULE_KEYS = RULE_KEYS

View file

@ -11,6 +11,8 @@ _BGP_PEER_RE = re.compile(
re.I,
)
RULE_KEYS: tuple[str, ...] = ()
def _detect_bgp_afi(command: str, params: dict[str, str] | None) -> str:
if params and params.get("afi"):
@ -30,12 +32,13 @@ def _detect_bgp_afi(command: str, params: dict[str, str] | None) -> str:
def normalize_bgp_peer(
*,
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)
_ = (vendor, device_type, fsm_tables)
afi = _detect_bgp_afi(command, params)
out: list[dict[str, Any]] = []
seen: set[str] = set()
@ -77,3 +80,5 @@ def normalize_bgp_peer(
}
)
return out
normalize_bgp_peer.RULE_KEYS = RULE_KEYS

View file

@ -3,10 +3,13 @@
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 parse_cli, resolve_cli_platform, row_get
from ....ntc_parse import apply_rules, resolve_cli_platform, row_get
from ..common.pipeline import prefer_fsm
RULE_KEYS = ("zte_zxros_show_interface_brief",)
_IFACE_RE = re.compile(
r"^(?P<iface>\S+)\s+(?P<attr>\S+)\s+(?P<mode>\S+)"
@ -16,22 +19,7 @@ _IFACE_RE = re.compile(
)
def normalize_interface_brief(
*,
raw_text: str,
vendor: str = "",
device_type: str = "",
command: str = "",
params: dict[str, str] | None = None,
) -> list[dict[str, Any]]:
_ = params
platform = resolve_cli_platform(
vendor=vendor,
device_type=device_type,
vendor_key=resolve_vendor_key(vendor, device_type),
)
cmd = str(command or "show interface brief").strip() or "show interface brief"
rows = parse_cli(platform=platform, command=cmd, text=raw_text) if platform else []
def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
seen: set[str] = set()
for r in rows:
@ -53,8 +41,20 @@ def normalize_interface_brief(
"description": row_get(r, "DESCRIPTION", "description")[:256],
}
)
if out:
return out
return out
def _hand_parse(
*,
raw_text: str,
vendor: str = "",
device_type: str = "",
command: str = "",
**_kw: Any,
) -> list[dict[str, Any]]:
_ = (vendor, device_type, command)
out: list[dict[str, Any]] = []
seen: set[str] = set()
for raw in str(raw_text or "").splitlines():
line = raw.strip()
if not line or line.lower().startswith("interface"):
@ -79,3 +79,40 @@ def normalize_interface_brief(
}
)
return out
def normalize_interface_brief(
*,
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 interface brief").strip() or "show interface brief"
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_interface_brief.RULE_KEYS = RULE_KEYS

View file

@ -13,16 +13,19 @@ _ISIS_ROW_RE = re.compile(
)
_PROCESS_RE = re.compile(r"(?i)^\s*Process\s+ID\s*:\s*(\d+)\s*$")
RULE_KEYS: tuple[str, ...] = ()
def normalize_isis_adjacency(
*,
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)
_ = (vendor, device_type, command, params, fsm_tables)
out: list[dict[str, Any]] = []
process_id = ""
for raw in str(raw_text or "").splitlines():
@ -54,3 +57,5 @@ def normalize_isis_adjacency(
}
)
return out
normalize_isis_adjacency.RULE_KEYS = RULE_KEYS

View file

@ -11,16 +11,19 @@ _ND6_ROW_RE = re.compile(
re.I,
)
RULE_KEYS: tuple[str, ...] = ()
def normalize_nd6_cache(
*,
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)
_ = (vendor, device_type, command, params, fsm_tables)
out: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for raw in str(raw_text or "").splitlines():
@ -52,3 +55,5 @@ def normalize_nd6_cache(
}
)
return out
normalize_nd6_cache.RULE_KEYS = RULE_KEYS