mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Use custom cli_templates plus community ntc-templates for LLDP and port traffic across ZTE/Huawei/Cisco/H3C/Juniper/Nokia, drop regex parsers, and surface Admin/Phy/Prot on port discover. Also batch-add topology NEs and tighten workbench branding. Co-authored-by: Cursor <cursoragent@cursor.com>
409 lines
14 KiB
Python
409 lines
14 KiB
Python
"""Interface brief/detail parsers via TextFSM only (``ntc_parse``).
|
|
|
|
No regex CLI parsers — add / fix templates under ``cli_templates/{vendor}/``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from .ntc_parse import parse_cli, resolve_cli_platform, row_get
|
|
|
|
|
|
_BW_UNIT = {
|
|
"k": 1_000,
|
|
"m": 1_000_000,
|
|
"g": 1_000_000_000,
|
|
"t": 1_000_000_000_000,
|
|
}
|
|
|
|
_RE_BW_COMPACT = re.compile(r"^(\d+(?:\.\d+)?)\s*([kKmMgGtT])(?:bit)?s?$", re.I)
|
|
_RE_BW_DETAIL = re.compile(
|
|
r"\bBW\s+(\d+(?:\.\d+)?)\s*([kKmMgGtT])?\s*(?:G?bit|bit)/s(?:ec)?\b",
|
|
re.I,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BriefPort:
|
|
ifname: str
|
|
attribute: str = ""
|
|
mode: str = ""
|
|
bw_raw: str = ""
|
|
bw_bps: int = 0
|
|
admin: str = ""
|
|
phy: str = ""
|
|
prot: str = ""
|
|
description: str = ""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DetailRates:
|
|
ifname: str = ""
|
|
admin_oper: str = ""
|
|
description: str = ""
|
|
bw_bps: int = 0
|
|
rate_period_sec: int = 0
|
|
in_bps: float = 0.0
|
|
out_bps: float = 0.0
|
|
in_util_pct: float = 0.0
|
|
out_util_pct: float = 0.0
|
|
|
|
|
|
def resolve_util_pct(vendor_util: float, bps: float, bw_bps: int) -> float:
|
|
"""Prefer vendor-reported util; derive from rate/BW when util is missing but traffic exists."""
|
|
u = float(vendor_util or 0.0)
|
|
if u > 0:
|
|
return u
|
|
bw = int(bw_bps or 0)
|
|
rate = float(bps or 0.0)
|
|
if bw > 0 and rate > 0:
|
|
return rate / float(bw) * 100.0
|
|
return u
|
|
|
|
|
|
def parse_bw_to_bps(raw: str) -> int:
|
|
"""Normalize bandwidth field values from TextFSM (``1G``, ``BW 1 Gbit/s``, …)."""
|
|
text = (raw or "").strip()
|
|
if not text or text.upper() == "N/A":
|
|
return 0
|
|
m = _RE_BW_COMPACT.match(text.replace(" ", ""))
|
|
if m:
|
|
return int(float(m.group(1)) * _BW_UNIT[m.group(2).lower()])
|
|
m2 = _RE_BW_DETAIL.search(text)
|
|
if m2:
|
|
value = float(m2.group(1))
|
|
unit = (m2.group(2) or "g").lower()
|
|
if m2.group(2) is None and "gbit" in text.lower():
|
|
unit = "g"
|
|
elif m2.group(2) is None and "mbit" in text.lower():
|
|
unit = "m"
|
|
elif m2.group(2) is None and "kbit" in text.lower():
|
|
unit = "k"
|
|
return int(value * _BW_UNIT.get(unit, 1_000_000_000))
|
|
digits = re.sub(r"[^\d.]", "", text)
|
|
if digits:
|
|
try:
|
|
return int(float(digits))
|
|
except ValueError:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def brief_port_to_dict(row: BriefPort) -> dict[str, Any]:
|
|
return {
|
|
"ifname": row.ifname,
|
|
"attribute": row.attribute,
|
|
"mode": row.mode,
|
|
"bw_raw": row.bw_raw,
|
|
"bw_bps": row.bw_bps,
|
|
"admin": row.admin,
|
|
"phy": row.phy,
|
|
"prot": row.prot,
|
|
"description": row.description,
|
|
}
|
|
|
|
|
|
def detail_to_dict(row: DetailRates) -> dict[str, Any]:
|
|
return {
|
|
"ifname": row.ifname,
|
|
"admin_oper": row.admin_oper,
|
|
"description": row.description,
|
|
"bw_bps": row.bw_bps,
|
|
"rate_period_sec": row.rate_period_sec,
|
|
"in_bps": row.in_bps,
|
|
"out_bps": row.out_bps,
|
|
"in_util_pct": row.in_util_pct,
|
|
"out_util_pct": row.out_util_pct,
|
|
}
|
|
|
|
|
|
def _brief_command_for_vendor(vendor_key: str) -> str:
|
|
from .port_traffic_commands import commands_for_vendor
|
|
|
|
key = str(vendor_key or "zte").strip().lower()
|
|
cmds = commands_for_vendor(key, key)
|
|
return cmds.brief if cmds else "show interface brief"
|
|
|
|
|
|
def _detail_command_for_vendor(vendor_key: str, ifname: str = "") -> str:
|
|
from .port_traffic_commands import commands_for_vendor, detail_command
|
|
|
|
key = str(vendor_key or "zte").strip().lower()
|
|
name = (ifname or "IFACE").strip() or "IFACE"
|
|
cmds = commands_for_vendor(key, key)
|
|
if cmds:
|
|
return detail_command(cmds, name)
|
|
return f"show interface {name}"
|
|
|
|
|
|
def _bw_field_to_bps(raw: str) -> int:
|
|
"""Normalize TextFSM bandwidth fields (e.g. ``1000000 Kbit``, ``1 Gbit/s``)."""
|
|
text = (raw or "").strip()
|
|
if not text:
|
|
return 0
|
|
m = re.search(r"(?i)(\d+(?:\.\d+)?)\s*([kKmMgGtT])\s*bit\b", text)
|
|
if m:
|
|
return int(float(m.group(1)) * _BW_UNIT[m.group(2).lower()])
|
|
prefixed = text if text.upper().startswith("BW") else f"BW {text}"
|
|
got = parse_bw_to_bps(prefixed)
|
|
if got:
|
|
return got
|
|
if text.isdigit():
|
|
try:
|
|
return int(text)
|
|
except ValueError:
|
|
return 0
|
|
return parse_bw_to_bps(text)
|
|
|
|
|
|
def _norm_updown(token: str) -> str:
|
|
text = (token or "").strip().lower()
|
|
if "administratively" in text:
|
|
return "down"
|
|
if text.startswith("*"):
|
|
text = text[1:]
|
|
if text.startswith("up"):
|
|
return "up"
|
|
if text.startswith("down"):
|
|
return "down"
|
|
return text if text in {"up", "down"} else ""
|
|
|
|
|
|
def _yes_no_updown(token: str) -> str:
|
|
text = (token or "").strip().lower()
|
|
if text in {"yes", "up", "true", "1"}:
|
|
return "up"
|
|
if text in {"no", "down", "false", "0", "ghost"}:
|
|
return "down"
|
|
return _norm_updown(text)
|
|
|
|
|
|
def _map_brief_rows(rows: list[dict[str, Any]], *, vendor_key: str) -> list[BriefPort]:
|
|
key = str(vendor_key or "zte").strip().lower()
|
|
out: list[BriefPort] = []
|
|
for row in rows:
|
|
ifname = row_get(row, "interface", "ifname", "port", "intf", "port_id", "name")
|
|
if not ifname or "#" in ifname or ">" in ifname:
|
|
continue
|
|
admin = _norm_updown(
|
|
row_get(row, "admin", "admin_state", "status", "phy", "link", "link_status")
|
|
)
|
|
phy = _norm_updown(row_get(row, "phy", "status", "link_status", "port_state", "link"))
|
|
prot = _norm_updown(row_get(row, "prot", "protocol", "proto", "protocol_status"))
|
|
if key == "cisco":
|
|
admin = _norm_updown(row_get(row, "status", "admin", "link_status")) or admin
|
|
phy = admin
|
|
prot = _norm_updown(row_get(row, "proto", "protocol", "prot")) or prot
|
|
elif key == "huawei":
|
|
phy = _norm_updown(row_get(row, "phy", "status")) or phy
|
|
prot = _norm_updown(row_get(row, "protocol", "prot", "proto")) or prot
|
|
admin = phy or admin
|
|
elif key == "h3c":
|
|
# Community: LINK / PROTOCOL (route mode) or LINK only (bridge mode).
|
|
link = _norm_updown(row_get(row, "link", "admin", "phy"))
|
|
admin = link or admin
|
|
phy = link or phy
|
|
prot = _norm_updown(row_get(row, "protocol", "prot", "proto")) or prot or phy
|
|
elif key == "juniper":
|
|
admin = _norm_updown(row_get(row, "admin_state", "admin", "status")) or admin
|
|
phy = _norm_updown(row_get(row, "link_status", "phy", "status")) or phy
|
|
prot = phy or prot or admin
|
|
elif key == "nokia":
|
|
admin = _norm_updown(row_get(row, "admin_state", "admin")) or admin
|
|
link = _yes_no_updown(row_get(row, "link"))
|
|
phy = _norm_updown(row_get(row, "port_state", "phy")) or link or phy
|
|
prot = phy or prot or admin
|
|
elif key == "mikrotik":
|
|
# print brief often exposes flags / name; tolerate missing proto.
|
|
admin = _norm_updown(row_get(row, "status", "admin", "link")) or admin or "up"
|
|
phy = _norm_updown(row_get(row, "link", "status", "phy")) or phy or admin
|
|
prot = _norm_updown(row_get(row, "protocol", "prot")) or prot or phy
|
|
if admin not in {"up", "down"} and phy in {"up", "down"}:
|
|
admin = phy
|
|
if phy not in {"up", "down"}:
|
|
phy = admin if admin in {"up", "down"} else phy
|
|
if prot not in {"up", "down"}:
|
|
# Prefer inferring prot from phy when vendor brief has no protocol column.
|
|
if phy in {"up", "down"} and key in {"juniper", "nokia", "mikrotik", "h3c"}:
|
|
prot = phy
|
|
else:
|
|
continue
|
|
if admin not in {"up", "down"} or phy not in {"up", "down"}:
|
|
continue
|
|
bw_raw = row_get(row, "bw", "bandwidth", "bw_raw", "speed")
|
|
out.append(
|
|
BriefPort(
|
|
ifname=ifname,
|
|
attribute=row_get(row, "attribute", "attr", "port_type", "type"),
|
|
mode=row_get(row, "mode", "port_mode", "duplex"),
|
|
bw_raw=bw_raw,
|
|
bw_bps=parse_bw_to_bps(bw_raw),
|
|
admin=admin,
|
|
phy=phy,
|
|
prot=prot,
|
|
description=row_get(row, "description", "descrip", "desc"),
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
def _period_to_sec(n: int, unit: str) -> int:
|
|
u = (unit or "").lower()
|
|
if u.startswith("second"):
|
|
return int(n)
|
|
if u.startswith("minute"):
|
|
return int(n) * 60
|
|
if u.startswith("hour"):
|
|
return int(n) * 3600
|
|
return int(n)
|
|
|
|
|
|
def _map_detail_rows(rows: list[dict[str, Any]]) -> list[DetailRates]:
|
|
best: dict[str, DetailRates] = {}
|
|
for row in rows:
|
|
ifname = row_get(row, "ifname", "interface", "port")
|
|
in_bps_s = row_get(row, "input_bps", "in_bps", "input_rate", "input")
|
|
out_bps_s = row_get(row, "output_bps", "out_bps", "output_rate", "output")
|
|
try:
|
|
in_bps = float(in_bps_s or 0)
|
|
except ValueError:
|
|
in_bps = 0.0
|
|
try:
|
|
out_bps = float(out_bps_s or 0)
|
|
except ValueError:
|
|
out_bps = 0.0
|
|
bw_raw = row_get(row, "bw_raw", "bandwidth", "bw")
|
|
bw_bps = _bw_field_to_bps(bw_raw)
|
|
period_s = row_get(row, "rate_period", "period", "rate_period_sec")
|
|
period_unit = row_get(row, "rate_period_unit", "period_unit")
|
|
try:
|
|
rate_period_sec = _period_to_sec(int(float(period_s or 0)), period_unit)
|
|
except ValueError:
|
|
rate_period_sec = 0
|
|
in_util_s = row_get(row, "in_util", "input_util", "inuti")
|
|
out_util_s = row_get(row, "out_util", "output_util", "oututi")
|
|
try:
|
|
in_util = float((in_util_s or "0").rstrip("%"))
|
|
except ValueError:
|
|
in_util = 0.0
|
|
try:
|
|
out_util = float((out_util_s or "0").rstrip("%"))
|
|
except ValueError:
|
|
out_util = 0.0
|
|
admin = _norm_updown(
|
|
row_get(
|
|
row,
|
|
"admin_oper",
|
|
"status",
|
|
"admin",
|
|
"link_status",
|
|
"line_status",
|
|
"admin_state",
|
|
)
|
|
)
|
|
ifname = ifname or row_get(row, "port_id", "name")
|
|
if not ifname and in_bps <= 0 and out_bps <= 0 and not bw_bps:
|
|
continue
|
|
cand = DetailRates(
|
|
ifname=ifname,
|
|
admin_oper=admin,
|
|
description=row_get(row, "description", "descrip", "interface_description"),
|
|
bw_bps=bw_bps,
|
|
rate_period_sec=rate_period_sec,
|
|
in_bps=in_bps,
|
|
out_bps=out_bps,
|
|
in_util_pct=in_util,
|
|
out_util_pct=out_util,
|
|
)
|
|
key = ifname or "_"
|
|
prev = best.get(key)
|
|
if prev is None or (in_util_s or out_util_s or in_bps or out_bps):
|
|
best[key] = cand
|
|
return list(best.values())
|
|
|
|
|
|
def parse_interface_brief(
|
|
text: str,
|
|
vendor_key: str = "zte",
|
|
*,
|
|
command: str = "",
|
|
device_type: str = "",
|
|
) -> list[BriefPort]:
|
|
"""Parse brief CLI via custom/community TextFSM. Empty list if no template match."""
|
|
key = str(vendor_key or "zte").strip().lower()
|
|
plat = resolve_cli_platform(vendor_key=key, device_type=device_type)
|
|
cmd = (command or _brief_command_for_vendor(key)).strip()
|
|
if not plat or not cmd:
|
|
return []
|
|
rows = parse_cli(platform=plat, command=cmd, text=text)
|
|
return _map_brief_rows(rows, vendor_key=key)
|
|
|
|
|
|
def parse_interface_detail(
|
|
text: str,
|
|
vendor_key: str = "zte",
|
|
*,
|
|
command: str = "",
|
|
device_type: str = "",
|
|
ifname: str = "",
|
|
) -> DetailRates:
|
|
"""Parse detail CLI via custom/community TextFSM. Empty DetailRates if no match."""
|
|
key = str(vendor_key or "zte").strip().lower()
|
|
plat = resolve_cli_platform(vendor_key=key, device_type=device_type)
|
|
cmd = (command or _detail_command_for_vendor(key, ifname=ifname)).strip()
|
|
if not plat or not cmd:
|
|
return DetailRates()
|
|
rows = parse_cli(platform=plat, command=cmd, text=text)
|
|
mapped = _map_detail_rows(rows)
|
|
return mapped[0] if mapped else DetailRates()
|
|
|
|
|
|
def parse_zte_interface_brief(text: str) -> list[BriefPort]:
|
|
return parse_interface_brief(text, "zte", device_type="zte_zxros")
|
|
|
|
|
|
def parse_zte_interface_detail(text: str, *, ifname: str = "") -> DetailRates:
|
|
return parse_interface_detail(text, "zte", device_type="zte_zxros", ifname=ifname)
|
|
|
|
|
|
def parse_huawei_interface_brief(text: str) -> list[BriefPort]:
|
|
return parse_interface_brief(text, "huawei", device_type="huawei_vrp")
|
|
|
|
|
|
def parse_huawei_interface_detail(text: str, *, ifname: str = "") -> DetailRates:
|
|
return parse_interface_detail(text, "huawei", device_type="huawei_vrp", ifname=ifname)
|
|
|
|
|
|
def parse_cisco_interface_brief(text: str) -> list[BriefPort]:
|
|
return parse_interface_brief(text, "cisco", device_type="cisco_ios")
|
|
|
|
|
|
def parse_cisco_interface_detail(text: str, *, ifname: str = "") -> DetailRates:
|
|
return parse_interface_detail(text, "cisco", device_type="cisco_ios", ifname=ifname)
|
|
|
|
|
|
def parse_h3c_interface_brief(text: str) -> list[BriefPort]:
|
|
return parse_interface_brief(text, "h3c", device_type="hp_comware")
|
|
|
|
|
|
def parse_h3c_interface_detail(text: str, *, ifname: str = "") -> DetailRates:
|
|
return parse_interface_detail(text, "h3c", device_type="hp_comware", ifname=ifname)
|
|
|
|
|
|
def parse_juniper_interface_brief(text: str) -> list[BriefPort]:
|
|
return parse_interface_brief(text, "juniper", device_type="juniper_junos")
|
|
|
|
|
|
def parse_juniper_interface_detail(text: str, *, ifname: str = "") -> DetailRates:
|
|
return parse_interface_detail(text, "juniper", device_type="juniper_junos", ifname=ifname)
|
|
|
|
|
|
def parse_nokia_interface_brief(text: str) -> list[BriefPort]:
|
|
return parse_interface_brief(text, "nokia", device_type="nokia_sros")
|
|
|
|
|
|
def parse_nokia_interface_detail(text: str, *, ifname: str = "") -> DetailRates:
|
|
return parse_interface_detail(text, "nokia", device_type="nokia_sros", ifname=ifname)
|