Fix ZTE LLDP Scope table parse and skip discover without vendor.

Match ZXROS Scope/Holdtime columns, disable Netmiko cmd_verify on show commands, and do not SSH LLDP when vendor/device_type cannot be resolved.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-02 23:26:28 +08:00
parent 393fd0e0b4
commit d2fb33ec0d
9 changed files with 146 additions and 19 deletions

View file

@ -1,15 +1,32 @@
Value LOCAL_INTERFACE (\S+)
Value SCOPE (\S+)
Value CHASSIS_ID (\S+)
Value NEIGHBOR_PORT_ID (\S+)
Value NEIGHBOR_NAME (\S+.*)
Value HOLDTIME (\d+)
Value NEIGHBOR_NAME (\S.*?)
Start
^Local\s+Interface\s+Chassis\s+ID -> Neighbors
^Local\s+Interface\s+Scope\s+Chassis\s+ID -> Neighbors
^Local\s+Interface\s+Chassis\s+ID -> NeighborsSimple
^Total\s+neighbors
^-+
^\s*$$
^.* -> Start
Neighbors
^${LOCAL_INTERFACE}\s+${CHASSIS_ID}\s+${NEIGHBOR_PORT_ID}\s+${NEIGHBOR_NAME}\s*$$ -> Record
^${LOCAL_INTERFACE}\s+${SCOPE}\s+${CHASSIS_ID}\s+${NEIGHBOR_PORT_ID}\s+${HOLDTIME}\s+${NEIGHBOR_NAME}\s*$$ -> Record
^-+
^Total\s+neighbors
^\s*$$
^Local\s+Interface -> Neighbors
^.* -> Start
^\S+\s*#\s*$$ -> End
^.*#\s*$$ -> End
^.* -> Neighbors
NeighborsSimple
^${LOCAL_INTERFACE}\s+${CHASSIS_ID}\s+${NEIGHBOR_PORT_ID}\s+${NEIGHBOR_NAME}\s*$$ -> Record
^-+
^Total\s+neighbors
^\s*$$
^\S+\s*#\s*$$ -> End
^.*#\s*$$ -> End
^.* -> NeighborsSimple

View file

@ -38,8 +38,22 @@ def send_show_command(conn: Any, command: str, *, read_timeout: int = 120) -> st
Do not use ``send_command_timing`` for Cisco config collection: long idle during
``Building configuration...`` is treated as end-of-output and truncates the config.
``cmd_verify=False``: Netmiko's default echo check often raises
``Pattern not detected: 'show\\ lldp\\ ...'`` on IOSv / hop / slow echo paths.
"""
cmd = str(command or "").strip()
if not cmd:
return ""
return str(conn.send_command(command_string=cmd, read_timeout=read_timeout) or "")
try:
return str(
conn.send_command(
command_string=cmd,
read_timeout=read_timeout,
cmd_verify=False,
)
or ""
)
except TypeError:
# Older Netmiko without cmd_verify kwarg.
return str(conn.send_command(command_string=cmd, read_timeout=read_timeout) or "")

View file

@ -23,7 +23,13 @@ from .topology_fabric import (
ensure_fabric_node_for_ume,
upsert_fabric_edge,
)
from .topology_lldp import NeighborHit, parse_neighbor_output, parser_meta, pick_neighbor_command
from .topology_lldp import (
NeighborHit,
can_discover_lldp,
parse_neighbor_output,
parser_meta,
pick_neighbor_command,
)
def _discover_one_target(
@ -70,10 +76,21 @@ def _discover_one_target(
# Release unique-index locks before slow SSH.
db.commit()
cmd, _proto = pick_neighbor_command(
vendor=target.get("vendor") or "",
device_type=target.get("device_type") or "",
)
vendor = target.get("vendor") or ""
device_type = target.get("device_type") or ""
pkey, is_stub = parser_meta(vendor=vendor, device_type=device_type)
if not can_discover_lldp(vendor=vendor, device_type=device_type):
return {
**base,
"ok": False,
"command": "",
"parser_key": pkey,
"parser_stub": True,
"error": "vendor_or_device_type_required",
"raw_preview": "",
}
cmd, _proto = pick_neighbor_command(vendor=vendor, device_type=device_type)
exec_kwargs: dict[str, Any] = {"read_timeout_sec": 60}
if target.get("ume_ne_id") and not db.get(ManagedNE, target["ne_id"]):
exec_kwargs["ume_ne_id"] = target["ume_ne_id"]
@ -102,14 +119,12 @@ def _discover_one_target(
}
raw = str(exec_out.get("output") or "")
pkey, is_stub = parser_meta(
vendor=target.get("vendor") or "", device_type=target.get("device_type") or ""
)
hits = parse_neighbor_output(
raw,
protocol="lldp",
vendor=target.get("vendor") or "",
device_type=target.get("device_type") or "",
vendor=vendor,
device_type=device_type,
command=cmd,
)
stub_flag = bool(is_stub and raw.strip() and not hits)

View file

@ -159,6 +159,11 @@ def pick_neighbor_command(
return lldp_command_for_vendor(vendor, device_type), "lldp"
def can_discover_lldp(*, vendor: str = "", device_type: str = "") -> bool:
"""False when inventory has no usable vendor/device_type (do not SSH a guess command)."""
return resolve_vendor_key(vendor, device_type) != "generic"
def parser_meta(*, vendor: str = "", device_type: str = "") -> tuple[str, bool]:
"""Return (parser_key, is_stub)."""
key = resolve_vendor_key(vendor, device_type)
@ -202,6 +207,17 @@ def _map_lldp_rows(rows: list[dict[str, Any]]) -> list[NeighborHit]:
return hits
def _strip_collect_wrapper(text: str) -> str:
"""Drop SecureCRT-style headers injected by ``_collect_on_device``."""
lines: list[str] = []
for ln in str(text or "").splitlines():
s = ln.strip()
if s.startswith(">>>") and '"String"' in s:
continue
lines.append(ln)
return "\n".join(lines)
def _parse_lldp_via_ntc(
text: str,
*,
@ -226,15 +242,18 @@ def parse_neighbor_output(
command: str = "",
) -> list[NeighborHit]:
"""Parse neighbor CLI via TextFSM only (custom then community)."""
raw = str(text or "")
raw = _strip_collect_wrapper(text)
if not raw.strip():
return []
_ = protocol # CDP discovery removed; always parse as LLDP
if not can_discover_lldp(vendor=vendor, device_type=device_type):
return []
cmd = (command or "").strip() or lldp_command_for_vendor(vendor, device_type)
return _parse_lldp_via_ntc(
raw,
vendor=vendor,
device_type=device_type,
command=command or lldp_command_for_vendor(vendor, device_type),
command=cmd,
)