mirror of
https://github.com/hansjone/netx.git
synced 2026-10-16 16:50:46 +08:00
Decouple cutover HF from portrait and tighten ops UI.
Portrait stays on biz-monitor; cutover create/ensure owns purpose=cutover_hf slots with project collect override, multi-interval bindings, window pause, and industrial detail UX. Also expand ZTE collect/config parsers and TextFSM for route/BGP/L2VPN/VRRP coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a65c9fc420
commit
285c19e5a3
67 changed files with 9510 additions and 723 deletions
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
LLDP already works via ``common.lldp_neighbors`` (profile ``parser_id=lldp_neighbors``).
|
||||
|
||||
Stub modules (not registered until implemented)::
|
||||
Shared ``metric_id`` schemas live in ``profiles.py`` (ZTE is the reference
|
||||
implementation). Stub modules keep normalize signatures aligned — do **not**
|
||||
register in ``PARSERS`` until implemented::
|
||||
|
||||
interface_brief.py arp.py isis_adjacency.py nd6_cache.py bgp_peer.py
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,14 @@ from .pipeline import prefer_fsm
|
|||
|
||||
RULE_KEYS = ("zte_zxros_show_ip_vrf",)
|
||||
|
||||
_VRF_LINE_RE = re.compile(
|
||||
r"^\s*(?P<name>[A-Za-z0-9_./:-]+)\s+"
|
||||
r"(?P<rd>\d+:\d+|<not set>|\S+:\S+)\s*"
|
||||
r"(?:(?P<protocols>ipv4(?:,ipv6)?|ipv6(?:,ipv4)?|ipv4|ipv6)\s*)?"
|
||||
r"(?P<vrf_id>\d+)?\s*$",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
|
|
@ -22,11 +30,16 @@ def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
protocols = row_get(r, "PROTOCOLS", "ADDRESS_FAMILY", "protocols")
|
||||
# Legacy templates may put protocols in INTERFACES
|
||||
if not protocols:
|
||||
protocols = row_get(r, "INTERFACES", "interfaces")
|
||||
out.append(
|
||||
{
|
||||
"vrf_name": name[:128],
|
||||
"rd": row_get(r, "DEFAULT_RD", "RD", "rd")[:64],
|
||||
"protocols": row_get(r, "PROTOCOLS", "ADDRESS_FAMILY", "INTERFACES", "protocols")[:64],
|
||||
"rd": row_get(r, "RD", "DEFAULT_RD", "rd")[:64],
|
||||
"protocols": protocols[:64],
|
||||
"vrf_id": row_get(r, "VRF_ID", "vrf_id")[:32],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
|
@ -36,16 +49,23 @@ def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
|||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for line in str(raw_text or "").splitlines():
|
||||
m = re.match(r"^\s*([A-Za-z0-9_./:-]+)\s+(\d+:\d+|<not set>|\S+:\S+)\s*", line)
|
||||
m = _VRF_LINE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group(1)
|
||||
name = m.group("name")
|
||||
if name.lower() in ("name", "vrf", "vpn-instance", "total"):
|
||||
continue
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
out.append({"vrf_name": name[:128], "rd": m.group(2)[:64], "protocols": ""})
|
||||
out.append(
|
||||
{
|
||||
"vrf_name": name[:128],
|
||||
"rd": (m.group("rd") or "")[:64],
|
||||
"protocols": (m.group("protocols") or "")[:64],
|
||||
"vrf_id": (m.group("vrf_id") or "")[:32],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@
|
|||
|
||||
LLDP already works via ``common.lldp_neighbors`` (``display lldp neighbor``).
|
||||
|
||||
Stub modules (not registered until implemented)::
|
||||
Shared ``metric_id`` / FieldDef schemas (cross-vendor compare) are defined in
|
||||
``profiles.py`` for ZTE first. Stub modules below keep the same normalize
|
||||
signatures for future Huawei implementations — do **not** register in
|
||||
``PARSERS`` until TextFSM + prefer_fsm paths exist (avoids NotImplementedError
|
||||
at collect time)::
|
||||
|
||||
interface_brief.py arp.py isis_adjacency.py nd6_cache.py bgp_peer.py
|
||||
|
||||
When ready, import and add to ``PARSERS``.
|
||||
When ready, import and add to ``PARSERS``. Prefer same field names as ZTE
|
||||
(``bgp_peer``, ``ospf_neighbor``, ``vrrp``, ``ip_route``, …).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -10,20 +10,54 @@ from typing import Any, Callable
|
|||
|
||||
from .arp import is_valid_arp_age, normalize_arp
|
||||
from .bgp_peer import normalize_bgp_peer
|
||||
from .bgp_route import normalize_bgp_route
|
||||
from .config_bgp_peer import normalize_config_bgp_peer
|
||||
from .config_interface import normalize_config_interface
|
||||
from .config_isis import normalize_config_isis
|
||||
from .config_l2vpn_pw import normalize_config_l2vpn_pw
|
||||
from .config_ospf import normalize_config_ospf
|
||||
from .config_static_route import normalize_config_static_route
|
||||
from .config_vrf import normalize_config_vrf
|
||||
from .evpn_mac import normalize_evpn_mac
|
||||
from .if_intf import normalize_if_intf, parse_if_intf_vrf_map
|
||||
from .interface_brief import normalize_interface_brief
|
||||
from .interface_detail import normalize_interface_detail
|
||||
from .ip_route import normalize_ip_route
|
||||
from .ipv6_route import normalize_ipv6_route
|
||||
from .isis_adjacency import normalize_isis_adjacency
|
||||
from .l2vpn_mac import normalize_l2vpn_mac
|
||||
from .l2vpn_pw import normalize_l2vpn_pw
|
||||
from .nd6_cache import normalize_nd6_cache
|
||||
from .optical_brief import normalize_optical_brief
|
||||
from .ospf_neighbor import normalize_ospf_neighbor
|
||||
from .vrrp import normalize_vrrp
|
||||
|
||||
NormalizeFn = Callable[..., list[dict[str, Any]]]
|
||||
|
||||
PARSERS: dict[str, NormalizeFn] = {
|
||||
"isis_adjacency": normalize_isis_adjacency,
|
||||
"interface_brief": normalize_interface_brief,
|
||||
"interface_detail": normalize_interface_detail,
|
||||
"arp": normalize_arp,
|
||||
"if_intf": normalize_if_intf,
|
||||
"nd6_cache": normalize_nd6_cache,
|
||||
"bgp_peer": normalize_bgp_peer,
|
||||
"ospf_neighbor": normalize_ospf_neighbor,
|
||||
"vrrp": normalize_vrrp,
|
||||
"optical_brief": normalize_optical_brief,
|
||||
"bgp_route": normalize_bgp_route,
|
||||
"ip_route": normalize_ip_route,
|
||||
"ipv6_route": normalize_ipv6_route,
|
||||
"l2vpn_pw": normalize_l2vpn_pw,
|
||||
"l2vpn_mac": normalize_l2vpn_mac,
|
||||
"evpn_mac": normalize_evpn_mac,
|
||||
"config_vrf": normalize_config_vrf,
|
||||
"config_interface": normalize_config_interface,
|
||||
"config_bgp_peer": normalize_config_bgp_peer,
|
||||
"config_l2vpn_pw": normalize_config_l2vpn_pw,
|
||||
"config_static_route": normalize_config_static_route,
|
||||
"config_ospf": normalize_config_ospf,
|
||||
"config_isis": normalize_config_isis,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -31,9 +65,26 @@ __all__ = [
|
|||
"is_valid_arp_age",
|
||||
"normalize_arp",
|
||||
"normalize_bgp_peer",
|
||||
"normalize_bgp_route",
|
||||
"normalize_config_bgp_peer",
|
||||
"normalize_config_interface",
|
||||
"normalize_config_isis",
|
||||
"normalize_config_l2vpn_pw",
|
||||
"normalize_config_ospf",
|
||||
"normalize_config_static_route",
|
||||
"normalize_config_vrf",
|
||||
"normalize_evpn_mac",
|
||||
"normalize_if_intf",
|
||||
"normalize_interface_brief",
|
||||
"normalize_interface_detail",
|
||||
"normalize_ip_route",
|
||||
"normalize_ipv6_route",
|
||||
"normalize_isis_adjacency",
|
||||
"normalize_l2vpn_mac",
|
||||
"normalize_l2vpn_pw",
|
||||
"normalize_nd6_cache",
|
||||
"normalize_optical_brief",
|
||||
"normalize_ospf_neighbor",
|
||||
"normalize_vrrp",
|
||||
"parse_if_intf_vrf_map",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,23 +1,39 @@
|
|||
"""ZTE: show bgp {vpnv4|ipv4|vpnv6} unicast summary."""
|
||||
"""ZTE: show bgp {afi} [unicast] [vrf <vrf>] summary (and l2vpn summaries)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Mapping
|
||||
|
||||
_BGP_PEER_RE = re.compile(
|
||||
r"^(?P<nei>\d{1,3}(?:\.\d{1,3}){3})\s+(?P<ver>\d+)\s+(?P<asn>\d+)\s+"
|
||||
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_bgp_summary",)
|
||||
|
||||
_PEER_LINE_RE = re.compile(
|
||||
r"^(?P<nei>\d{1,3}(?:\.\d{1,3}){3}|[0-9A-Fa-f:]+)\s+"
|
||||
r"(?P<ver>\d+)\s+(?P<asn>\S+)\s+"
|
||||
r"(?P<rx>\d+)\s+(?P<tx>\d+)\s+(?P<up>\S+)\s+(?P<state>\S+)\s*$",
|
||||
re.I,
|
||||
)
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
_NEI_ONLY_RE = re.compile(r"^(?P<nei>[0-9A-Fa-f:]+)\s*$", re.I)
|
||||
_CONT_RE = re.compile(
|
||||
r"^\s+(?P<ver>\d+)\s+(?P<asn>\S+)\s+"
|
||||
r"(?P<rx>\d+)\s+(?P<tx>\d+)\s+(?P<up>\S+)\s+(?P<state>\S+)\s*$",
|
||||
re.I,
|
||||
)
|
||||
_VRF_IN_CMD_RE = re.compile(r"(?i)\bvrf\s+(\S+)")
|
||||
|
||||
|
||||
def _detect_bgp_afi(command: str, params: dict[str, str] | None) -> str:
|
||||
if params and params.get("afi"):
|
||||
return str(params.get("afi") or "").strip().lower()
|
||||
low = str(command or "").lower()
|
||||
if "l2vpn" in low and "evpn" in low:
|
||||
return "evpn"
|
||||
if "l2vpn" in low and "vpls" in low:
|
||||
return "vpls"
|
||||
if "vpnv6" in low:
|
||||
return "vpnv6"
|
||||
if "vpnv4" in low:
|
||||
|
|
@ -29,55 +45,186 @@ def _detect_bgp_afi(command: str, params: dict[str, str] | None) -> str:
|
|||
return "unknown"
|
||||
|
||||
|
||||
def _detect_vrf(command: str, params: dict[str, str] | None) -> str:
|
||||
if params and params.get("vrf"):
|
||||
return str(params.get("vrf") or "").strip()
|
||||
m = _VRF_IN_CMD_RE.search(str(command or ""))
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
|
||||
def _state_and_pfx(state_raw: str) -> tuple[str, str]:
|
||||
raw = str(state_raw or "").strip()
|
||||
if raw.isdigit():
|
||||
return "Established", raw
|
||||
# e.g. (NoNeg)
|
||||
return raw, ""
|
||||
|
||||
|
||||
def _peer_row(
|
||||
*,
|
||||
afi: str,
|
||||
vrf: str,
|
||||
nei: str,
|
||||
ver: str,
|
||||
asn: str,
|
||||
rx: str,
|
||||
tx: str,
|
||||
up: str,
|
||||
state_raw: str,
|
||||
) -> dict[str, Any]:
|
||||
state, pfx = _state_and_pfx(state_raw)
|
||||
return {
|
||||
"afi": afi[:32],
|
||||
"vrf": vrf[:128],
|
||||
"neighbor": nei[:128],
|
||||
"ver": ver[:8],
|
||||
"as_num": asn[:16],
|
||||
"msg_rcvd": rx[:32],
|
||||
"msg_send": tx[:32],
|
||||
"up_down": up[:32],
|
||||
"state": state[:64],
|
||||
"pfx_rcd": pfx[:32],
|
||||
}
|
||||
|
||||
|
||||
def _map_fsm_rows(
|
||||
rows: list[dict[str, Any]],
|
||||
*,
|
||||
afi: str,
|
||||
vrf: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
pending = ""
|
||||
for r in rows:
|
||||
nei = row_get(r, "NEIGHBOR", "neighbor")
|
||||
ver = row_get(r, "VER", "ver")
|
||||
if nei and not ver:
|
||||
pending = nei
|
||||
continue
|
||||
if not nei and pending and ver:
|
||||
nei = pending
|
||||
pending = ""
|
||||
if not nei or not ver:
|
||||
continue
|
||||
if nei in seen:
|
||||
continue
|
||||
seen.add(nei)
|
||||
state_raw = row_get(r, "STATE_PFX", "STATE", "state", "pfx_rcd")
|
||||
out.append(
|
||||
_peer_row(
|
||||
afi=afi,
|
||||
vrf=vrf,
|
||||
nei=nei,
|
||||
ver=ver,
|
||||
asn=row_get(r, "ASN", "AS", "as_num"),
|
||||
rx=row_get(r, "MSG_RCVD", "msg_rcvd"),
|
||||
tx=row_get(r, "MSG_SEND", "msg_send"),
|
||||
up=row_get(r, "UP_DOWN", "up_down"),
|
||||
state_raw=state_raw,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(
|
||||
*,
|
||||
raw_text: str,
|
||||
afi: str = "unknown",
|
||||
vrf: str = "",
|
||||
**_kw: Any,
|
||||
) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
pending = ""
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low.startswith("bgp router") or low.startswith("local as") or low.startswith("all "):
|
||||
continue
|
||||
if low.startswith("neighbor") and "msg" in low:
|
||||
continue
|
||||
m = _PEER_LINE_RE.match(line.strip())
|
||||
if m:
|
||||
nei = m.group("nei")
|
||||
if nei in seen:
|
||||
continue
|
||||
seen.add(nei)
|
||||
pending = ""
|
||||
out.append(
|
||||
_peer_row(
|
||||
afi=afi,
|
||||
vrf=vrf,
|
||||
nei=nei,
|
||||
ver=m.group("ver"),
|
||||
asn=m.group("asn"),
|
||||
rx=m.group("rx"),
|
||||
tx=m.group("tx"),
|
||||
up=m.group("up"),
|
||||
state_raw=m.group("state"),
|
||||
)
|
||||
)
|
||||
continue
|
||||
m_only = _NEI_ONLY_RE.match(line.strip())
|
||||
if m_only:
|
||||
pending = m_only.group("nei")
|
||||
continue
|
||||
m_cont = _CONT_RE.match(line)
|
||||
if m_cont and pending:
|
||||
nei = pending
|
||||
pending = ""
|
||||
if nei in seen:
|
||||
continue
|
||||
seen.add(nei)
|
||||
out.append(
|
||||
_peer_row(
|
||||
afi=afi,
|
||||
vrf=vrf,
|
||||
nei=nei,
|
||||
ver=m_cont.group("ver"),
|
||||
asn=m_cont.group("asn"),
|
||||
rx=m_cont.group("rx"),
|
||||
tx=m_cont.group("tx"),
|
||||
up=m_cont.group("up"),
|
||||
state_raw=m_cont.group("state"),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_bgp_peer(
|
||||
*,
|
||||
raw_text: str,
|
||||
fsm_tables=None,
|
||||
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]]:
|
||||
_ = (vendor, device_type, fsm_tables)
|
||||
afi = _detect_bgp_afi(command, params)
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
low = line.lower()
|
||||
if low.startswith("bgp router") or low.startswith("local as") or low.startswith("all "):
|
||||
continue
|
||||
if low.startswith("neighbor") and "msg" in low:
|
||||
continue
|
||||
m = _BGP_PEER_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
nei = m.group("nei")
|
||||
if nei in seen:
|
||||
continue
|
||||
seen.add(nei)
|
||||
state_raw = m.group("state")
|
||||
if state_raw.isdigit():
|
||||
state = "Established"
|
||||
pfx = state_raw
|
||||
else:
|
||||
state = state_raw
|
||||
pfx = ""
|
||||
out.append(
|
||||
{
|
||||
"afi": afi[:32],
|
||||
"neighbor": nei[:64],
|
||||
"ver": m.group("ver")[:8],
|
||||
"as_num": m.group("asn")[:16],
|
||||
"msg_rcvd": m.group("rx")[:32],
|
||||
"msg_send": m.group("tx")[:32],
|
||||
"up_down": m.group("up")[:32],
|
||||
"state": state[:64],
|
||||
"pfx_rcd": pfx[:32],
|
||||
}
|
||||
vrf = _detect_vrf(command, 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),
|
||||
)
|
||||
return out
|
||||
cmd = str(command or "show bgp summary").strip()
|
||||
if platform and cmd:
|
||||
tables = apply_rules(
|
||||
platform=platform, text=raw_text, rule_keys=RULE_KEYS, command=cmd
|
||||
)
|
||||
|
||||
def _map(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return _map_fsm_rows(rows, afi=afi, vrf=vrf)
|
||||
|
||||
def _hand(*, raw_text: str, **kw: Any) -> list[dict[str, Any]]:
|
||||
return _hand_parse(raw_text=raw_text, afi=afi, vrf=vrf, **kw)
|
||||
|
||||
return prefer_fsm(tables, RULE_KEYS, _map, _hand, raw_text=raw_text)
|
||||
|
||||
|
||||
normalize_bgp_peer.RULE_KEYS = RULE_KEYS
|
||||
|
|
|
|||
199
netx_api/biz_state/parsers/zte/bgp_route.py
Normal file
199
netx_api/biz_state/parsers/zte/bgp_route.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
"""ZTE: show bgp … neighbor {in|out} [vrf] routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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
|
||||
from .bgp_peer import _detect_bgp_afi, _detect_vrf
|
||||
|
||||
RULE_KEYS = ("zte_zxros_show_bgp_neighbor_routes",)
|
||||
|
||||
_ROUTE_RE = re.compile(
|
||||
r"^\s*(?P<flags>[*<>isd]*)\s*"
|
||||
r"(?P<net>\d{1,3}(?:\.\d{1,3}){3}/\d+|[0-9A-Fa-f:]+(?:/\d+)?)\s+"
|
||||
r"(?P<nh>\S+)\s+"
|
||||
r"(?P<rest>.*)$"
|
||||
)
|
||||
_DIR_RE = re.compile(r"(?i)\bneighbor\s+(in|out)\s+")
|
||||
_NEI_RE = re.compile(r"(?i)\bneighbor\s+(?:in|out)\s+(\S+)")
|
||||
|
||||
|
||||
def _detect_direction(command: str, params: dict[str, str] | None) -> str:
|
||||
if params and params.get("direction"):
|
||||
return str(params.get("direction") or "").strip().lower()
|
||||
m = _DIR_RE.search(str(command or ""))
|
||||
return m.group(1).lower() if m else ""
|
||||
|
||||
|
||||
def _detect_neighbor(command: str, params: dict[str, str] | None) -> str:
|
||||
if params and params.get("neighbor"):
|
||||
return str(params.get("neighbor") or "").strip()
|
||||
m = _NEI_RE.search(str(command or ""))
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
|
||||
def _split_rest(rest: str) -> tuple[str, str, str, str]:
|
||||
"""Parse trailing Metric LocPrf Tag/RtPrf Path columns (some may be blank)."""
|
||||
parts = str(rest or "").split()
|
||||
if not parts:
|
||||
return "", "", "", ""
|
||||
# Last token(s) are AS path + origin; path ends with i|e|?
|
||||
path = " ".join(parts)
|
||||
metric = loc = tag = ""
|
||||
# Heuristic: numeric-only leading fields are metric/loc/tag when present
|
||||
nums: list[str] = []
|
||||
path_parts: list[str] = []
|
||||
for p in parts:
|
||||
if not path_parts and re.fullmatch(r"\d+", p):
|
||||
nums.append(p)
|
||||
else:
|
||||
path_parts.append(p)
|
||||
if len(nums) >= 1:
|
||||
metric = nums[0]
|
||||
if len(nums) >= 2:
|
||||
loc = nums[1]
|
||||
if len(nums) >= 3:
|
||||
tag = nums[2]
|
||||
path = " ".join(path_parts)
|
||||
return metric, loc, tag, path
|
||||
|
||||
|
||||
def _map_fsm_rows(
|
||||
rows: list[dict[str, Any]],
|
||||
*,
|
||||
afi: str,
|
||||
vrf: str,
|
||||
neighbor: str,
|
||||
direction: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for r in rows:
|
||||
net = row_get(r, "NETWORK", "network")
|
||||
if not net or net.lower() == "network" or net in seen:
|
||||
continue
|
||||
seen.add(net)
|
||||
path = row_get(r, "PATH", "path")
|
||||
out.append(
|
||||
{
|
||||
"afi": afi[:32],
|
||||
"vrf": vrf[:128],
|
||||
"neighbor": neighbor[:128],
|
||||
"direction": direction[:8],
|
||||
"network": net[:128],
|
||||
"next_hop": row_get(r, "NEXT_HOP", "next_hop")[:128],
|
||||
"metric": row_get(r, "METRIC", "metric")[:32],
|
||||
"loc_prf": row_get(r, "LOC_PRF", "loc_prf")[:32],
|
||||
"tag": row_get(r, "TAG", "RT_PRF", "tag")[:32],
|
||||
"path": path[:256],
|
||||
"status_codes": "",
|
||||
"as_num": "",
|
||||
"state": "",
|
||||
"pfx_rcd": "",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(
|
||||
*,
|
||||
raw_text: str,
|
||||
afi: str = "unknown",
|
||||
vrf: str = "",
|
||||
neighbor: str = "",
|
||||
direction: str = "",
|
||||
**_kw: Any,
|
||||
) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low.startswith("network") or "next hop" in low:
|
||||
continue
|
||||
if low.startswith("status") or low.startswith("origin") or low.startswith("routes "):
|
||||
continue
|
||||
if low.startswith("local ") or low.startswith("remote ") or low.startswith("total "):
|
||||
continue
|
||||
if low.startswith("route distinguisher"):
|
||||
continue
|
||||
m = _ROUTE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
net = m.group("net")
|
||||
if net in seen:
|
||||
continue
|
||||
seen.add(net)
|
||||
metric, loc, tag, path = _split_rest(m.group("rest"))
|
||||
out.append(
|
||||
{
|
||||
"afi": afi[:32],
|
||||
"vrf": vrf[:128],
|
||||
"neighbor": neighbor[:128],
|
||||
"direction": direction[:8],
|
||||
"network": net[:128],
|
||||
"next_hop": m.group("nh")[:128],
|
||||
"metric": metric[:32],
|
||||
"loc_prf": loc[:32],
|
||||
"tag": tag[:32],
|
||||
"path": path[:256],
|
||||
"status_codes": (m.group("flags") or "").strip()[:16],
|
||||
"as_num": "",
|
||||
"state": "",
|
||||
"pfx_rcd": "",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_bgp_route(
|
||||
*,
|
||||
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]]:
|
||||
afi = _detect_bgp_afi(command, params)
|
||||
vrf = _detect_vrf(command, params)
|
||||
neighbor = _detect_neighbor(command, params)
|
||||
direction = _detect_direction(command, 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 bgp neighbor routes").strip()
|
||||
if platform and cmd:
|
||||
tables = apply_rules(
|
||||
platform=platform, text=raw_text, rule_keys=RULE_KEYS, command=cmd
|
||||
)
|
||||
|
||||
def _map(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return _map_fsm_rows(
|
||||
rows, afi=afi, vrf=vrf, neighbor=neighbor, direction=direction
|
||||
)
|
||||
|
||||
def _hand(*, raw_text: str, **kw: Any) -> list[dict[str, Any]]:
|
||||
return _hand_parse(
|
||||
raw_text=raw_text,
|
||||
afi=afi,
|
||||
vrf=vrf,
|
||||
neighbor=neighbor,
|
||||
direction=direction,
|
||||
**kw,
|
||||
)
|
||||
|
||||
return prefer_fsm(tables, RULE_KEYS, _map, _hand, raw_text=raw_text)
|
||||
|
||||
|
||||
normalize_bgp_route.RULE_KEYS = RULE_KEYS
|
||||
216
netx_api/biz_state/parsers/zte/config_bgp_peer.py
Normal file
216
netx_api/biz_state/parsers/zte/config_bgp_peer.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
"""ZTE config intent: ``show running-config bgp`` / MIM ``!<bgp>``.
|
||||
|
||||
Emits one row per ``(afi, vrf, neighbor|peer_group)`` activation under
|
||||
address-family. IP literals go in ``neighbor``; non-IP names (peer-groups)
|
||||
go in ``peer_group``. Never captures password / secret lines.
|
||||
|
||||
Route-maps under an address-family are scoped to that ``(afi, vrf)``;
|
||||
global (top-level) route-maps apply to all AF rows for that neighbor and
|
||||
are overlaid by AF-specific maps when both exist.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_NEI_REMOTE_RE = re.compile(r"^\s*neighbor\s+(\S+)\s+remote-as\s+(\S+)\s*$", re.I)
|
||||
_NEI_UPD_RE = re.compile(r"^\s*neighbor\s+(\S+)\s+update-source\s+(\S+)\s*$", re.I)
|
||||
_NEI_PG_RE = re.compile(r"^\s*neighbor\s+(\S+)\s+peer-group(?:\s+(\S+))?\s*$", re.I)
|
||||
_NEI_RM_RE = re.compile(
|
||||
r"^\s*neighbor\s+(\S+)\s+route-map\s+(\S+)\s+(in|out)\s*$", re.I
|
||||
)
|
||||
_NEI_ACT_RE = re.compile(
|
||||
r"^\s*neighbor\s+(\S+)\s+activate(?:\s+(disable))?\s*$", re.I
|
||||
)
|
||||
_IPV4_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$")
|
||||
|
||||
_AfKey = tuple[str, str, str] # (afi, vrf, token)
|
||||
|
||||
|
||||
def _is_ip_neighbor(token: str) -> bool:
|
||||
"""True for IPv4/IPv6 literals; false for peer-group names (even with ':')."""
|
||||
tok = str(token or "").strip()
|
||||
if not tok:
|
||||
return False
|
||||
if _IPV4_RE.match(tok):
|
||||
return True
|
||||
# IPv6 must look like an address, not ``asn:name`` peer-group labels.
|
||||
if ":" not in tok:
|
||||
return False
|
||||
try:
|
||||
import ipaddress
|
||||
|
||||
ipaddress.ip_address(tok.split("%", 1)[0])
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _split_neighbor_cols(token: str, info: Mapping[str, str]) -> tuple[str, str]:
|
||||
"""Return ``(neighbor, peer_group)`` columns for a neighbor token."""
|
||||
tok = str(token or "").strip()
|
||||
if _is_ip_neighbor(tok):
|
||||
return tok, str(info.get("peer_group") or "")
|
||||
# Non-IP token is a peer-group name
|
||||
return "", tok
|
||||
|
||||
|
||||
def _merge_info(
|
||||
token: str,
|
||||
*,
|
||||
afi: str,
|
||||
vrf: str,
|
||||
meta: Mapping[str, dict[str, str]],
|
||||
af_rm: Mapping[_AfKey, dict[str, str]],
|
||||
) -> dict[str, str]:
|
||||
"""Global neighbor meta + AF-scoped route-maps (AF wins on conflict)."""
|
||||
info = dict(meta.get(token) or {})
|
||||
scoped = af_rm.get((afi, vrf, token)) or {}
|
||||
for key in ("route_map_in", "route_map_out"):
|
||||
if scoped.get(key):
|
||||
info[key] = scoped[key]
|
||||
return info
|
||||
|
||||
|
||||
def _row(
|
||||
*,
|
||||
afi: str,
|
||||
vrf: str,
|
||||
token: str,
|
||||
act: str,
|
||||
info: Mapping[str, str],
|
||||
) -> dict[str, Any]:
|
||||
neighbor, peer_group = _split_neighbor_cols(token, info)
|
||||
return {
|
||||
"afi": afi[:32],
|
||||
"vrf": vrf[:128],
|
||||
"neighbor": neighbor[:128],
|
||||
"peer_group": peer_group[:64],
|
||||
"remote_as": str(info.get("remote_as") or "")[:16],
|
||||
"activate": act[:16],
|
||||
"update_source": str(info.get("update_source") or "")[:64],
|
||||
"route_map_in": str(info.get("route_map_in") or "")[:128],
|
||||
"route_map_out": str(info.get("route_map_out") or "")[:128],
|
||||
}
|
||||
|
||||
|
||||
def normalize_config_bgp_peer(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, command, params)
|
||||
body = extract_mim_section(raw_text, "bgp")
|
||||
meta: dict[str, dict[str, str]] = {}
|
||||
af_rm: dict[_AfKey, dict[str, str]] = {}
|
||||
afi = ""
|
||||
vrf = ""
|
||||
activations: list[tuple[str, str, str, str]] = []
|
||||
# (afi, vrf, token, activate)
|
||||
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _NEI_REMOTE_RE.match(line)
|
||||
if m:
|
||||
nei = m.group(1).strip()
|
||||
meta.setdefault(nei, {})
|
||||
meta[nei]["remote_as"] = m.group(2).strip()
|
||||
continue
|
||||
m = _NEI_UPD_RE.match(line)
|
||||
if m:
|
||||
nei = m.group(1).strip()
|
||||
meta.setdefault(nei, {})
|
||||
meta[nei]["update_source"] = m.group(2).strip()
|
||||
continue
|
||||
m = _NEI_PG_RE.match(line)
|
||||
if m:
|
||||
nei = m.group(1).strip()
|
||||
meta.setdefault(nei, {})
|
||||
group = (m.group(2) or "").strip()
|
||||
if group:
|
||||
# neighbor <ip> peer-group <name>
|
||||
meta[nei]["peer_group"] = group
|
||||
else:
|
||||
# neighbor <name> peer-group → peer-group definition
|
||||
meta[nei]["is_group"] = "1"
|
||||
continue
|
||||
m = _NEI_RM_RE.match(line)
|
||||
if m and not afi:
|
||||
nei = m.group(1).strip()
|
||||
meta.setdefault(nei, {})
|
||||
direction = m.group(3).lower()
|
||||
key = "route_map_in" if direction == "in" else "route_map_out"
|
||||
meta[nei][key] = m.group(2).strip()
|
||||
continue
|
||||
m = re.match(r"^\s*address-family\s+ipv4\s+vrf\s+(\S+)\s*$", line, re.I)
|
||||
if m:
|
||||
afi, vrf = "ipv4", m.group(1).strip()
|
||||
continue
|
||||
m = re.match(r"^\s*address-family\s+ipv6\s+vrf\s+(\S+)\s*$", line, re.I)
|
||||
if m:
|
||||
afi, vrf = "ipv6", m.group(1).strip()
|
||||
continue
|
||||
m = re.match(r"^\s*address-family\s+l2vpn\s+(\S+)\s*$", line, re.I)
|
||||
if m:
|
||||
afi, vrf = f"l2vpn-{m.group(1).lower()}", ""
|
||||
continue
|
||||
m = re.match(r"^\s*address-family\s+(\S+)\s*$", line, re.I)
|
||||
if m:
|
||||
afi, vrf = m.group(1).lower(), ""
|
||||
continue
|
||||
if re.match(r"^\s*\$\s*$", line) and afi:
|
||||
if line.startswith(" ") and not line.startswith(" "):
|
||||
afi, vrf = "", ""
|
||||
continue
|
||||
m = _NEI_ACT_RE.match(line)
|
||||
if m and afi:
|
||||
nei = m.group(1).strip()
|
||||
act = "disable" if m.group(2) else "enable"
|
||||
activations.append((afi, vrf, nei, act))
|
||||
continue
|
||||
m = _NEI_RM_RE.match(line)
|
||||
if m and afi:
|
||||
nei = m.group(1).strip()
|
||||
direction = m.group(3).lower()
|
||||
rk = "route_map_in" if direction == "in" else "route_map_out"
|
||||
slot = af_rm.setdefault((afi, vrf, nei), {})
|
||||
slot[rk] = m.group(2).strip()
|
||||
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str, str]] = set()
|
||||
for afi_s, vrf_s, token, act in activations:
|
||||
info = _merge_info(token, afi=afi_s, vrf=vrf_s, meta=meta, af_rm=af_rm)
|
||||
row = _row(afi=afi_s, vrf=vrf_s, token=token, act=act, info=info)
|
||||
key = (row["afi"], row["vrf"], row["neighbor"], row["peer_group"])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(row)
|
||||
|
||||
# Global peers with remote-as but no AF activate
|
||||
for token, info in meta.items():
|
||||
if not info.get("remote_as"):
|
||||
continue
|
||||
if any(n == token for _, _, n, _ in activations):
|
||||
continue
|
||||
row = _row(afi="global", vrf="", token=token, act="", info=info)
|
||||
key = (row["afi"], row["vrf"], row["neighbor"], row["peer_group"])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
normalize_config_bgp_peer.RULE_KEYS = RULE_KEYS
|
||||
46
netx_api/biz_state/parsers/zte/config_common.py
Normal file
46
netx_api/biz_state/parsers/zte/config_common.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Helpers for ZTE MIM running-config section parsing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_MIM_OPEN = re.compile(r"^!<([^/>][^>]*)>\s*$")
|
||||
_MIM_CLOSE = re.compile(r"^!</([^>]+)>\s*$")
|
||||
|
||||
|
||||
def extract_mim_section(raw_text: str, section: str) -> str:
|
||||
"""Return body of ``!<section>`` … ``!</section>`` if present; else original text."""
|
||||
name = str(section or "").strip().lower()
|
||||
if not name:
|
||||
return str(raw_text or "")
|
||||
text = str(raw_text or "")
|
||||
lines = text.splitlines()
|
||||
start = -1
|
||||
for i, line in enumerate(lines):
|
||||
m = _MIM_OPEN.match(line.strip())
|
||||
if m and m.group(1).strip().lower() == name:
|
||||
start = i + 1
|
||||
break
|
||||
if start < 0:
|
||||
return text
|
||||
out: list[str] = []
|
||||
for line in lines[start:]:
|
||||
m = _MIM_CLOSE.match(line.strip())
|
||||
if m and m.group(1).strip().lower() == name:
|
||||
break
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def is_secret_line(line: str) -> bool:
|
||||
low = str(line or "").lower()
|
||||
return any(
|
||||
k in low
|
||||
for k in (
|
||||
"password",
|
||||
"secret",
|
||||
"encrypted",
|
||||
"authentication-key",
|
||||
"pre-shared-key",
|
||||
)
|
||||
)
|
||||
148
netx_api/biz_state/parsers/zte/config_interface.py
Normal file
148
netx_api/biz_state/parsers/zte/config_interface.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""ZTE config intent: ``show running-config if-intf`` / MIM ``!<if-intf>``.
|
||||
|
||||
Duplicate ``interface`` blocks (common for tunnels) are merged: later values
|
||||
override empty ones; ``admin`` always takes the last explicit setting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_IFACE_RE = re.compile(r"^\s*interface\s+(\S+)\s*$", re.I)
|
||||
_VRF_RE = re.compile(r"^\s*ip\s+vrf\s+forwarding\s+(\S+)\s*$", re.I)
|
||||
_IP_RE = re.compile(r"^\s*ip\s+address\s+(\S+)(?:\s+(\S+))?\s*$", re.I)
|
||||
_IP6_RE = re.compile(r"^\s*ipv6\s+address\s+(\S+)\s*$", re.I)
|
||||
_DESC_RE = re.compile(r"^\s*description\s+(.*?)\s*$", re.I)
|
||||
_MTU_RE = re.compile(r"^\s*mtu\s+(\d+)\s*$", re.I)
|
||||
|
||||
|
||||
def _row_from_cur(cur: dict[str, Any]) -> dict[str, Any]:
|
||||
ips = cur.get("_ips") or []
|
||||
ip6s = cur.get("_ip6s") or []
|
||||
return {
|
||||
"interface": str(cur.get("interface") or "")[:128],
|
||||
"vrf": str(cur.get("vrf") or "")[:128],
|
||||
"description": str(cur.get("description") or "")[:256],
|
||||
"admin": str(cur.get("admin") or "up")[:16],
|
||||
"ip_address": ",".join(ips)[:256],
|
||||
"ipv6_address": ",".join(ip6s)[:256],
|
||||
"mtu": str(cur.get("mtu") or "")[:16],
|
||||
"_admin_set": bool(cur.get("_admin_set")),
|
||||
}
|
||||
|
||||
|
||||
def _flush(
|
||||
cur: dict[str, Any] | None,
|
||||
out: list[dict[str, Any]],
|
||||
by_iface: dict[str, int],
|
||||
) -> None:
|
||||
if not cur:
|
||||
return
|
||||
iface = str(cur.get("interface") or "")
|
||||
if not iface:
|
||||
return
|
||||
row = _row_from_cur(cur)
|
||||
admin_set = bool(row.pop("_admin_set", False))
|
||||
if iface in by_iface:
|
||||
prev = out[by_iface[iface]]
|
||||
# Only override admin when this block explicitly set shutdown / no shutdown.
|
||||
if admin_set:
|
||||
prev["admin"] = row["admin"]
|
||||
for key in ("vrf", "description", "mtu"):
|
||||
if row[key]:
|
||||
prev[key] = row[key]
|
||||
if row["ip_address"]:
|
||||
existing = [x for x in (prev["ip_address"] or "").split(",") if x]
|
||||
for ip in row["ip_address"].split(","):
|
||||
if ip and ip not in existing:
|
||||
existing.append(ip)
|
||||
prev["ip_address"] = ",".join(existing)[:256]
|
||||
if row["ipv6_address"]:
|
||||
existing = [x for x in (prev["ipv6_address"] or "").split(",") if x]
|
||||
for ip in row["ipv6_address"].split(","):
|
||||
if ip and ip not in existing:
|
||||
existing.append(ip)
|
||||
prev["ipv6_address"] = ",".join(existing)[:256]
|
||||
return
|
||||
by_iface[iface] = len(out)
|
||||
out.append(row)
|
||||
|
||||
|
||||
def normalize_config_interface(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, command, params)
|
||||
body = extract_mim_section(raw_text, "if-intf")
|
||||
out: list[dict[str, Any]] = []
|
||||
by_iface: dict[str, int] = {}
|
||||
cur: dict[str, Any] | None = None
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _IFACE_RE.match(line)
|
||||
if m:
|
||||
_flush(cur, out, by_iface)
|
||||
cur = {
|
||||
"interface": m.group(1).strip(),
|
||||
"vrf": "",
|
||||
"description": "",
|
||||
"admin": "up",
|
||||
"_admin_set": False,
|
||||
"mtu": "",
|
||||
"_ips": [],
|
||||
"_ip6s": [],
|
||||
}
|
||||
continue
|
||||
if not cur:
|
||||
continue
|
||||
if re.match(r"^\$\s*$", line):
|
||||
_flush(cur, out, by_iface)
|
||||
cur = None
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low == "shutdown":
|
||||
cur["admin"] = "down"
|
||||
cur["_admin_set"] = True
|
||||
continue
|
||||
if low == "no shutdown":
|
||||
cur["admin"] = "up"
|
||||
cur["_admin_set"] = True
|
||||
continue
|
||||
m = _VRF_RE.match(line)
|
||||
if m:
|
||||
cur["vrf"] = m.group(1).strip()
|
||||
continue
|
||||
m = _DESC_RE.match(line)
|
||||
if m:
|
||||
cur["description"] = (m.group(1) or "").strip().strip('"')[:256]
|
||||
continue
|
||||
m = _IP_RE.match(line)
|
||||
if m:
|
||||
addr = m.group(1).strip()
|
||||
mask = (m.group(2) or "").strip()
|
||||
cur["_ips"].append(f"{addr}/{mask}" if mask else addr)
|
||||
continue
|
||||
m = _IP6_RE.match(line)
|
||||
if m:
|
||||
cur["_ip6s"].append(m.group(1).strip())
|
||||
continue
|
||||
m = _MTU_RE.match(line)
|
||||
if m:
|
||||
cur["mtu"] = m.group(1)
|
||||
_flush(cur, out, by_iface)
|
||||
return out
|
||||
|
||||
|
||||
normalize_config_interface.RULE_KEYS = RULE_KEYS
|
||||
210
netx_api/biz_state/parsers/zte/config_isis.py
Normal file
210
netx_api/biz_state/parsers/zte/config_isis.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
"""ZTE config intent: IS-IS from ``!<isis>`` / ``show running-config isis``.
|
||||
|
||||
One row per ``(process, vrf, interface)``. Process-level attrs (area, system-id,
|
||||
router-id, is-type) are copied onto each interface row.
|
||||
Never captures hello-authentication / encrypted secrets.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_PROC_RE = re.compile(
|
||||
r"(?i)^\s*router\s+isis\s+(\S+)(?:\s+vrf\s+(\S+))?\s*$"
|
||||
)
|
||||
_AREA_RE = re.compile(r"(?i)^\s*area\s+(\S+)\s*$")
|
||||
_SYS_RE = re.compile(r"(?i)^\s*system-id\s+(\S+)\s*$")
|
||||
_RID_RE = re.compile(r"(?i)^\s*router-id\s+(\S+)\s*$")
|
||||
_ISTYPE_RE = re.compile(r"(?i)^\s*is-type\s+(\S+)\s*$")
|
||||
_IFACE_RE = re.compile(r"(?i)^\s*interface\s+(\S+)\s*$")
|
||||
_CIRCUIT_RE = re.compile(r"(?i)^\s*circuit-type\s+(\S+)\s*$")
|
||||
_NET_RE = re.compile(r"(?i)^\s*network\s+(\S+)\s*$")
|
||||
_METRIC_RE = re.compile(r"(?i)^\s*metric\s+(\d+)\s*$")
|
||||
_V6_METRIC_RE = re.compile(r"(?i)^\s*ipv6\s+metric\s+(\d+)\s*$")
|
||||
_DOLLAR_RE = re.compile(r"^(\s*)\$\s*$")
|
||||
|
||||
|
||||
def _indent(spaces: str) -> int:
|
||||
return len(spaces.expandtabs(2))
|
||||
|
||||
|
||||
def _flush(
|
||||
proc: dict[str, Any] | None,
|
||||
iface: dict[str, Any] | None,
|
||||
out: list[dict[str, Any]],
|
||||
seen: set[tuple[str, ...]],
|
||||
) -> None:
|
||||
if not proc or not iface:
|
||||
return
|
||||
key = (proc["process_id"], proc["vrf"], iface["interface"])
|
||||
if key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"process_id": str(proc["process_id"])[:32],
|
||||
"vrf": str(proc.get("vrf") or "")[:128],
|
||||
"area": str(proc.get("area") or "")[:64],
|
||||
"system_id": str(proc.get("system_id") or "")[:64],
|
||||
"router_id": str(proc.get("router_id") or "")[:64],
|
||||
"is_type": str(proc.get("is_type") or "")[:32],
|
||||
"interface": str(iface.get("interface") or "")[:128],
|
||||
"circuit_type": str(iface.get("circuit_type") or "")[:32],
|
||||
"network_type": str(iface.get("network_type") or "")[:32],
|
||||
"ip_enable": str(iface.get("ip_enable") or "")[:8],
|
||||
"ipv6_enable": str(iface.get("ipv6_enable") or "")[:8],
|
||||
"metric": str(iface.get("metric") or "")[:16],
|
||||
"ipv6_metric": str(iface.get("ipv6_metric") or "")[:16],
|
||||
"passive": str(iface.get("passive") or "")[:8],
|
||||
"bfd": str(iface.get("bfd") or "")[:16],
|
||||
"ipv6_bfd": str(iface.get("ipv6_bfd") or "")[:16],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def normalize_config_isis(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, command, params)
|
||||
body = extract_mim_section(raw_text, "isis")
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
proc: dict[str, Any] | None = None
|
||||
iface: dict[str, Any] | None = None
|
||||
in_af = False
|
||||
af_indent = 0
|
||||
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _PROC_RE.match(line)
|
||||
if m:
|
||||
_flush(proc, iface, out, seen)
|
||||
iface = None
|
||||
in_af = False
|
||||
af_indent = 0
|
||||
proc = {
|
||||
"process_id": m.group(1).strip(),
|
||||
"vrf": (m.group(2) or "").strip(),
|
||||
"area": "",
|
||||
"system_id": "",
|
||||
"router_id": "",
|
||||
"is_type": "",
|
||||
}
|
||||
continue
|
||||
if not proc:
|
||||
continue
|
||||
|
||||
# Skip address-family subtree (nested $ must not close the process).
|
||||
if in_af:
|
||||
m = _DOLLAR_RE.match(line)
|
||||
if m and _indent(m.group(1)) <= af_indent:
|
||||
in_af = False
|
||||
af_indent = 0
|
||||
continue
|
||||
|
||||
if re.match(r"(?i)^\s*address-family\s+", line):
|
||||
in_af = True
|
||||
af_indent = len(line) - len(line.lstrip(" \t"))
|
||||
continue
|
||||
|
||||
m = _DOLLAR_RE.match(line)
|
||||
if m:
|
||||
ind = _indent(m.group(1))
|
||||
if iface is not None and ind >= 2:
|
||||
_flush(proc, iface, out, seen)
|
||||
iface = None
|
||||
continue
|
||||
_flush(proc, iface, out, seen)
|
||||
iface = None
|
||||
proc = None
|
||||
continue
|
||||
|
||||
if iface is not None:
|
||||
m = _CIRCUIT_RE.match(line)
|
||||
if m:
|
||||
iface["circuit_type"] = m.group(1).lower()
|
||||
continue
|
||||
m = _NET_RE.match(line)
|
||||
if m:
|
||||
iface["network_type"] = m.group(1).lower()
|
||||
continue
|
||||
m = _V6_METRIC_RE.match(line)
|
||||
if m:
|
||||
iface["ipv6_metric"] = m.group(1)
|
||||
continue
|
||||
m = _METRIC_RE.match(line)
|
||||
if m:
|
||||
iface["metric"] = m.group(1)
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low == "ip router isis" or low.startswith("ip router isis "):
|
||||
iface["ip_enable"] = "yes"
|
||||
continue
|
||||
if low == "ipv6 router isis" or low.startswith("ipv6 router isis "):
|
||||
iface["ipv6_enable"] = "yes"
|
||||
continue
|
||||
if low == "passive-mode" or low == "passive":
|
||||
iface["passive"] = "yes"
|
||||
continue
|
||||
if low == "bfd-enable" or low.startswith("bfd-enable"):
|
||||
iface["bfd"] = "enable"
|
||||
continue
|
||||
if low == "ipv6 bfd-enable" or low.startswith("ipv6 bfd-enable"):
|
||||
iface["ipv6_bfd"] = "enable"
|
||||
continue
|
||||
continue
|
||||
|
||||
m = _IFACE_RE.match(line)
|
||||
if m:
|
||||
_flush(proc, iface, out, seen)
|
||||
iface = {
|
||||
"interface": m.group(1).strip(),
|
||||
"circuit_type": "",
|
||||
"network_type": "",
|
||||
"ip_enable": "",
|
||||
"ipv6_enable": "",
|
||||
"metric": "",
|
||||
"ipv6_metric": "",
|
||||
"passive": "",
|
||||
"bfd": "",
|
||||
"ipv6_bfd": "",
|
||||
}
|
||||
continue
|
||||
m = _AREA_RE.match(line)
|
||||
if m:
|
||||
proc["area"] = m.group(1).strip()
|
||||
continue
|
||||
m = _SYS_RE.match(line)
|
||||
if m:
|
||||
proc["system_id"] = m.group(1).strip()
|
||||
continue
|
||||
m = _RID_RE.match(line)
|
||||
if m:
|
||||
# prefer IPv4-looking router-id at process level
|
||||
rid = m.group(1).strip()
|
||||
if ":" not in rid or not proc.get("router_id"):
|
||||
proc["router_id"] = rid
|
||||
continue
|
||||
m = _ISTYPE_RE.match(line)
|
||||
if m:
|
||||
proc["is_type"] = m.group(1).lower()
|
||||
continue
|
||||
|
||||
_flush(proc, iface, out, seen)
|
||||
return out
|
||||
|
||||
|
||||
normalize_config_isis.RULE_KEYS = RULE_KEYS
|
||||
140
netx_api/biz_state/parsers/zte/config_l2vpn_pw.py
Normal file
140
netx_api/biz_state/parsers/zte/config_l2vpn_pw.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""ZTE config intent: ``show running-config l2vpn`` / MIM ``!<l2vpn>``.
|
||||
|
||||
One row per ``pseudo-wire`` / ``backup-pw`` under ``vpws`` / ``vpls``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_VPN_RE = re.compile(r'^\s*(vpws|vpls)\s+("?)([^"\s]+)\2\s*$', re.I)
|
||||
_DESC_RE = re.compile(r"^\s*description\s+(.*?)\s*$", re.I)
|
||||
_AP_RE = re.compile(r"^\s*access-point\s+(\S+)\s*$", re.I)
|
||||
_PW_RE = re.compile(r"^\s*pseudo-wire\s+(\S+)", re.I)
|
||||
_BACKUP_PW_RE = re.compile(r"^\s*backup-pw\s+(\S+)", re.I)
|
||||
_NEI_RE = re.compile(
|
||||
r"^\s*neighbou?r\s+(\S+)\s+vcid\s+(\S+)\s*$",
|
||||
re.I,
|
||||
)
|
||||
_ENC_RE = re.compile(r"^\s*encapsulation\s+(\S+)\s*$", re.I)
|
||||
|
||||
|
||||
def _flush_pw(
|
||||
cur_vpn: dict[str, Any] | None,
|
||||
cur_pw: dict[str, Any] | None,
|
||||
out: list[dict[str, Any]],
|
||||
seen: set[tuple[str, str]],
|
||||
) -> None:
|
||||
if not cur_vpn or not cur_pw:
|
||||
return
|
||||
pw = str(cur_pw.get("pw_name") or "")
|
||||
vpn = str(cur_vpn.get("vpn_name") or "")
|
||||
key = (vpn, pw)
|
||||
if not pw or not vpn or key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"vpn_type": str(cur_vpn.get("vpn_type") or "")[:16],
|
||||
"vpn_name": vpn[:128],
|
||||
"pw_name": pw[:128],
|
||||
"peer": str(cur_pw.get("peer") or "")[:64],
|
||||
"vcid": str(cur_pw.get("vcid") or "")[:64],
|
||||
"encapsulation": str(cur_pw.get("encapsulation") or "")[:32],
|
||||
"access_point": str(cur_vpn.get("access_point") or "")[:128],
|
||||
"description": str(cur_vpn.get("description") or "")[:256],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _start_pw(
|
||||
cur_vpn: dict[str, Any] | None,
|
||||
cur_pw: dict[str, Any] | None,
|
||||
out: list[dict[str, Any]],
|
||||
seen: set[tuple[str, str]],
|
||||
pw_name: str,
|
||||
) -> dict[str, Any]:
|
||||
_flush_pw(cur_vpn, cur_pw, out, seen)
|
||||
return {"pw_name": pw_name.strip(), "peer": "", "vcid": "", "encapsulation": ""}
|
||||
|
||||
|
||||
def normalize_config_l2vpn_pw(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, command, params)
|
||||
body = extract_mim_section(raw_text, "l2vpn")
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
cur_vpn: dict[str, Any] | None = None
|
||||
cur_pw: dict[str, Any] | None = None
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _VPN_RE.match(line)
|
||||
if m:
|
||||
_flush_pw(cur_vpn, cur_pw, out, seen)
|
||||
cur_pw = None
|
||||
cur_vpn = {
|
||||
"vpn_type": m.group(1).lower(),
|
||||
"vpn_name": m.group(3).strip(),
|
||||
"description": "",
|
||||
"access_point": "",
|
||||
}
|
||||
continue
|
||||
if not cur_vpn:
|
||||
continue
|
||||
if re.match(r"^\$\s*$", line):
|
||||
_flush_pw(cur_vpn, cur_pw, out, seen)
|
||||
cur_pw = None
|
||||
cur_vpn = None
|
||||
continue
|
||||
m = _DESC_RE.match(line)
|
||||
if m and cur_pw is None:
|
||||
cur_vpn["description"] = (m.group(1) or "").strip().strip('"')[:256]
|
||||
continue
|
||||
m = _AP_RE.match(line)
|
||||
if m:
|
||||
cur_vpn["access_point"] = m.group(1).strip()
|
||||
continue
|
||||
m = _PW_RE.match(line)
|
||||
if m:
|
||||
cur_pw = _start_pw(cur_vpn, cur_pw, out, seen, m.group(1))
|
||||
continue
|
||||
m = _BACKUP_PW_RE.match(line)
|
||||
if m:
|
||||
cur_pw = _start_pw(cur_vpn, cur_pw, out, seen, m.group(1))
|
||||
continue
|
||||
# indented $ closes nested block; flush PW once peer is known so
|
||||
# backup-pw / redundancy neighbours do not overwrite the primary.
|
||||
if re.match(r"^\s+\$\s*$", line):
|
||||
if cur_pw and cur_pw.get("peer"):
|
||||
_flush_pw(cur_vpn, cur_pw, out, seen)
|
||||
cur_pw = None
|
||||
continue
|
||||
if not cur_pw:
|
||||
continue
|
||||
m = _NEI_RE.match(line)
|
||||
if m:
|
||||
cur_pw["peer"] = m.group(1).strip()
|
||||
cur_pw["vcid"] = m.group(2).strip()
|
||||
continue
|
||||
m = _ENC_RE.match(line)
|
||||
if m:
|
||||
cur_pw["encapsulation"] = m.group(1).strip()
|
||||
_flush_pw(cur_vpn, cur_pw, out, seen)
|
||||
return out
|
||||
|
||||
|
||||
normalize_config_l2vpn_pw.RULE_KEYS = RULE_KEYS
|
||||
267
netx_api/biz_state/parsers/zte/config_ospf.py
Normal file
267
netx_api/biz_state/parsers/zte/config_ospf.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
"""ZTE config intent: OSPFv2/v3 from ``!<ospfv2>`` / ``!<ospfv3>``.
|
||||
|
||||
One row per ``(af, process, vrf, area, interface)``. Areas with no interfaces
|
||||
still emit a row with empty ``interface``. Redistribute is process-scoped and
|
||||
applied when the process block closes (it often appears after areas).
|
||||
Never captures digest keys / encrypted auth material.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_V2_RE = re.compile(r"(?i)^\s*router\s+ospf\s+(\S+)(?:\s+vrf\s+(\S+))?\s*$")
|
||||
_V3_RE = re.compile(r"(?i)^\s*ipv6\s+router\s+ospf\s+(\S+)(?:\s+vrf\s+(\S+))?\s*$")
|
||||
_RID_RE = re.compile(r"(?i)^\s*router-id\s+(\S+)\s*$")
|
||||
_AREA_RE = re.compile(r"(?i)^\s*area\s+(\S+)\s*$")
|
||||
_IFACE_RE = re.compile(r"(?i)^\s*interface\s+(\S+)\s*$")
|
||||
_COST_RE = re.compile(r"(?i)^\s*cost\s+(\d+)\s*$")
|
||||
_HELLO_RE = re.compile(r"(?i)^\s*hello-interval\s+(\d+)\s*$")
|
||||
_DEAD_RE = re.compile(r"(?i)^\s*dead-interval\s+(\d+)\s*$")
|
||||
_NET_RE = re.compile(r"(?i)^\s*network\s+(\S+)\s*$")
|
||||
_REDIST_RE = re.compile(r"(?i)^\s*redistribute\s+(\S+)")
|
||||
_DOLLAR_RE = re.compile(r"^(\s*)\$\s*$")
|
||||
|
||||
|
||||
def _indent(spaces: str) -> int:
|
||||
return len(spaces.expandtabs(2))
|
||||
|
||||
|
||||
def _queue_iface(
|
||||
proc: dict[str, Any],
|
||||
area: dict[str, Any],
|
||||
iface: dict[str, Any],
|
||||
) -> None:
|
||||
proc.setdefault("_pending", []).append(
|
||||
{
|
||||
"area": str(area.get("area") or ""),
|
||||
"area_type": str(area.get("area_type") or "normal"),
|
||||
"interface": str(iface.get("interface") or ""),
|
||||
"network_type": str(iface.get("network_type") or ""),
|
||||
"cost": str(iface.get("cost") or ""),
|
||||
"hello_interval": str(iface.get("hello_interval") or ""),
|
||||
"dead_interval": str(iface.get("dead_interval") or ""),
|
||||
"bfd": str(iface.get("bfd") or ""),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _emit_process(
|
||||
proc: dict[str, Any] | None,
|
||||
out: list[dict[str, Any]],
|
||||
seen: set[tuple[str, ...]],
|
||||
) -> None:
|
||||
if not proc:
|
||||
return
|
||||
redist = ",".join(sorted(proc.get("_redistribute") or []))[:256]
|
||||
for item in proc.get("_pending") or []:
|
||||
key = (
|
||||
proc["af"],
|
||||
proc["process_id"],
|
||||
proc["vrf"],
|
||||
item["area"],
|
||||
item["interface"],
|
||||
)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"af": proc["af"][:8],
|
||||
"process_id": str(proc["process_id"])[:32],
|
||||
"vrf": str(proc.get("vrf") or "")[:128],
|
||||
"router_id": str(proc.get("router_id") or "")[:64],
|
||||
"area": item["area"][:64],
|
||||
"area_type": item["area_type"][:32],
|
||||
"interface": item["interface"][:128],
|
||||
"network_type": item["network_type"][:32],
|
||||
"cost": item["cost"][:16],
|
||||
"hello_interval": item["hello_interval"][:16],
|
||||
"dead_interval": item["dead_interval"][:16],
|
||||
"bfd": item["bfd"][:16],
|
||||
"redistribute": redist,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _close_area(proc: dict[str, Any] | None, area: dict[str, Any] | None) -> None:
|
||||
if not proc or not area:
|
||||
return
|
||||
if int(area.get("_iface_count") or 0) == 0:
|
||||
_queue_iface(
|
||||
proc,
|
||||
area,
|
||||
{
|
||||
"interface": "",
|
||||
"network_type": "",
|
||||
"cost": "",
|
||||
"hello_interval": "",
|
||||
"dead_interval": "",
|
||||
"bfd": "",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _parse_ospf_body(body: str, *, af: str) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
proc: dict[str, Any] | None = None
|
||||
area: dict[str, Any] | None = None
|
||||
iface: dict[str, Any] | None = None
|
||||
head_re = _V3_RE if af == "ipv6" else _V2_RE
|
||||
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = head_re.match(line)
|
||||
if m:
|
||||
if iface and proc and area:
|
||||
_queue_iface(proc, area, iface)
|
||||
_close_area(proc, area)
|
||||
_emit_process(proc, out, seen)
|
||||
iface = None
|
||||
area = None
|
||||
proc = {
|
||||
"af": af,
|
||||
"process_id": m.group(1).strip(),
|
||||
"vrf": (m.group(2) or "").strip(),
|
||||
"router_id": "",
|
||||
"_redistribute": set(),
|
||||
"_pending": [],
|
||||
}
|
||||
continue
|
||||
if not proc:
|
||||
continue
|
||||
|
||||
m = _DOLLAR_RE.match(line)
|
||||
if m:
|
||||
ind = _indent(m.group(1))
|
||||
if iface is not None and ind >= 4:
|
||||
_queue_iface(proc, area, iface) # type: ignore[arg-type]
|
||||
iface = None
|
||||
elif area is not None and ind >= 2:
|
||||
if iface and area:
|
||||
_queue_iface(proc, area, iface)
|
||||
iface = None
|
||||
_close_area(proc, area)
|
||||
area = None
|
||||
else:
|
||||
if iface and area:
|
||||
_queue_iface(proc, area, iface)
|
||||
iface = None
|
||||
_close_area(proc, area)
|
||||
area = None
|
||||
_emit_process(proc, out, seen)
|
||||
proc = None
|
||||
continue
|
||||
|
||||
if iface is not None:
|
||||
m = _COST_RE.match(line)
|
||||
if m:
|
||||
iface["cost"] = m.group(1)
|
||||
continue
|
||||
m = _HELLO_RE.match(line)
|
||||
if m:
|
||||
iface["hello_interval"] = m.group(1)
|
||||
continue
|
||||
m = _DEAD_RE.match(line)
|
||||
if m:
|
||||
iface["dead_interval"] = m.group(1)
|
||||
continue
|
||||
m = _NET_RE.match(line)
|
||||
if m:
|
||||
iface["network_type"] = m.group(1).lower()
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low == "bfd" or low.startswith("bfd interval") or low.startswith("bfd enable"):
|
||||
iface["bfd"] = "enable"
|
||||
continue
|
||||
if low == "bfd disable":
|
||||
iface["bfd"] = "disable"
|
||||
continue
|
||||
continue
|
||||
|
||||
m = _AREA_RE.match(line)
|
||||
if m:
|
||||
if iface and area:
|
||||
_queue_iface(proc, area, iface)
|
||||
iface = None
|
||||
_close_area(proc, area)
|
||||
area = {
|
||||
"area": m.group(1).strip(),
|
||||
"area_type": "normal",
|
||||
"_iface_count": 0,
|
||||
}
|
||||
continue
|
||||
|
||||
if area is not None:
|
||||
m = _IFACE_RE.match(line)
|
||||
if m:
|
||||
if iface:
|
||||
_queue_iface(proc, area, iface)
|
||||
iface = {
|
||||
"interface": m.group(1).strip(),
|
||||
"network_type": "",
|
||||
"cost": "",
|
||||
"hello_interval": "",
|
||||
"dead_interval": "",
|
||||
"bfd": "",
|
||||
}
|
||||
area["_iface_count"] = int(area.get("_iface_count") or 0) + 1
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low.startswith("stub"):
|
||||
area["area_type"] = "stub"
|
||||
continue
|
||||
if low.startswith("nssa"):
|
||||
area["area_type"] = "nssa"
|
||||
continue
|
||||
|
||||
# process-level (also after areas closed)
|
||||
if iface is None:
|
||||
m = _RID_RE.match(line)
|
||||
if m:
|
||||
proc["router_id"] = m.group(1).strip()
|
||||
continue
|
||||
m = _REDIST_RE.match(line)
|
||||
if m:
|
||||
proc["_redistribute"].add(m.group(1).lower())
|
||||
continue
|
||||
|
||||
if iface and proc and area:
|
||||
_queue_iface(proc, area, iface)
|
||||
_close_area(proc, area)
|
||||
_emit_process(proc, out, seen)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_config_ospf(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, params)
|
||||
cmd = str(command or "").lower()
|
||||
text = str(raw_text or "")
|
||||
if "ospfv3" in cmd:
|
||||
return _parse_ospf_body(extract_mim_section(text, "ospfv3"), af="ipv6")
|
||||
if "ospfv2" in cmd or re.search(r"(?i)running-config\s+ospf(?:v2)?\b", cmd):
|
||||
return _parse_ospf_body(extract_mim_section(text, "ospfv2"), af="ipv4")
|
||||
if re.search(r"(?i)^!<ospfv3>\s*$", text, re.M) and not re.search(
|
||||
r"(?i)^!<ospfv2>\s*$", text, re.M
|
||||
):
|
||||
return _parse_ospf_body(extract_mim_section(text, "ospfv3"), af="ipv6")
|
||||
return _parse_ospf_body(extract_mim_section(text, "ospfv2"), af="ipv4")
|
||||
|
||||
|
||||
normalize_config_ospf.RULE_KEYS = RULE_KEYS
|
||||
283
netx_api/biz_state/parsers/zte/config_static_route.py
Normal file
283
netx_api/biz_state/parsers/zte/config_static_route.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
"""ZTE config intent: static routes from ``!<static>`` / ``!<ipv6-static-route>``.
|
||||
|
||||
Commands:
|
||||
``show running-config static``
|
||||
``show running-config ipv6-static-route``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_V4_HEAD = re.compile(
|
||||
r"(?i)^\s*ip\s+route(?:\s+vrf\s+(?P<vrf>\S+))?\s+"
|
||||
r"(?P<prefix>\d{1,3}(?:\.\d{1,3}){3})\s+"
|
||||
r"(?P<mask>\d{1,3}(?:\.\d{1,3}){3})\s+"
|
||||
r"(?P<rest>.*)$"
|
||||
)
|
||||
_V6_HEAD = re.compile(
|
||||
r"(?i)^\s*ipv6\s+route(?:\s+vrf\s+(?P<vrf>\S+))?\s+"
|
||||
r"(?P<prefix>\S+)\s+"
|
||||
r"(?P<rest>.*)$"
|
||||
)
|
||||
|
||||
_IPV4_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$")
|
||||
_IFACE_RE = re.compile(
|
||||
r"(?i)^(null\d*|gei-\S+|xgei-\S+|cgei-\S+|bvi\S*|vlan\S*|smartgroup\S*|"
|
||||
r"loopback\S*|mgmt\S*|pos\S*|eth\S*|tunnel\S*|irb\S*|ve\S*)$"
|
||||
)
|
||||
_KW = frozenset(
|
||||
{
|
||||
"name",
|
||||
"track",
|
||||
"tag",
|
||||
"metric",
|
||||
"distance",
|
||||
"preference",
|
||||
"nexthop-vrf",
|
||||
"bfd",
|
||||
"enable",
|
||||
"disable",
|
||||
"permanent",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_ipv4(tok: str) -> bool:
|
||||
return bool(_IPV4_RE.match(tok or ""))
|
||||
|
||||
|
||||
def _is_iface(tok: str) -> bool:
|
||||
return bool(_IFACE_RE.match(tok or ""))
|
||||
|
||||
|
||||
def _take_kw(tokens: list[str], i: int, row: dict[str, str]) -> int:
|
||||
"""Consume optional keyword/value pairs starting at ``i``; return new index."""
|
||||
n = len(tokens)
|
||||
while i < n:
|
||||
low = tokens[i].lower()
|
||||
if low in ("bfd",):
|
||||
# bfd [enable|disable]
|
||||
row["bfd"] = "enable"
|
||||
i += 1
|
||||
if i < n and tokens[i].lower() in ("enable", "disable"):
|
||||
row["bfd"] = tokens[i].lower()
|
||||
i += 1
|
||||
continue
|
||||
if low in ("enable", "disable", "permanent"):
|
||||
if low == "permanent":
|
||||
row["permanent"] = "yes"
|
||||
i += 1
|
||||
continue
|
||||
if low in ("name", "track", "tag", "metric", "distance", "preference", "nexthop-vrf"):
|
||||
if i + 1 >= n:
|
||||
break
|
||||
val = tokens[i + 1]
|
||||
if low == "nexthop-vrf":
|
||||
row["nexthop_vrf"] = val
|
||||
elif low == "name":
|
||||
row["route_name"] = val
|
||||
else:
|
||||
row[low] = val
|
||||
i += 2
|
||||
continue
|
||||
break
|
||||
return i
|
||||
|
||||
|
||||
def _parse_v4_rest(rest: str) -> dict[str, str]:
|
||||
tokens = (rest or "").split()
|
||||
row: dict[str, str] = {
|
||||
"next_hop": "",
|
||||
"interface": "",
|
||||
"nexthop_vrf": "",
|
||||
"metric": "",
|
||||
"track": "",
|
||||
"tag": "",
|
||||
"route_name": "",
|
||||
"bfd": "",
|
||||
"distance": "",
|
||||
}
|
||||
if not tokens:
|
||||
return row
|
||||
i = 0
|
||||
# nexthop-vrf without explicit IP
|
||||
if tokens[0].lower() == "nexthop-vrf" and len(tokens) >= 2:
|
||||
row["nexthop_vrf"] = tokens[1]
|
||||
_take_kw(tokens, 2, row)
|
||||
return row
|
||||
if _is_iface(tokens[0]):
|
||||
row["interface"] = tokens[0]
|
||||
i = 1
|
||||
if i < len(tokens) and _is_ipv4(tokens[i]) and tokens[i].lower() not in _KW:
|
||||
row["next_hop"] = tokens[i]
|
||||
i += 1
|
||||
elif _is_ipv4(tokens[0]):
|
||||
row["next_hop"] = tokens[0]
|
||||
i = 1
|
||||
_take_kw(tokens, i, row)
|
||||
return row
|
||||
|
||||
|
||||
def _parse_v6_rest(rest: str) -> dict[str, str]:
|
||||
tokens = (rest or "").split()
|
||||
row: dict[str, str] = {
|
||||
"next_hop": "",
|
||||
"interface": "",
|
||||
"nexthop_vrf": "",
|
||||
"metric": "",
|
||||
"track": "",
|
||||
"tag": "",
|
||||
"route_name": "",
|
||||
"bfd": "",
|
||||
"distance": "",
|
||||
}
|
||||
if not tokens:
|
||||
return row
|
||||
i = 0
|
||||
if tokens[0].lower() == "nexthop-vrf" and len(tokens) >= 2:
|
||||
row["nexthop_vrf"] = tokens[1]
|
||||
_take_kw(tokens, 2, row)
|
||||
return row
|
||||
if _is_iface(tokens[0]):
|
||||
row["interface"] = tokens[0]
|
||||
i = 1
|
||||
if i < len(tokens) and ":" in tokens[i] and tokens[i].lower() not in _KW:
|
||||
row["next_hop"] = tokens[i]
|
||||
i += 1
|
||||
elif ":" in tokens[0]:
|
||||
row["next_hop"] = tokens[0]
|
||||
i = 1
|
||||
_take_kw(tokens, i, row)
|
||||
return row
|
||||
|
||||
|
||||
def _row(
|
||||
*,
|
||||
af: str,
|
||||
vrf: str,
|
||||
prefix: str,
|
||||
mask: str,
|
||||
extra: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"af": af[:8],
|
||||
"vrf": (vrf or "")[:128],
|
||||
"prefix": prefix[:128],
|
||||
"mask": (mask or "")[:64],
|
||||
"next_hop": (extra.get("next_hop") or "")[:64],
|
||||
"interface": (extra.get("interface") or "")[:128],
|
||||
"nexthop_vrf": (extra.get("nexthop_vrf") or "")[:128],
|
||||
"metric": (extra.get("metric") or "")[:16],
|
||||
"bfd": (extra.get("bfd") or "")[:16],
|
||||
"track": (extra.get("track") or "")[:64],
|
||||
"route_name": (extra.get("route_name") or "")[:128],
|
||||
"tag": (extra.get("tag") or "")[:32],
|
||||
"distance": (extra.get("distance") or extra.get("preference") or "")[:16],
|
||||
}
|
||||
|
||||
|
||||
def _parse_v4(body: str) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _V4_HEAD.match(line)
|
||||
if not m:
|
||||
continue
|
||||
extra = _parse_v4_rest(m.group("rest") or "")
|
||||
row = _row(
|
||||
af="ipv4",
|
||||
vrf=(m.group("vrf") or "").strip(),
|
||||
prefix=m.group("prefix").strip(),
|
||||
mask=m.group("mask").strip(),
|
||||
extra=extra,
|
||||
)
|
||||
key = (
|
||||
row["vrf"],
|
||||
row["prefix"],
|
||||
row["mask"],
|
||||
row["next_hop"],
|
||||
row["interface"],
|
||||
row["nexthop_vrf"],
|
||||
)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def _parse_v6(body: str) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _V6_HEAD.match(line)
|
||||
if not m:
|
||||
continue
|
||||
extra = _parse_v6_rest(m.group("rest") or "")
|
||||
row = _row(
|
||||
af="ipv6",
|
||||
vrf=(m.group("vrf") or "").strip(),
|
||||
prefix=m.group("prefix").strip(),
|
||||
mask="",
|
||||
extra=extra,
|
||||
)
|
||||
key = (
|
||||
row["vrf"],
|
||||
row["prefix"],
|
||||
row["next_hop"],
|
||||
row["interface"],
|
||||
row["nexthop_vrf"],
|
||||
)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def _detect_af(command: str, raw_text: str) -> str:
|
||||
cmd = str(command or "").lower()
|
||||
if "ipv6-static" in cmd or "ipv6 static" in cmd:
|
||||
return "ipv6"
|
||||
if re.search(r"(?i)^\s*show\s+running-config\s+static\b", cmd):
|
||||
return "ipv4"
|
||||
# Full dump / ambiguous: prefer section presence
|
||||
text = str(raw_text or "")
|
||||
if re.search(r"(?i)^!<ipv6-static-route>\s*$", text, re.M) and not re.search(
|
||||
r"(?i)^!<static>\s*$", text, re.M
|
||||
):
|
||||
return "ipv6"
|
||||
return "ipv4"
|
||||
|
||||
|
||||
def normalize_config_static_route(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, params)
|
||||
af = _detect_af(command, raw_text)
|
||||
if af == "ipv6":
|
||||
body = extract_mim_section(raw_text, "ipv6-static-route")
|
||||
return _parse_v6(body)
|
||||
body = extract_mim_section(raw_text, "static")
|
||||
return _parse_v4(body)
|
||||
|
||||
|
||||
normalize_config_static_route.RULE_KEYS = RULE_KEYS
|
||||
107
netx_api/biz_state/parsers/zte/config_vrf.py
Normal file
107
netx_api/biz_state/parsers/zte/config_vrf.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""ZTE config intent: ``show running-config vrf`` / MIM ``!<vrf>``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from .config_common import extract_mim_section, is_secret_line
|
||||
|
||||
RULE_KEYS: tuple[str, ...] = ()
|
||||
|
||||
_VRF_RE = re.compile(r"^\s*ip\s+vrf\s+(\S+)\s*$", re.I)
|
||||
_RD_RE = re.compile(r"^\s*rd\s+(\S+)\s*$", re.I)
|
||||
_DESC_RE = re.compile(r'^\s*description\s+("?)(.*?)\1\s*$', re.I)
|
||||
_AF_RE = re.compile(r"^\s*address-family\s+(ipv4|ipv6)\s*$", re.I)
|
||||
_RT_RE = re.compile(r"^\s*route-target\s+(export|import)\s+(\S+)\s*$", re.I)
|
||||
|
||||
|
||||
def _flush(cur: dict[str, Any] | None, out: list[dict[str, Any]], seen: set[str]) -> None:
|
||||
if not cur:
|
||||
return
|
||||
name = str(cur.get("vrf_name") or "")
|
||||
if not name or name in seen:
|
||||
return
|
||||
seen.add(name)
|
||||
afs = sorted(cur.get("_afs") or [])
|
||||
exports = sorted(cur.get("_rt_export") or [])
|
||||
imports = sorted(cur.get("_rt_import") or [])
|
||||
out.append(
|
||||
{
|
||||
"vrf_name": name[:128],
|
||||
"rd": str(cur.get("rd") or "")[:64],
|
||||
"description": str(cur.get("description") or "")[:256],
|
||||
"address_families": ",".join(afs)[:64],
|
||||
"rt_export": ",".join(exports)[:512],
|
||||
"rt_import": ",".join(imports)[:512],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def normalize_config_vrf(
|
||||
*,
|
||||
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]]:
|
||||
_ = (fsm_tables, vendor, device_type, command, params)
|
||||
body = extract_mim_section(raw_text, "vrf")
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
cur: dict[str, Any] | None = None
|
||||
for raw in body.splitlines():
|
||||
line = raw.rstrip()
|
||||
if is_secret_line(line):
|
||||
continue
|
||||
m = _VRF_RE.match(line)
|
||||
if m:
|
||||
_flush(cur, out, seen)
|
||||
cur = {
|
||||
"vrf_name": m.group(1).strip(),
|
||||
"rd": "",
|
||||
"description": "",
|
||||
"_afs": set(),
|
||||
"_rt_export": set(),
|
||||
"_rt_import": set(),
|
||||
}
|
||||
continue
|
||||
if not cur:
|
||||
continue
|
||||
if line.strip() == "$" and not line.startswith(" "):
|
||||
# top-level close for vrf block (indent 0 $)
|
||||
if not line.startswith(" ") and not line.startswith("\t"):
|
||||
_flush(cur, out, seen)
|
||||
cur = None
|
||||
continue
|
||||
# ZTE uses bare `$` at column 0 to end blocks; nested `$` are indented
|
||||
if re.match(r"^\$\s*$", line):
|
||||
_flush(cur, out, seen)
|
||||
cur = None
|
||||
continue
|
||||
m = _RD_RE.match(line)
|
||||
if m:
|
||||
cur["rd"] = m.group(1).strip()
|
||||
continue
|
||||
m = _DESC_RE.match(line)
|
||||
if m:
|
||||
cur["description"] = (m.group(2) or "").strip()[:256]
|
||||
continue
|
||||
m = _AF_RE.match(line)
|
||||
if m:
|
||||
cur["_afs"].add(m.group(1).lower())
|
||||
continue
|
||||
m = _RT_RE.match(line)
|
||||
if m:
|
||||
direction, value = m.group(1).lower(), m.group(2).strip()
|
||||
if direction == "export":
|
||||
cur["_rt_export"].add(value)
|
||||
else:
|
||||
cur["_rt_import"].add(value)
|
||||
_flush(cur, out, seen)
|
||||
return out
|
||||
|
||||
|
||||
normalize_config_vrf.RULE_KEYS = RULE_KEYS
|
||||
119
netx_api/biz_state/parsers/zte/evpn_mac.py
Normal file
119
netx_api/biz_state/parsers/zte/evpn_mac.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""ZTE: show bgp evpn mac."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_bgp_evpn_mac",)
|
||||
|
||||
_ROUTE_RE = re.compile(
|
||||
r"^\s*(?P<flags>[*>isd]*)\s*"
|
||||
r"(?P<net>\S+)\s+(?P<nh>\S+)\s+(?P<rest>.*)$"
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
net = row_get(r, "NETWORK", "network")
|
||||
if not net or net.lower() == "network" or net in seen:
|
||||
continue
|
||||
seen.add(net)
|
||||
out.append(
|
||||
{
|
||||
"network": net[:256],
|
||||
"next_hop": row_get(r, "NEXT_HOP", "next_hop")[:128],
|
||||
"metric": row_get(r, "METRIC", "metric")[:32],
|
||||
"loc_prf": row_get(r, "LOC_PRF", "loc_prf")[:32],
|
||||
"rt_prf": row_get(r, "RT_PRF", "rt_prf")[:32],
|
||||
"path": row_get(r, "PATH", "path")[:256],
|
||||
"status_codes": "",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
low = line.strip().lower()
|
||||
if low.startswith("network") or low.startswith("status") or low.startswith("origin"):
|
||||
continue
|
||||
if low.startswith("route distinguisher"):
|
||||
continue
|
||||
m = _ROUTE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
net = m.group("net")
|
||||
if "/" not in net and ":" not in net and not re.search(r"[0-9a-fA-F]{4}\.", net):
|
||||
# skip non-NLRI-looking tokens
|
||||
if not re.match(r"^\[", net):
|
||||
continue
|
||||
if net in seen:
|
||||
continue
|
||||
seen.add(net)
|
||||
rest = m.group("rest").split()
|
||||
metric = loc = rt = ""
|
||||
path_parts: list[str] = []
|
||||
for p in rest:
|
||||
if not path_parts and re.fullmatch(r"\d+", p):
|
||||
if not metric:
|
||||
metric = p
|
||||
elif not loc:
|
||||
loc = p
|
||||
elif not rt:
|
||||
rt = p
|
||||
else:
|
||||
path_parts.append(p)
|
||||
else:
|
||||
path_parts.append(p)
|
||||
out.append(
|
||||
{
|
||||
"network": net[:256],
|
||||
"next_hop": m.group("nh")[:128],
|
||||
"metric": metric[:32],
|
||||
"loc_prf": loc[:32],
|
||||
"rt_prf": rt[:32],
|
||||
"path": " ".join(path_parts)[:256],
|
||||
"status_codes": (m.group("flags") or "").strip()[:16],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_evpn_mac(
|
||||
*,
|
||||
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 bgp evpn mac").strip()
|
||||
if platform and cmd:
|
||||
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)
|
||||
|
||||
|
||||
normalize_evpn_mac.RULE_KEYS = RULE_KEYS
|
||||
195
netx_api/biz_state/parsers/zte/interface_detail.py
Normal file
195
netx_api/biz_state/parsers/zte/interface_detail.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"""ZTE: filtered ``show interface | include …`` (rate / util / BW)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_interface",)
|
||||
|
||||
_IF_HDR_RE = re.compile(
|
||||
r"^(?P<iface>\S+)\s+is\s+(?P<state>administratively\s+down|up|down)"
|
||||
r"(?:,\s*ifindex:\s*(?P<ifindex>\d+))?\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)
|
||||
_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)
|
||||
_NEG_RE = re.compile(r"^\s*Negotiation\s+(?P<neg>\S+)", re.I)
|
||||
_PERIOD_RE = re.compile(r"^\s*Rate\s+period\s*:\s*(?P<p>\d+)", re.I)
|
||||
_IN_RE = re.compile(r"^\s*Input\s*:\s*(?P<bps>[\d.]+)\s*bit/s", re.I)
|
||||
_OUT_RE = re.compile(r"^\s*Output\s*:\s*(?P<bps>[\d.]+)\s*bit/s", re.I)
|
||||
_UTIL_RE = re.compile(
|
||||
r"^\s*Intf\s+utilization:\s*input\s*(?P<inu>[\d.]+)%\s+output\s*(?P<outu>[\d.]+)%",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def _empty_row(iface: str) -> dict[str, Any]:
|
||||
return {
|
||||
"interface": iface[:128],
|
||||
"admin": "",
|
||||
"ifindex": "",
|
||||
"description": "",
|
||||
"port_media": "",
|
||||
"negotiation": "",
|
||||
"bw": "",
|
||||
"mtu": "",
|
||||
"rate_period": "",
|
||||
"input_bps": "",
|
||||
"output_bps": "",
|
||||
"in_util": "",
|
||||
"out_util": "",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_admin(state: str) -> str:
|
||||
s = str(state or "").strip().lower()
|
||||
# Guard against FSM greedily eating ", ifindex:"
|
||||
if "," in s:
|
||||
s = s.split(",", 1)[0].strip()
|
||||
if "administratively" in s:
|
||||
return "admin-down"
|
||||
if s == "up":
|
||||
return "up"
|
||||
if s == "down":
|
||||
return "down"
|
||||
return s[:32]
|
||||
|
||||
|
||||
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:
|
||||
iface = row_get(r, "IFNAME", "interface")
|
||||
in_util = row_get(r, "IN_UTIL", "in_util")
|
||||
# Incomplete FSM rows (no util yet) are filldown noise — skip
|
||||
if not iface or not in_util or iface in seen:
|
||||
continue
|
||||
seen.add(iface)
|
||||
admin_raw = row_get(r, "ADMIN_STATE", "ADMIN_OPER", "admin")
|
||||
out.append(
|
||||
{
|
||||
"interface": iface[:128],
|
||||
"admin": _normalize_admin(admin_raw)[:32],
|
||||
"ifindex": row_get(r, "IFINDEX", "ifindex")[:32],
|
||||
"description": row_get(r, "DESCRIPTION", "description")[:256],
|
||||
"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],
|
||||
"mtu": row_get(r, "MTU", "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],
|
||||
"in_util": in_util[:16],
|
||||
"out_util": row_get(r, "OUT_UTIL", "out_util")[:16],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
cur: dict[str, Any] | None = None
|
||||
in_rates = False
|
||||
saw_input = False
|
||||
|
||||
def _flush() -> None:
|
||||
nonlocal cur
|
||||
if cur and cur.get("interface"):
|
||||
out.append(cur)
|
||||
cur = None
|
||||
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
m = _IF_HDR_RE.match(line.strip())
|
||||
if m:
|
||||
_flush()
|
||||
cur = _empty_row(m.group("iface"))
|
||||
cur["admin"] = _normalize_admin(m.group("state"))
|
||||
cur["ifindex"] = (m.group("ifindex") or "")[:32]
|
||||
in_rates = False
|
||||
saw_input = False
|
||||
continue
|
||||
if not cur:
|
||||
continue
|
||||
m = _DESC_RE.match(line)
|
||||
if m:
|
||||
cur["description"] = (m.group("desc") or "").strip()[:256]
|
||||
continue
|
||||
m = _MEDIA_RE.match(line)
|
||||
if m:
|
||||
cur["port_media"] = m.group("media")[:32]
|
||||
continue
|
||||
m = _NEG_RE.match(line)
|
||||
if m:
|
||||
cur["negotiation"] = m.group("neg")[:32]
|
||||
continue
|
||||
m = _BW_RE.match(line)
|
||||
if m:
|
||||
cur["bw"] = m.group("bw").strip()[:64]
|
||||
continue
|
||||
m = _MTU_RE.match(line)
|
||||
if m:
|
||||
cur["mtu"] = m.group("mtu")[:16]
|
||||
continue
|
||||
m = _PERIOD_RE.match(line)
|
||||
if m:
|
||||
cur["rate_period"] = m.group("p")[:16]
|
||||
in_rates = True
|
||||
saw_input = False
|
||||
continue
|
||||
if in_rates and "Peak rate" in line:
|
||||
continue
|
||||
m = _IN_RE.match(line)
|
||||
if m and in_rates and not saw_input:
|
||||
cur["input_bps"] = m.group("bps")[:32]
|
||||
saw_input = True
|
||||
continue
|
||||
m = _OUT_RE.match(line)
|
||||
if m and in_rates and saw_input and not cur.get("output_bps"):
|
||||
cur["output_bps"] = m.group("bps")[:32]
|
||||
continue
|
||||
m = _UTIL_RE.match(line)
|
||||
if m:
|
||||
cur["in_util"] = m.group("inu")[:16]
|
||||
cur["out_util"] = m.group("outu")[:16]
|
||||
in_rates = False
|
||||
continue
|
||||
_flush()
|
||||
return out
|
||||
|
||||
|
||||
def normalize_interface_detail(
|
||||
*,
|
||||
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").strip()
|
||||
if platform and cmd:
|
||||
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)
|
||||
|
||||
|
||||
normalize_interface_detail.RULE_KEYS = RULE_KEYS
|
||||
107
netx_api/biz_state/parsers/zte/ip_route.py
Normal file
107
netx_api/biz_state/parsers/zte/ip_route.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""ZTE: show ip forwarding route [vrf <vrf>]."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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
|
||||
from .bgp_peer import _detect_vrf
|
||||
|
||||
RULE_KEYS = ("zte_zxros_show_ip_forwarding_route",)
|
||||
|
||||
_ROUTE_RE = re.compile(
|
||||
r"^\s*(?P<flags>[*>R]*)\s*"
|
||||
r"(?P<dest>\d{1,3}(?:\.\d{1,3}){3}/\d+)\s+"
|
||||
r"(?P<gw>\S+)\s+(?P<iface>\S+)\s+(?P<owner>\S+)\s+"
|
||||
r"(?P<pri>\S+)\s+(?P<metric>\S+)\s*$"
|
||||
)
|
||||
|
||||
|
||||
def _map_fsm_rows(rows: list[dict[str, Any]], *, vrf: str) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for r in rows:
|
||||
dest = row_get(r, "DEST", "dest")
|
||||
if not dest or dest.lower() == "dest" or dest in seen:
|
||||
continue
|
||||
seen.add(dest)
|
||||
out.append(
|
||||
{
|
||||
"vrf": vrf[:128],
|
||||
"dest": dest[:64],
|
||||
"gateway": row_get(r, "GATEWAY", "gateway")[:64],
|
||||
"interface": row_get(r, "INTERFACE", "interface")[:128],
|
||||
"owner": row_get(r, "OWNER", "owner")[:64],
|
||||
"pri": row_get(r, "PRI", "pri")[:16],
|
||||
"metric": row_get(r, "METRIC", "metric")[:32],
|
||||
"flags": row_get(r, "FLAGS", "flags")[:16],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, vrf: str = "", **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line.strip() or line.strip().lower().startswith("dest"):
|
||||
continue
|
||||
m = _ROUTE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
dest = m.group("dest")
|
||||
if dest in seen:
|
||||
continue
|
||||
seen.add(dest)
|
||||
out.append(
|
||||
{
|
||||
"vrf": vrf[:128],
|
||||
"dest": dest[:64],
|
||||
"gateway": m.group("gw")[:64],
|
||||
"interface": m.group("iface")[:128],
|
||||
"owner": m.group("owner")[:64],
|
||||
"pri": m.group("pri")[:16],
|
||||
"metric": m.group("metric")[:32],
|
||||
"flags": (m.group("flags") or "").strip()[:16],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_ip_route(
|
||||
*,
|
||||
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]]:
|
||||
vrf = _detect_vrf(command, 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 ip forwarding route").strip()
|
||||
if platform and cmd:
|
||||
tables = apply_rules(
|
||||
platform=platform, text=raw_text, rule_keys=RULE_KEYS, command=cmd
|
||||
)
|
||||
|
||||
def _map(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return _map_fsm_rows(rows, vrf=vrf)
|
||||
|
||||
def _hand(*, raw_text: str, **kw: Any) -> list[dict[str, Any]]:
|
||||
return _hand_parse(raw_text=raw_text, vrf=vrf, **kw)
|
||||
|
||||
return prefer_fsm(tables, RULE_KEYS, _map, _hand, raw_text=raw_text)
|
||||
|
||||
|
||||
normalize_ip_route.RULE_KEYS = RULE_KEYS
|
||||
128
netx_api/biz_state/parsers/zte/ipv6_route.py
Normal file
128
netx_api/biz_state/parsers/zte/ipv6_route.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""ZTE: show ipv6 forwarding route [vrf <vrf>]."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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
|
||||
from .bgp_peer import _detect_vrf
|
||||
|
||||
RULE_KEYS = ("zte_zxros_show_ipv6_forwarding_route",)
|
||||
|
||||
_DEST_RE = re.compile(
|
||||
r"^\s*(?P<dest>[0-9A-Fa-f:]+/\d+)\s+(?P<owner>\S+)\s+(?P<pri>\S+)\s+"
|
||||
r"(?P<metric>\S+)(?:\s+(?P<flags>\S+))?\s*$"
|
||||
)
|
||||
_NH_RE = re.compile(r"^\s+(?P<gw>[0-9A-Fa-f:]+|\S+),(?P<iface>\S+)\s*$")
|
||||
|
||||
|
||||
def _map_fsm_rows(rows: list[dict[str, Any]], *, vrf: str) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
cur: dict[str, Any] | None = None
|
||||
for r in rows:
|
||||
dest = row_get(r, "DEST", "dest")
|
||||
gw = row_get(r, "GATEWAY", "gateway")
|
||||
iface = row_get(r, "INTERFACE", "interface")
|
||||
if dest:
|
||||
cur = {
|
||||
"vrf": vrf[:128],
|
||||
"dest": dest[:128],
|
||||
"gateway": "",
|
||||
"interface": "",
|
||||
"owner": row_get(r, "OWNER", "owner")[:64],
|
||||
"pri": row_get(r, "PRI", "pri")[:16],
|
||||
"metric": row_get(r, "METRIC", "metric")[:32],
|
||||
"flags": row_get(r, "FLAGS", "flags")[:16],
|
||||
}
|
||||
if gw or iface:
|
||||
cur["gateway"] = gw[:128]
|
||||
cur["interface"] = iface[:128]
|
||||
key = (dest, gw)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append(cur)
|
||||
cur = None
|
||||
continue
|
||||
if cur and (gw or iface):
|
||||
cur["gateway"] = gw[:128]
|
||||
cur["interface"] = iface[:128]
|
||||
key = (cur["dest"], gw)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append(dict(cur))
|
||||
cur = None
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, vrf: str = "", **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
pending: dict[str, Any] | None = None
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
m = _DEST_RE.match(line)
|
||||
if m:
|
||||
pending = {
|
||||
"vrf": vrf[:128],
|
||||
"dest": m.group("dest")[:128],
|
||||
"gateway": "",
|
||||
"interface": "",
|
||||
"owner": m.group("owner")[:64],
|
||||
"pri": m.group("pri")[:16],
|
||||
"metric": m.group("metric")[:32],
|
||||
"flags": (m.group("flags") or "")[:16],
|
||||
}
|
||||
continue
|
||||
m2 = _NH_RE.match(line)
|
||||
if m2 and pending:
|
||||
gw = m2.group("gw")
|
||||
key = (pending["dest"], gw)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
row = dict(pending)
|
||||
row["gateway"] = gw[:128]
|
||||
row["interface"] = m2.group("iface")[:128]
|
||||
out.append(row)
|
||||
pending = None
|
||||
return out
|
||||
|
||||
|
||||
def normalize_ipv6_route(
|
||||
*,
|
||||
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]]:
|
||||
vrf = _detect_vrf(command, 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 ipv6 forwarding route").strip()
|
||||
if platform and cmd:
|
||||
tables = apply_rules(
|
||||
platform=platform, text=raw_text, rule_keys=RULE_KEYS, command=cmd
|
||||
)
|
||||
|
||||
def _map(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return _map_fsm_rows(rows, vrf=vrf)
|
||||
|
||||
def _hand(*, raw_text: str, **kw: Any) -> list[dict[str, Any]]:
|
||||
return _hand_parse(raw_text=raw_text, vrf=vrf, **kw)
|
||||
|
||||
return prefer_fsm(tables, RULE_KEYS, _map, _hand, raw_text=raw_text)
|
||||
|
||||
|
||||
normalize_ipv6_route.RULE_KEYS = RULE_KEYS
|
||||
95
netx_api/biz_state/parsers/zte/l2vpn_mac.py
Normal file
95
netx_api/biz_state/parsers/zte/l2vpn_mac.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""ZTE: show mac l2vpn."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_mac_l2vpn",)
|
||||
|
||||
_MAC_RE = re.compile(
|
||||
r"^(?P<mac>[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4})\s+"
|
||||
r"(?P<vpn>\S+)\s+(?P<vlan>\S+)\s+(?P<out>.+?)\s+(?P<attr>\S+)\s*$"
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
mac = row_get(r, "MAC", "mac")
|
||||
vpn = row_get(r, "VPN", "vpn")
|
||||
if not mac or mac.lower() == "mac":
|
||||
continue
|
||||
key = (mac, vpn)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"mac": mac[:64],
|
||||
"vpn": vpn[:128],
|
||||
"vlan": row_get(r, "VLAN", "vlan")[:32],
|
||||
"outgoing": row_get(r, "OUTGOING", "outgoing")[:256],
|
||||
"attribute": row_get(r, "ATTRIBUTE", "attribute")[:64],
|
||||
}
|
||||
)
|
||||
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()
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.lower().startswith("mac "):
|
||||
continue
|
||||
m = _MAC_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
key = (m.group("mac"), m.group("vpn"))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"mac": m.group("mac")[:64],
|
||||
"vpn": m.group("vpn")[:128],
|
||||
"vlan": m.group("vlan")[:32],
|
||||
"outgoing": m.group("out").strip()[:256],
|
||||
"attribute": m.group("attr")[:64],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_l2vpn_mac(
|
||||
*,
|
||||
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 mac l2vpn").strip()
|
||||
if platform and cmd:
|
||||
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)
|
||||
|
||||
|
||||
normalize_l2vpn_mac.RULE_KEYS = RULE_KEYS
|
||||
106
netx_api/biz_state/parsers/zte/l2vpn_pw.py
Normal file
106
netx_api/biz_state/parsers/zte/l2vpn_pw.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""ZTE: show l2vpn forwardinfo (PW state)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_l2vpn_forwardinfo",)
|
||||
|
||||
_PW_RE = re.compile(
|
||||
r"^(?P<name>\S+)\s+(?P<peer>\S+)\s+(?P<fec>\S+)\s+(?P<pwt>\S+)\s+"
|
||||
r"(?:(?P<mode>[A-Za-z$]+)\s+)?"
|
||||
r"(?P<state>UP|DOWN)\s+(?P<ll>\S+)\s+(?P<rl>\S+)\s+(?P<owner>\S+)\s*$",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
name = row_get(r, "PW_NAME", "pw_name")
|
||||
if not name or name.lower() == "pwname" or name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
state = row_get(r, "STATE", "state")
|
||||
# FSM may glue mode into pw_type
|
||||
pwt = row_get(r, "PW_TYPE", "pw_type")
|
||||
out.append(
|
||||
{
|
||||
"pw_name": name[:128],
|
||||
"peer": row_get(r, "PEER", "peer")[:64],
|
||||
"fec": row_get(r, "FEC", "fec")[:32],
|
||||
"pw_type": pwt[:64],
|
||||
"state": state[:32],
|
||||
"local_label": row_get(r, "LLABEL", "local_label")[:32],
|
||||
"remote_label": row_get(r, "RLABEL", "remote_label")[:32],
|
||||
"vpn_owner": row_get(r, "VPN_OWNER", "vpn_owner")[:256],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||
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("pwname"):
|
||||
continue
|
||||
m = _PW_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group("name")
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
mode = (m.group("mode") or "").strip()
|
||||
pwt = m.group("pwt")
|
||||
if mode:
|
||||
pwt = f"{pwt} {mode}"
|
||||
out.append(
|
||||
{
|
||||
"pw_name": name[:128],
|
||||
"peer": m.group("peer")[:64],
|
||||
"fec": m.group("fec")[:32],
|
||||
"pw_type": pwt[:64],
|
||||
"state": m.group("state")[:32],
|
||||
"local_label": m.group("ll")[:32],
|
||||
"remote_label": m.group("rl")[:32],
|
||||
"vpn_owner": m.group("owner")[:256],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_l2vpn_pw(
|
||||
*,
|
||||
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 l2vpn forwardinfo").strip()
|
||||
if platform and cmd:
|
||||
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)
|
||||
|
||||
|
||||
normalize_l2vpn_pw.RULE_KEYS = RULE_KEYS
|
||||
93
netx_api/biz_state/parsers/zte/optical_brief.py
Normal file
93
netx_api/biz_state/parsers/zte/optical_brief.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""ZTE: show opticalinfo brief."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_opticalinfo_brief",)
|
||||
|
||||
_OPT_RE = re.compile(
|
||||
r"^(?P<iface>\S+)\s+(?P<otype>\S+)\s+(?P<wave>\S+)\s+"
|
||||
r"(?P<rx>\S+)\s+(?P<tx>\S+)\s+(?P<status>\S+)",
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
iface = row_get(r, "INTERFACE", "interface")
|
||||
if not iface or iface.lower() == "interface" or iface in seen:
|
||||
continue
|
||||
seen.add(iface)
|
||||
out.append(
|
||||
{
|
||||
"interface": iface[:128],
|
||||
"optic_type": row_get(r, "OPTIC_TYPE", "optic_type")[:64],
|
||||
"wavelength": row_get(r, "WAVELENGTH", "wavelength")[:32],
|
||||
"rx_power": row_get(r, "RX_POWER", "rx_power")[:64],
|
||||
"tx_power": row_get(r, "TX_POWER", "tx_power")[:64],
|
||||
"status": row_get(r, "STATUS", "status")[:32],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||
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"):
|
||||
continue
|
||||
m = _OPT_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
iface = m.group("iface")
|
||||
if iface in seen:
|
||||
continue
|
||||
seen.add(iface)
|
||||
out.append(
|
||||
{
|
||||
"interface": iface[:128],
|
||||
"optic_type": m.group("otype")[:64],
|
||||
"wavelength": m.group("wave")[:32],
|
||||
"rx_power": m.group("rx")[:64],
|
||||
"tx_power": m.group("tx")[:64],
|
||||
"status": m.group("status")[:32],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_optical_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 opticalinfo brief").strip()
|
||||
if platform and cmd:
|
||||
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)
|
||||
|
||||
|
||||
normalize_optical_brief.RULE_KEYS = RULE_KEYS
|
||||
108
netx_api/biz_state/parsers/zte/ospf_neighbor.py
Normal file
108
netx_api/biz_state/parsers/zte/ospf_neighbor.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""ZTE: show ip ospf neighbor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_ospf_neighbor",)
|
||||
|
||||
_PROC_RE = re.compile(r"Process\s+ID\s+(\d+)", re.I)
|
||||
_NEI_RE = re.compile(
|
||||
r"^(?P<nid>\d{1,3}(?:\.\d{1,3}){3})\s+(?P<pri>\S+)\s+(?P<state>\S+)\s+"
|
||||
r"(?P<dead>\S+)\s+(?P<addr>\S+)\s+(?P<iface>\S+)\s*$"
|
||||
)
|
||||
|
||||
|
||||
def _map_fsm_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
for r in rows:
|
||||
nid = row_get(r, "NEIGHBOR_ID", "neighbor_id")
|
||||
iface = row_get(r, "INTERFACE", "interface")
|
||||
proc = row_get(r, "PROCESS_ID", "process_id") or "0"
|
||||
if not nid or not iface:
|
||||
continue
|
||||
key = (proc, nid, iface)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"process_id": proc[:32],
|
||||
"neighbor_id": nid[:64],
|
||||
"pri": row_get(r, "PRI", "pri")[:16],
|
||||
"state": row_get(r, "STATE", "state")[:64],
|
||||
"dead_time": row_get(r, "DEAD_TIME", "dead_time")[:32],
|
||||
"address": row_get(r, "ADDRESS", "address")[:64],
|
||||
"interface": iface[:128],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
proc = "0"
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
pm = _PROC_RE.search(line)
|
||||
if pm:
|
||||
proc = pm.group(1)
|
||||
continue
|
||||
if line.lower().startswith("neighbor id"):
|
||||
continue
|
||||
m = _NEI_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
key = (proc, m.group("nid"), m.group("iface"))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"process_id": proc[:32],
|
||||
"neighbor_id": m.group("nid")[:64],
|
||||
"pri": m.group("pri")[:16],
|
||||
"state": m.group("state")[:64],
|
||||
"dead_time": m.group("dead")[:32],
|
||||
"address": m.group("addr")[:64],
|
||||
"interface": m.group("iface")[:128],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_ospf_neighbor(
|
||||
*,
|
||||
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 ip ospf neighbor").strip()
|
||||
if platform and cmd:
|
||||
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)
|
||||
|
||||
|
||||
normalize_ospf_neighbor.RULE_KEYS = RULE_KEYS
|
||||
122
netx_api/biz_state/parsers/zte/vrrp.py
Normal file
122
netx_api/biz_state/parsers/zte/vrrp.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""ZTE: show vrrp {ipv4|ipv6} brief."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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_vrrp_brief",)
|
||||
|
||||
_VRRP_RE = re.compile(
|
||||
r"^(?P<iface>\S+)\s+(?P<vrid>\d+)\s+(?P<pri>\d+)\s+(?P<time>\d+)\s+"
|
||||
r"(?:(?P<flags>[APL\s]+)\s+)?"
|
||||
r"(?P<state>Master|Backup|Init)\s+"
|
||||
r"(?P<master>\S+)\s+(?P<vrouter>\S+)\s*$",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def _detect_af(command: str, params: dict[str, str] | None) -> str:
|
||||
if params and params.get("af"):
|
||||
return str(params.get("af") or "").strip().lower()
|
||||
low = str(command or "").lower()
|
||||
if "ipv6" in low:
|
||||
return "ipv6"
|
||||
return "ipv4"
|
||||
|
||||
|
||||
def _map_fsm_rows(rows: list[dict[str, Any]], *, af: str) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
for r in rows:
|
||||
iface = row_get(r, "INTERFACE", "interface")
|
||||
vrid = row_get(r, "VR_ID", "vr_id")
|
||||
if not iface or not vrid:
|
||||
continue
|
||||
key = (af, iface, vrid)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"af": af[:16],
|
||||
"interface": iface[:128],
|
||||
"vr_id": vrid[:32],
|
||||
"priority": row_get(r, "PRIORITY", "priority")[:16],
|
||||
"time": row_get(r, "TIME", "time")[:16],
|
||||
"flags": row_get(r, "FLAGS", "flags")[:16],
|
||||
"state": row_get(r, "STATE", "state")[:32],
|
||||
"master_addr": row_get(r, "MASTER_ADDR", "master_addr")[:128],
|
||||
"vrouter_addr": row_get(r, "VROUTER_ADDR", "vrouter_addr")[:128],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _hand_parse(*, raw_text: str, af: str = "ipv4", **_kw: Any) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
for raw in str(raw_text or "").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.lower().startswith("interface"):
|
||||
continue
|
||||
m = _VRRP_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
key = (af, m.group("iface"), m.group("vrid"))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(
|
||||
{
|
||||
"af": af[:16],
|
||||
"interface": m.group("iface")[:128],
|
||||
"vr_id": m.group("vrid")[:32],
|
||||
"priority": m.group("pri")[:16],
|
||||
"time": m.group("time")[:16],
|
||||
"flags": (m.group("flags") or "").strip()[:16],
|
||||
"state": m.group("state")[:32],
|
||||
"master_addr": m.group("master")[:128],
|
||||
"vrouter_addr": m.group("vrouter")[:128],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_vrrp(
|
||||
*,
|
||||
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]]:
|
||||
af = _detect_af(command, 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 vrrp ipv4 brief").strip()
|
||||
if platform and cmd:
|
||||
tables = apply_rules(
|
||||
platform=platform, text=raw_text, rule_keys=RULE_KEYS, command=cmd
|
||||
)
|
||||
|
||||
def _map(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return _map_fsm_rows(rows, af=af)
|
||||
|
||||
def _hand(*, raw_text: str, **kw: Any) -> list[dict[str, Any]]:
|
||||
return _hand_parse(raw_text=raw_text, af=af, **kw)
|
||||
|
||||
return prefer_fsm(tables, RULE_KEYS, _map, _hand, raw_text=raw_text)
|
||||
|
||||
|
||||
normalize_vrrp.RULE_KEYS = RULE_KEYS
|
||||
Loading…
Add table
Add a link
Reference in a new issue