mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 22:20:58 +08:00
Paginate LLDP job items, surface port-traffic raw logs with a 300s default, harden topology discover/edit/zoom/fullscreen, and join mid-token CLI wraps before TextFSM. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
3.8 KiB
Python
124 lines
3.8 KiB
Python
"""Pre-TextFSM CLI wrap-line joiner (vendor-agnostic).
|
|
|
|
Many vendor CLIs wrap the last column (description / system name) onto the next
|
|
line with leading spaces. Pure TextFSM cannot reliably merge those mid-token wraps
|
|
without creating duplicate or incomplete records — especially when the wrap
|
|
breaks inside a hostname (``AL5458-ACC-612`` + ``0HS``).
|
|
|
|
This module flattens matching wrap lines **before** TextFSM runs. Rules are
|
|
registered per platform + command so indented multi-field blocks (e.g. Huawei
|
|
LLDP detail) are left alone.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CliWrapRule:
|
|
"""Join indented continuation lines into the previous table row."""
|
|
|
|
id: str
|
|
platforms: frozenset[str]
|
|
# Match ntc / NetX command string (already the resolved show/display command).
|
|
command: re.Pattern[str]
|
|
# A data row that may be continued (typically starts with interface name).
|
|
record_line: re.Pattern[str]
|
|
# Continuation fragment: leading whitespace + non-empty payload.
|
|
cont_line: re.Pattern[str]
|
|
# ZTE column wraps usually split mid-token → join with "" ; use " " if needed.
|
|
join_sep: str = ""
|
|
# Lines that must never be treated as records or continuations.
|
|
skip_line: re.Pattern[str] | None = None
|
|
|
|
|
|
# ZTE ZXROS table rows: interface-like token at column 0.
|
|
_ZTE_RECORD = re.compile(r"^(?P<head>[A-Za-z][\w./:-]*)\s+\S+")
|
|
_ZTE_CONT = re.compile(r"^\s{8,}(?P<tail>\S.*?)\s*$")
|
|
_ZTE_SKIP = re.compile(
|
|
r"^(?:"
|
|
r"-{3,}"
|
|
r"|Local\s+Interface\b"
|
|
r"|Interface\s+\S+\s+Mode\b"
|
|
r"|Interface\s+Attribute\b"
|
|
r"|Scope\s+codes\s*:"
|
|
r"|Total\s+neighbors\b"
|
|
r"|NB\s+=\s+"
|
|
r"|NC\s+=\s+"
|
|
r"|NTPMR\s+=\s+"
|
|
r"|PHY:\s*"
|
|
r"|.*#\s*$"
|
|
r")",
|
|
re.I,
|
|
)
|
|
|
|
CLI_WRAP_RULES: tuple[CliWrapRule, ...] = (
|
|
CliWrapRule(
|
|
id="zte_zxros_show_interface_brief",
|
|
platforms=frozenset({"zte_zxros"}),
|
|
command=re.compile(r"^show\s+interface\s+brief\b", re.I),
|
|
record_line=_ZTE_RECORD,
|
|
cont_line=_ZTE_CONT,
|
|
join_sep="",
|
|
skip_line=_ZTE_SKIP,
|
|
),
|
|
CliWrapRule(
|
|
id="zte_zxros_show_lldp_neighbor_brief",
|
|
platforms=frozenset({"zte_zxros"}),
|
|
command=re.compile(r"^show\s+lldp\s+neighbor(?:s)?\s+brief\b", re.I),
|
|
record_line=_ZTE_RECORD,
|
|
cont_line=_ZTE_CONT,
|
|
join_sep="",
|
|
skip_line=_ZTE_SKIP,
|
|
),
|
|
)
|
|
|
|
|
|
def _rules_for(*, platform: str, command: str) -> list[CliWrapRule]:
|
|
plat = str(platform or "").strip().lower()
|
|
cmd = str(command or "").strip()
|
|
if not plat or not cmd:
|
|
return []
|
|
return [
|
|
rule
|
|
for rule in CLI_WRAP_RULES
|
|
if plat in rule.platforms and rule.command.search(cmd)
|
|
]
|
|
|
|
|
|
def _is_skip(line: str, rule: CliWrapRule) -> bool:
|
|
if not rule.skip_line:
|
|
return False
|
|
if rule.skip_line.match(line):
|
|
return True
|
|
stripped = line.strip()
|
|
return bool(stripped and rule.skip_line.match(stripped))
|
|
|
|
|
|
def _apply_rule(text: str, rule: CliWrapRule) -> str:
|
|
lines = str(text or "").splitlines()
|
|
if not lines:
|
|
return str(text or "")
|
|
out: list[str] = []
|
|
for ln in lines:
|
|
if _is_skip(ln, rule):
|
|
out.append(ln)
|
|
continue
|
|
cont = rule.cont_line.match(ln)
|
|
if cont and out:
|
|
prev = out[-1]
|
|
if rule.record_line.match(prev) and not _is_skip(prev, rule):
|
|
out[-1] = f"{prev.rstrip()}{rule.join_sep}{cont.group('tail')}"
|
|
continue
|
|
out.append(ln)
|
|
return "\n".join(out)
|
|
|
|
|
|
def apply_cli_wrap(text: str, *, platform: str = "", command: str = "") -> str:
|
|
"""Return text with registered wrap continuations flattened for TextFSM."""
|
|
raw = str(text or "")
|
|
for rule in _rules_for(platform=platform, command=command):
|
|
raw = _apply_rule(raw, rule)
|
|
return raw
|