netx/netx_api/biz_state/parsers/zte/config_bgp_peer.py
oliver 0aa2bcbbe6 Fix VPNv6 wrap parsing and expand global BGP peer-group members.
Join IPv6 summary/route continuations; reject false FSM hits. For non-VRF AF activates, expand peer-group to member Neighbor IPs (VRF stays direct-only).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 15:10:35 +08:00

242 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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.
For **global** (non-VRF) address-families, a peer-group ``activate`` is also
expanded into one row per global member ``neighbor <ip> peer-group <name>``
so discover/bind sees the real Neighbor IPs (direct activates ∪ group
members). VRF address-families do **not** expand from global membership.
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()
def _append(row: dict[str, Any]) -> None:
key = (row["afi"], row["vrf"], row["neighbor"], row["peer_group"])
if key in seen:
return
seen.add(key)
out.append(row)
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)
_append(row)
# Global AF: peer-group activate → expand to member Neighbor IPs.
# VRF AF: do not expand from global peer-group membership.
if vrf_s:
continue
pg = str(row.get("peer_group") or "").strip()
if row.get("neighbor") or not pg:
continue
for member, minfo in meta.items():
if not _is_ip_neighbor(member):
continue
if str(minfo.get("peer_group") or "").strip() != pg:
continue
m_info = _merge_info(
member, afi=afi_s, vrf=vrf_s, meta=meta, af_rm=af_rm
)
m_row = _row(
afi=afi_s, vrf=vrf_s, token=member, act=act, info=m_info
)
m_row["peer_group"] = pg[:64]
_append(m_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)
_append(row)
return out
normalize_config_bgp_peer.RULE_KEYS = RULE_KEYS