mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 07:40:50 +08:00
Switch CLI parsing to TextFSM and polish topology/workbench UX.
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>
This commit is contained in:
parent
60186a12e3
commit
393fd0e0b4
41 changed files with 1771 additions and 1075 deletions
|
|
@ -1,4 +1,7 @@
|
|||
"""Parsers for ZTE / Huawei / Cisco interface brief & detail (rate bit/s)."""
|
||||
"""Interface brief/detail parsers via TextFSM only (``ntc_parse``).
|
||||
|
||||
No regex CLI parsers — add / fix templates under ``cli_templates/{vendor}/``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -6,6 +9,8 @@ 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,
|
||||
|
|
@ -15,79 +20,10 @@ _BW_UNIT = {
|
|||
}
|
||||
|
||||
_RE_BW_COMPACT = re.compile(r"^(\d+(?:\.\d+)?)\s*([kKmMgGtT])(?:bit)?s?$", re.I)
|
||||
# ZTE "BW 1 Gbit/s" and Cisco "BW 1000000 Kbit/sec"
|
||||
_RE_BW_DETAIL = re.compile(
|
||||
r"\bBW\s+(\d+(?:\.\d+)?)\s*([kKmMgGtT])?\s*(?:G?bit|bit)/s(?:ec)?\b",
|
||||
re.I,
|
||||
)
|
||||
_RE_RATE_PERIOD = re.compile(r"Rate\s+period\s*:\s*(\d+)\s*s", re.I)
|
||||
_RE_INPUT_BPS = re.compile(r"^\s*Input\s*:\s*([\d.]+)\s*bit/s", re.I | re.M)
|
||||
_RE_OUTPUT_BPS = re.compile(r"^\s*Output\s*:\s*([\d.]+)\s*bit/s", re.I | re.M)
|
||||
_RE_UTIL = re.compile(
|
||||
r"Intf\s+utilization\s*:\s*input\s*([\d.]+)%\s*output\s*([\d.]+)%",
|
||||
re.I,
|
||||
)
|
||||
_RE_IF_UP = re.compile(r"^(\S+)\s+is\s+(up|down)\b", re.I | re.M)
|
||||
_RE_DESC = re.compile(r"^\s*Description:\s*(.+?)\s*$", re.I | re.M)
|
||||
_RE_PROMPT_LINE = re.compile(r"[#>]\s*$")
|
||||
_RE_UPDOWN = re.compile(r"^(up|down)$", re.I)
|
||||
_RE_UPDOWN_TOKEN = re.compile(r"^(up|down)\b", re.I)
|
||||
# ZTE / Huawei / Cisco / common logical iface names.
|
||||
_RE_IFNAME = re.compile(
|
||||
r"^(?:"
|
||||
r"xxvgei|xgei|cgei|gei|fei|qli|smartgroup|bvi|vlan|loopback|mgmt|"
|
||||
r"null|pos|atm|tunnel|irb|pw|eth|ethernet|port-channel|bundle|"
|
||||
r"gigabitethernet|xgigabitethernet|fastethernet|tengigabitethernet|"
|
||||
r"hundredgige|fivegige|fortygige|serial|dialer|cellular|multilink|"
|
||||
r"10ge|25ge|40ge|100ge|eth-trunk|vlanif|meth|loopback"
|
||||
r")[\w./:-]*$",
|
||||
re.I,
|
||||
)
|
||||
_RE_CISCO_BRIEF_ROW = re.compile(
|
||||
r"^(\S+)\s+(\S+)\s+(YES|NO)\s+(\S+)\s+(.+?)\s+(up|down)\s*$",
|
||||
re.I,
|
||||
)
|
||||
_RE_CISCO_IF_STATE = re.compile(
|
||||
r"^(\S+)\s+is\s+(administratively\s+)?(up|down),\s*line\s+protocol\s+is\s+(up|down)\b",
|
||||
re.I | re.M,
|
||||
)
|
||||
_RE_CISCO_RATE = re.compile(
|
||||
r"(\d+)\s+(second|minute|hour)s?\s+(input|output)\s+rate\s+([\d.]+)\s*bits/sec",
|
||||
re.I,
|
||||
)
|
||||
_RE_HW_BRIEF_ROW = re.compile(
|
||||
r"^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s*$"
|
||||
)
|
||||
_RE_HW_STATE = re.compile(
|
||||
r"^(\S+)\s+current\s+state\s*:\s*(UP|DOWN|Administratively\s+DOWN)\b",
|
||||
re.I | re.M,
|
||||
)
|
||||
_RE_HW_IN_RATE = re.compile(
|
||||
r"Last\s+(\d+)\s+seconds\s+input\s+rate\s*:\s*([\d.]+)\s*bits/sec",
|
||||
re.I,
|
||||
)
|
||||
_RE_HW_OUT_RATE = re.compile(
|
||||
r"Last\s+(\d+)\s+seconds\s+output\s+rate\s*:\s*([\d.]+)\s*bits/sec",
|
||||
re.I,
|
||||
)
|
||||
_RE_HW_IN_UTIL = re.compile(
|
||||
r"Last\s+(\d+)\s+seconds\s+input\s+utility\s+rate\s*:\s*([\d.]+)\s*%",
|
||||
re.I,
|
||||
)
|
||||
_RE_HW_OUT_UTIL = re.compile(
|
||||
r"Last\s+(\d+)\s+seconds\s+output\s+utility\s+rate\s*:\s*([\d.]+)\s*%",
|
||||
re.I,
|
||||
)
|
||||
|
||||
# Fixed-width columns from ZTE `show interface brief` header.
|
||||
_COL_IF = (0, 24)
|
||||
_COL_ATTR = (24, 35)
|
||||
_COL_MODE = (35, 48)
|
||||
_COL_BW = (48, 54)
|
||||
_COL_ADMIN = (54, 60)
|
||||
_COL_PHY = (60, 66)
|
||||
_COL_PROT = (66, 72)
|
||||
_COL_DESC = (72, None)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -129,28 +65,24 @@ def resolve_util_pct(vendor_util: float, bps: float, bw_bps: int) -> float:
|
|||
|
||||
|
||||
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:
|
||||
value = float(m.group(1))
|
||||
mult = _BW_UNIT[m.group(2).lower()]
|
||||
return int(value * mult)
|
||||
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()
|
||||
# "BW 1 Gbit/s" — unit letter may be in "Gbit" when group2 empty after "1 "
|
||||
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"
|
||||
mult = _BW_UNIT.get(unit, 1_000_000_000)
|
||||
return int(value * mult)
|
||||
# Plain "BW 1000000000" unlikely; try digits only
|
||||
return int(value * _BW_UNIT.get(unit, 1_000_000_000))
|
||||
digits = re.sub(r"[^\d.]", "", text)
|
||||
if digits:
|
||||
try:
|
||||
|
|
@ -160,149 +92,6 @@ def parse_bw_to_bps(raw: str) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def _slice(line: str, start: int, end: int | None) -> str:
|
||||
if end is None:
|
||||
return line[start:].rstrip() if len(line) > start else ""
|
||||
if len(line) <= start:
|
||||
return ""
|
||||
return line[start:end].strip()
|
||||
|
||||
|
||||
def _looks_like_brief_port(ifname: str, admin: str, phy: str, prot: str) -> bool:
|
||||
"""Reject prompts / hostname lines that leak after the brief table."""
|
||||
name = (ifname or "").strip()
|
||||
if not name or name.lower() == "interface":
|
||||
return False
|
||||
if "#" in name or ">" in name:
|
||||
return False
|
||||
if _RE_PROMPT_LINE.search(name):
|
||||
return False
|
||||
# Real brief rows always have Admin/Phy/Prot as up|down.
|
||||
if not (
|
||||
_RE_UPDOWN.match(admin or "")
|
||||
and _RE_UPDOWN.match(phy or "")
|
||||
and _RE_UPDOWN.match(prot or "")
|
||||
):
|
||||
return False
|
||||
# Prefer known iface prefixes; still allow smartgroup/bvi style names.
|
||||
if not _RE_IFNAME.match(name):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def parse_zte_interface_brief(text: str) -> list[BriefPort]:
|
||||
"""Parse ZTE `show interface brief` into port rows."""
|
||||
lines = (text or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||
out: list[BriefPort] = []
|
||||
started = False
|
||||
for raw in lines:
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.lstrip().startswith("Interface") and "Admin" in line and "Description" in line:
|
||||
started = True
|
||||
continue
|
||||
if not started:
|
||||
continue
|
||||
stripped = line.strip()
|
||||
# Trailing device prompt after the table (Netmiko often leaves hostname#).
|
||||
if "#" in stripped or (stripped.endswith(">") and " " not in stripped):
|
||||
admin_probe = _slice(line, *_COL_ADMIN).lower()
|
||||
if not _RE_UPDOWN.match(admin_probe):
|
||||
break
|
||||
if " is " in line and "ifindex" in line.lower():
|
||||
break
|
||||
if_field = _slice(line, *_COL_IF)
|
||||
ifname = if_field.split()[0] if if_field else ""
|
||||
admin = _slice(line, *_COL_ADMIN).lower()
|
||||
phy = _slice(line, *_COL_PHY).lower()
|
||||
prot = _slice(line, *_COL_PROT).lower()
|
||||
if not _looks_like_brief_port(ifname, admin, phy, prot):
|
||||
continue
|
||||
bw_raw = _slice(line, *_COL_BW)
|
||||
out.append(
|
||||
BriefPort(
|
||||
ifname=ifname,
|
||||
attribute=_slice(line, *_COL_ATTR),
|
||||
mode=_slice(line, *_COL_MODE),
|
||||
bw_raw=bw_raw,
|
||||
bw_bps=parse_bw_to_bps(bw_raw),
|
||||
admin=admin,
|
||||
phy=phy,
|
||||
prot=prot,
|
||||
description=_slice(line, *_COL_DESC).strip(),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def parse_zte_interface_detail(text: str) -> DetailRates:
|
||||
"""Parse ZTE `show interface {ifname}` rate / util / BW."""
|
||||
blob = text or ""
|
||||
ifname = ""
|
||||
admin_oper = ""
|
||||
m_if = _RE_IF_UP.search(blob)
|
||||
if m_if:
|
||||
ifname = m_if.group(1)
|
||||
admin_oper = m_if.group(2).lower()
|
||||
desc = ""
|
||||
m_desc = _RE_DESC.search(blob)
|
||||
if m_desc:
|
||||
desc = m_desc.group(1).strip()
|
||||
|
||||
bw_bps = 0
|
||||
m_bw = _RE_BW_DETAIL.search(blob)
|
||||
if m_bw:
|
||||
bw_bps = parse_bw_to_bps(m_bw.group(0))
|
||||
else:
|
||||
# Fallback line scan
|
||||
for line in blob.splitlines():
|
||||
if re.search(r"\bBW\b", line, re.I):
|
||||
bw_bps = parse_bw_to_bps(line)
|
||||
if bw_bps:
|
||||
break
|
||||
|
||||
rate_period = 0
|
||||
m_rp = _RE_RATE_PERIOD.search(blob)
|
||||
if m_rp:
|
||||
rate_period = int(m_rp.group(1))
|
||||
|
||||
# Prefer Rate period block: first Input/Output after "Rate period"
|
||||
in_bps = 0.0
|
||||
out_bps = 0.0
|
||||
rp_idx = blob.lower().find("rate period")
|
||||
rate_blob = blob[rp_idx:] if rp_idx >= 0 else blob
|
||||
# Stop before Peak rate to avoid peak Input/Output
|
||||
peak_idx = rate_blob.lower().find("peak rate")
|
||||
if peak_idx >= 0:
|
||||
rate_blob = rate_blob[:peak_idx]
|
||||
m_in = _RE_INPUT_BPS.search(rate_blob)
|
||||
m_out = _RE_OUTPUT_BPS.search(rate_blob)
|
||||
if m_in:
|
||||
in_bps = float(m_in.group(1))
|
||||
if m_out:
|
||||
out_bps = float(m_out.group(1))
|
||||
|
||||
in_util = 0.0
|
||||
out_util = 0.0
|
||||
m_util = _RE_UTIL.search(blob)
|
||||
if m_util:
|
||||
in_util = float(m_util.group(1))
|
||||
out_util = float(m_util.group(2))
|
||||
|
||||
return DetailRates(
|
||||
ifname=ifname,
|
||||
admin_oper=admin_oper,
|
||||
description=desc,
|
||||
bw_bps=bw_bps,
|
||||
rate_period_sec=rate_period,
|
||||
in_bps=in_bps,
|
||||
out_bps=out_bps,
|
||||
in_util_pct=in_util,
|
||||
out_util_pct=out_util,
|
||||
)
|
||||
|
||||
|
||||
def brief_port_to_dict(row: BriefPort) -> dict[str, Any]:
|
||||
return {
|
||||
"ifname": row.ifname,
|
||||
|
|
@ -331,138 +120,137 @@ def detail_to_dict(row: DetailRates) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
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:
|
||||
"""Normalize Huawei tokens like up(s) / *down to up|down|''."""
|
||||
text = (token or "").strip().lower()
|
||||
if "administratively" in text:
|
||||
return "down"
|
||||
if text.startswith("*"):
|
||||
text = text[1:]
|
||||
m = _RE_UPDOWN_TOKEN.match(text)
|
||||
return m.group(1).lower() if m else ""
|
||||
if text.startswith("up"):
|
||||
return "up"
|
||||
if text.startswith("down"):
|
||||
return "down"
|
||||
return text if text in {"up", "down"} else ""
|
||||
|
||||
|
||||
def parse_huawei_interface_brief(text: str) -> list[BriefPort]:
|
||||
"""Parse Huawei `display interface brief` into port rows (BW ignored / 0)."""
|
||||
lines = (text or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||
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] = []
|
||||
started = False
|
||||
for raw in lines:
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
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
|
||||
low = line.lstrip().lower()
|
||||
if low.startswith("interface") and "phy" in low and ("inuti" in low or "protocol" in low):
|
||||
started = True
|
||||
continue
|
||||
if not started:
|
||||
continue
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("<") and stripped.endswith(">"):
|
||||
break
|
||||
if "#" in stripped and " " not in stripped.split("#", 1)[0]:
|
||||
break
|
||||
m = _RE_HW_BRIEF_ROW.match(stripped)
|
||||
if not m:
|
||||
continue
|
||||
ifname = m.group(1)
|
||||
phy = _norm_updown(m.group(2))
|
||||
prot = _norm_updown(m.group(3))
|
||||
if not ifname or ifname.lower() == "interface":
|
||||
continue
|
||||
if "#" in ifname or ">" in ifname:
|
||||
continue
|
||||
if not _RE_IFNAME.match(ifname):
|
||||
continue
|
||||
if not phy or not prot:
|
||||
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="",
|
||||
mode="",
|
||||
bw_raw="",
|
||||
bw_bps=0,
|
||||
admin=phy, # Huawei brief has no separate Admin; PHY is closest.
|
||||
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="",
|
||||
description=row_get(row, "description", "descrip", "desc"),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def parse_huawei_interface_detail(text: str) -> DetailRates:
|
||||
"""Parse Huawei `display interface {if}` Last N seconds rate / utility."""
|
||||
blob = text or ""
|
||||
ifname = ""
|
||||
admin_oper = ""
|
||||
m_state = _RE_HW_STATE.search(blob)
|
||||
if m_state:
|
||||
ifname = m_state.group(1)
|
||||
st = m_state.group(2).lower()
|
||||
admin_oper = "down" if "down" in st else "up"
|
||||
desc = ""
|
||||
m_desc = _RE_DESC.search(blob)
|
||||
if m_desc:
|
||||
desc = m_desc.group(1).strip()
|
||||
|
||||
rate_period = 0
|
||||
in_bps = 0.0
|
||||
out_bps = 0.0
|
||||
m_in = _RE_HW_IN_RATE.search(blob)
|
||||
if m_in:
|
||||
rate_period = int(m_in.group(1))
|
||||
in_bps = float(m_in.group(2))
|
||||
m_out = _RE_HW_OUT_RATE.search(blob)
|
||||
if m_out:
|
||||
if not rate_period:
|
||||
rate_period = int(m_out.group(1))
|
||||
out_bps = float(m_out.group(2))
|
||||
|
||||
in_util = 0.0
|
||||
out_util = 0.0
|
||||
m_iu = _RE_HW_IN_UTIL.search(blob)
|
||||
if m_iu:
|
||||
if not rate_period:
|
||||
rate_period = int(m_iu.group(1))
|
||||
in_util = float(m_iu.group(2))
|
||||
m_ou = _RE_HW_OUT_UTIL.search(blob)
|
||||
if m_ou:
|
||||
if not rate_period:
|
||||
rate_period = int(m_ou.group(1))
|
||||
out_util = float(m_ou.group(2))
|
||||
|
||||
return DetailRates(
|
||||
ifname=ifname,
|
||||
admin_oper=admin_oper,
|
||||
description=desc,
|
||||
bw_bps=0, # sample has no BW; leave 0 for now
|
||||
rate_period_sec=rate_period,
|
||||
in_bps=in_bps,
|
||||
out_bps=out_bps,
|
||||
in_util_pct=in_util,
|
||||
out_util_pct=out_util,
|
||||
)
|
||||
|
||||
|
||||
def parse_interface_brief(text: str, vendor_key: str = "zte") -> list[BriefPort]:
|
||||
key = str(vendor_key or "zte").strip().lower()
|
||||
if key == "huawei":
|
||||
return parse_huawei_interface_brief(text)
|
||||
if key == "cisco":
|
||||
return parse_cisco_interface_brief(text)
|
||||
return parse_zte_interface_brief(text)
|
||||
|
||||
|
||||
def parse_interface_detail(text: str, vendor_key: str = "zte") -> DetailRates:
|
||||
key = str(vendor_key or "zte").strip().lower()
|
||||
if key == "huawei":
|
||||
return parse_huawei_interface_detail(text)
|
||||
if key == "cisco":
|
||||
return parse_cisco_interface_detail(text)
|
||||
return parse_zte_interface_detail(text)
|
||||
|
||||
|
||||
def _cisco_period_to_sec(n: int, unit: str) -> int:
|
||||
def _period_to_sec(n: int, unit: str) -> int:
|
||||
u = (unit or "").lower()
|
||||
if u.startswith("second"):
|
||||
return int(n)
|
||||
|
|
@ -473,109 +261,149 @@ def _cisco_period_to_sec(n: int, unit: str) -> int:
|
|||
return int(n)
|
||||
|
||||
|
||||
def parse_cisco_interface_brief(text: str) -> list[BriefPort]:
|
||||
"""Parse Cisco `show ip interface brief` into port rows."""
|
||||
lines = (text or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||
out: list[BriefPort] = []
|
||||
started = False
|
||||
for raw in lines:
|
||||
line = raw.rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
low = line.lstrip().lower()
|
||||
if low.startswith("interface") and "status" in low and "protocol" in low:
|
||||
started = True
|
||||
continue
|
||||
if not started:
|
||||
continue
|
||||
stripped = line.strip()
|
||||
if stripped.endswith("#") or (stripped.endswith(">") and stripped.startswith("<")):
|
||||
break
|
||||
if "#" in stripped and " " not in stripped.split("#", 1)[0]:
|
||||
break
|
||||
m = _RE_CISCO_BRIEF_ROW.match(stripped)
|
||||
if not m:
|
||||
continue
|
||||
ifname = m.group(1)
|
||||
status = (m.group(5) or "").strip().lower()
|
||||
prot = (m.group(6) or "").strip().lower()
|
||||
if not _RE_IFNAME.match(ifname):
|
||||
continue
|
||||
if prot not in ("up", "down"):
|
||||
continue
|
||||
admin_down = "administratively" in status
|
||||
if "down" in status:
|
||||
phy = "down"
|
||||
elif "up" in status:
|
||||
phy = "up"
|
||||
else:
|
||||
continue
|
||||
admin = "down" if admin_down else phy
|
||||
out.append(
|
||||
BriefPort(
|
||||
ifname=ifname,
|
||||
attribute="",
|
||||
mode="",
|
||||
bw_raw="",
|
||||
bw_bps=0,
|
||||
admin=admin,
|
||||
phy=phy,
|
||||
prot=prot,
|
||||
description="",
|
||||
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",
|
||||
)
|
||||
)
|
||||
return out
|
||||
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_cisco_interface_detail(text: str) -> DetailRates:
|
||||
"""Parse Cisco `show interfaces {if}` BW + N minute/second rate (util ignored)."""
|
||||
blob = text or ""
|
||||
ifname = ""
|
||||
admin_oper = ""
|
||||
m_state = _RE_CISCO_IF_STATE.search(blob)
|
||||
if m_state:
|
||||
ifname = m_state.group(1)
|
||||
admin_oper = "down" if m_state.group(2) or m_state.group(3).lower() == "down" else "up"
|
||||
if not ifname:
|
||||
m_up = _RE_IF_UP.search(blob)
|
||||
if m_up:
|
||||
ifname = m_up.group(1)
|
||||
admin_oper = m_up.group(2).lower()
|
||||
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)
|
||||
|
||||
desc = ""
|
||||
m_desc = _RE_DESC.search(blob)
|
||||
if m_desc:
|
||||
desc = m_desc.group(1).strip()
|
||||
|
||||
bw_bps = 0
|
||||
for line in blob.splitlines():
|
||||
if "BW" in line.upper() and ("bit" in line.lower()):
|
||||
bw_bps = parse_bw_to_bps(line)
|
||||
if bw_bps:
|
||||
break
|
||||
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()
|
||||
|
||||
rate_period = 0
|
||||
in_bps = 0.0
|
||||
out_bps = 0.0
|
||||
for m in _RE_CISCO_RATE.finditer(blob):
|
||||
period = _cisco_period_to_sec(int(m.group(1)), m.group(2))
|
||||
direction = m.group(3).lower()
|
||||
rate = float(m.group(4))
|
||||
if not rate_period:
|
||||
rate_period = period
|
||||
if direction == "input":
|
||||
in_bps = rate
|
||||
else:
|
||||
out_bps = rate
|
||||
|
||||
return DetailRates(
|
||||
ifname=ifname,
|
||||
admin_oper=admin_oper,
|
||||
description=desc,
|
||||
bw_bps=bw_bps,
|
||||
rate_period_sec=rate_period,
|
||||
in_bps=in_bps,
|
||||
out_bps=out_bps,
|
||||
in_util_pct=0.0, # sample has no util %; ignore for now
|
||||
out_util_pct=0.0,
|
||||
)
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue