mirror of
https://github.com/hansjone/netx.git
synced 2026-10-19 14:20:47 +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
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue