netx/netx_api/biz_state/parsers/zte/config_isis.py
oliver 285c19e5a3 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>
2026-09-19 21:11:31 +08:00

210 lines
6.8 KiB
Python

"""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