netx/netx_api/biz_state/iface_normalize.py
oliver 05357e971e Fix ZTE BGP RR *i/RD parsing and add batch collect detail UX.
Decouple heavy CLI parse via ParsePool, keep ECMP/multi-RD keys, and expose per-command CLI lines vs DB rows with raw.txt export.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 10:52:43 +08:00

144 lines
4.5 KiB
Python

"""Interface name normalize + port-map resolve (exact + parent.subif).
Normalize rules (template-owned): rewrite type aliases like ``GE`` → ``gei``.
Port map (job-owned): rewrite before → after, with subinterface suffix inheritance.
"""
from __future__ import annotations
from typing import Any, Mapping, Sequence
def normalize_iface_rules(raw: Any) -> list[dict[str, str]]:
"""Accept list of {from,to} or ``from,to`` lines; drop empties; longest ``from`` first."""
out: list[dict[str, str]] = []
if not raw:
return out
if isinstance(raw, str):
items: list[Any] = []
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "," in line:
a, b = line.split(",", 1)
items.append({"from": a.strip(), "to": b.strip()})
elif "\t" in line:
a, b = line.split("\t", 1)
items.append({"from": a.strip(), "to": b.strip()})
else:
parts = line.split()
if len(parts) >= 2:
items.append({"from": parts[0], "to": parts[1]})
raw = items
if not isinstance(raw, (list, tuple)):
return out
for item in raw:
if not isinstance(item, Mapping):
continue
fr = str(item.get("from") or item.get("src") or "").strip()
to = str(item.get("to") or item.get("dst") or "").strip()
if fr and to:
out.append({"from": fr, "to": to})
# Longest prefix first so XXVGE wins over XGE / GE.
out.sort(key=lambda r: len(r["from"]), reverse=True)
return out
def _prefix_ok(name: str, prefix: str) -> bool:
"""True when ``name`` starts with ``prefix`` (ci) and boundary is safe."""
if not name or not prefix:
return False
n = name
p = prefix
if len(n) < len(p):
return False
if n[: len(p)].lower() != p.lower():
return False
if len(n) == len(p):
return True
nxt = n[len(p)]
return nxt.isdigit() or nxt in "-/"
def normalize_iface_name(name: str, rules: Sequence[Mapping[str, str]] | None) -> str:
"""Apply first matching type-prefix rule; leave subif / rest intact."""
text = str(name or "").strip()
if not text or not rules:
return text
for rule in rules:
fr = str(rule.get("from") or "").strip()
to = str(rule.get("to") or "").strip()
if not fr or not to:
continue
if _prefix_ok(text, fr):
return to + text[len(fr) :]
return text
def apply_iface_normalize(
row: dict[str, Any],
*,
iface_fields: Sequence[str],
rules: Sequence[Mapping[str, str]] | None,
) -> dict[str, Any]:
if not rules or not iface_fields:
return dict(row)
out = dict(row)
for f in iface_fields:
key = str(f or "").strip()
if not key:
continue
val = str(out.get(key) or "").strip()
if val:
out[key] = normalize_iface_name(val, rules)
return out
def apply_iface_normalize_rows(
rows: Sequence[Mapping[str, Any]],
*,
iface_fields: Sequence[str],
rules: Sequence[Mapping[str, str]] | None,
) -> list[dict[str, Any]]:
return [
apply_iface_normalize(dict(r), iface_fields=iface_fields, rules=rules)
for r in rows
]
def resolve_mapped_iface(name: str, port_map: Mapping[str, str] | None) -> str:
"""Exact map hit, else longest mapped parent + remaining suffix (QinQ-safe).
``gei-0/1/0/1.100.200`` tries exact, then ``gei-0/1/0/1.100``, then
``gei-0/1/0/1`` — not only the last ``.`` segment.
"""
text = str(name or "").strip()
if not text or not port_map:
return text
if text in port_map:
return str(port_map[text])
if "." not in text:
return text
parts = text.split(".")
for i in range(len(parts) - 1, 0, -1):
parent = ".".join(parts[:i])
suffix = ".".join(parts[i:])
if not parent or not suffix:
continue
if parent in port_map:
return f"{port_map[parent]}.{suffix}"
return text
def default_zte_iface_normalize_rules() -> list[dict[str, str]]:
"""Common ZTE brief aliases → canonical CLI names."""
return normalize_iface_rules(
[
{"from": "XXVGE", "to": "xxvgei"},
{"from": "XGE", "to": "xgei"},
{"from": "CGE", "to": "cgei"},
{"from": "GE", "to": "gei"},
{"from": "SG", "to": "smartgroup"},
]
)