mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 17:20:44 +08:00
Allow topology discovery to scan UME nodes on the map.
Previously only managed NEs were scanned, so UME-only maps reported Scanned 0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bf82a01a03
commit
ec736b6dc8
4 changed files with 122 additions and 47 deletions
|
|
@ -9,6 +9,7 @@ from uuid import uuid4
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from .cli_resolve import get_default_profile, infer_device_type_vendor
|
||||||
from .models import ManagedNE, TopologyEdge, TopologyMap, TopologyNode, UmeInventoryNE
|
from .models import ManagedNE, TopologyEdge, TopologyMap, TopologyNode, UmeInventoryNE
|
||||||
from .ne_exec import execute_managed_ne_commands
|
from .ne_exec import execute_managed_ne_commands
|
||||||
from .topology_lldp import NeighborHit, parse_neighbor_output, pick_neighbor_command
|
from .topology_lldp import NeighborHit, parse_neighbor_output, pick_neighbor_command
|
||||||
|
|
@ -278,6 +279,7 @@ def _match_neighbor_to_node(
|
||||||
*,
|
*,
|
||||||
nodes: list[TopologyNode],
|
nodes: list[TopologyNode],
|
||||||
nes: dict[str, ManagedNE],
|
nes: dict[str, ManagedNE],
|
||||||
|
umes: dict[str, UmeInventoryNE],
|
||||||
self_node_id: str,
|
self_node_id: str,
|
||||||
) -> TopologyNode | None:
|
) -> TopologyNode | None:
|
||||||
name_key = _norm_key(hit.remote_name)
|
name_key = _norm_key(hit.remote_name)
|
||||||
|
|
@ -286,17 +288,20 @@ def _match_neighbor_to_node(
|
||||||
if n.id == self_node_id:
|
if n.id == self_node_id:
|
||||||
continue
|
continue
|
||||||
ne = nes.get(str(n.managed_ne_id or ""))
|
ne = nes.get(str(n.managed_ne_id or ""))
|
||||||
|
ume = umes.get(str(n.ume_ne_id or ""))
|
||||||
candidates = [
|
candidates = [
|
||||||
_norm_key(n.label or ""),
|
_norm_key(n.label or ""),
|
||||||
_norm_key(ne.name if ne else ""),
|
_norm_key(ne.name if ne else ""),
|
||||||
str(ne.ip_address if ne else "").strip(),
|
str(ne.ip_address if ne else "").strip(),
|
||||||
|
_norm_key(ume.host_name if ume else ""),
|
||||||
|
_norm_key(ume.ne_name if ume else ""),
|
||||||
|
_norm_key(ume.user_label if ume else ""),
|
||||||
|
str(ume.ip_address if ume else "").strip(),
|
||||||
]
|
]
|
||||||
if ip_key and ip_key in candidates:
|
cand_set = {_norm_key(c) for c in candidates if str(c or "").strip()}
|
||||||
|
if ip_key and ip_key in {str(c).strip() for c in candidates if str(c or "").strip()}:
|
||||||
return n
|
return n
|
||||||
if name_key and name_key in {_norm_key(c) for c in candidates if c}:
|
if name_key and name_key in cand_set:
|
||||||
return n
|
|
||||||
# Also match managed NE name without case
|
|
||||||
if ne and name_key and name_key == _norm_key(ne.name):
|
|
||||||
return n
|
return n
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -307,6 +312,47 @@ def _edge_pair_key(a: str, b: str, local_port: str, remote_port: str) -> tuple[s
|
||||||
return (b, a, remote_port, local_port)
|
return (b, a, remote_port, local_port)
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_target_for_node(
|
||||||
|
n: TopologyNode,
|
||||||
|
*,
|
||||||
|
nes: dict[str, ManagedNE],
|
||||||
|
umes: dict[str, UmeInventoryNE],
|
||||||
|
filter_ids: set[str],
|
||||||
|
default_profile,
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
"""Resolve CLI target for a topology node (managed preferred, else UME)."""
|
||||||
|
mid = str(n.managed_ne_id or "").strip()
|
||||||
|
uid = str(n.ume_ne_id or "").strip()
|
||||||
|
if filter_ids and mid not in filter_ids and uid not in filter_ids:
|
||||||
|
return None
|
||||||
|
if mid and mid in nes:
|
||||||
|
ne = nes[mid]
|
||||||
|
return {
|
||||||
|
"ne_id": ne.id,
|
||||||
|
"ume_ne_id": "",
|
||||||
|
"ne_name": ne.name or "",
|
||||||
|
"ne_ip": ne.ip_address or "",
|
||||||
|
"vendor": ne.vendor or "",
|
||||||
|
"device_type": ne.device_type or "",
|
||||||
|
}
|
||||||
|
if uid and uid in umes:
|
||||||
|
ume = umes[uid]
|
||||||
|
if default_profile is not None:
|
||||||
|
dtype, vendor = infer_device_type_vendor(str(ume.ne_type or ""), default_profile)
|
||||||
|
else:
|
||||||
|
dtype, vendor = "zte_zxros", (ume.vendor or "ZTE")
|
||||||
|
name = (ume.host_name or ume.ne_name or ume.user_label or ume.ip_address or uid).strip()
|
||||||
|
return {
|
||||||
|
"ne_id": uid,
|
||||||
|
"ume_ne_id": uid,
|
||||||
|
"ne_name": name,
|
||||||
|
"ne_ip": ume.ip_address or "",
|
||||||
|
"vendor": vendor or (ume.vendor or "ZTE"),
|
||||||
|
"device_type": dtype or "zte_zxros",
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def iter_discover_neighbors(
|
def iter_discover_neighbors(
|
||||||
db: Session,
|
db: Session,
|
||||||
map_id: str,
|
map_id: str,
|
||||||
|
|
@ -317,15 +363,17 @@ def iter_discover_neighbors(
|
||||||
nodes = db.query(TopologyNode).filter(TopologyNode.map_id == row.id).all()
|
nodes = db.query(TopologyNode).filter(TopologyNode.map_id == row.id).all()
|
||||||
edges = db.query(TopologyEdge).filter(TopologyEdge.map_id == row.id).all()
|
edges = db.query(TopologyEdge).filter(TopologyEdge.map_id == row.id).all()
|
||||||
nes = _ne_lookup(db, {str(n.managed_ne_id or "") for n in nodes if n.managed_ne_id})
|
nes = _ne_lookup(db, {str(n.managed_ne_id or "") for n in nodes if n.managed_ne_id})
|
||||||
|
umes = _ume_lookup(db, {str(n.ume_ne_id or "") for n in nodes if n.ume_ne_id})
|
||||||
|
default_profile = get_default_profile(db)
|
||||||
|
|
||||||
filter_ids = {str(x).strip() for x in (body.ne_ids or []) if str(x).strip()}
|
filter_ids = {str(x).strip() for x in (body.ne_ids or []) if str(x).strip()}
|
||||||
scan_nodes = [
|
scan_targets: list[tuple[TopologyNode, dict[str, str]]] = []
|
||||||
n
|
for n in nodes:
|
||||||
for n in nodes
|
target = _discover_target_for_node(
|
||||||
if n.managed_ne_id
|
n, nes=nes, umes=umes, filter_ids=filter_ids, default_profile=default_profile
|
||||||
and n.managed_ne_id in nes
|
)
|
||||||
and (not filter_ids or n.managed_ne_id in filter_ids)
|
if target is not None:
|
||||||
]
|
scan_targets.append((n, target))
|
||||||
|
|
||||||
existing: dict[tuple[str, str, str, str], TopologyEdge] = {}
|
existing: dict[tuple[str, str, str, str], TopologyEdge] = {}
|
||||||
for e in edges:
|
for e in edges:
|
||||||
|
|
@ -343,7 +391,7 @@ def iter_discover_neighbors(
|
||||||
stale_count = 0
|
stale_count = 0
|
||||||
now = _utcnow()
|
now = _utcnow()
|
||||||
proto_req = str(body.protocol or "auto").strip().lower() or "auto"
|
proto_req = str(body.protocol or "auto").strip().lower() or "auto"
|
||||||
total = len(scan_nodes)
|
total = len(scan_targets)
|
||||||
touched_edge_ids: set[str] = set()
|
touched_edge_ids: set[str] = set()
|
||||||
scanned_ok_node_ids: set[str] = set()
|
scanned_ok_node_ids: set[str] = set()
|
||||||
|
|
||||||
|
|
@ -354,28 +402,25 @@ def iter_discover_neighbors(
|
||||||
"total": total,
|
"total": total,
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, n in enumerate(scan_nodes, start=1):
|
for index, (n, target) in enumerate(scan_targets, start=1):
|
||||||
ne = nes.get(n.managed_ne_id)
|
|
||||||
if ne is None:
|
|
||||||
continue
|
|
||||||
yield {
|
yield {
|
||||||
"type": "ne_start",
|
"type": "ne_start",
|
||||||
"index": index,
|
"index": index,
|
||||||
"total": total,
|
"total": total,
|
||||||
"ne_id": ne.id,
|
"ne_id": target["ne_id"],
|
||||||
"ne_name": ne.name or "",
|
"ne_name": target["ne_name"],
|
||||||
"ne_ip": ne.ip_address or "",
|
"ne_ip": target["ne_ip"],
|
||||||
}
|
}
|
||||||
cmd, proto_tag = pick_neighbor_command(
|
cmd, proto_tag = pick_neighbor_command(
|
||||||
protocol=proto_req,
|
protocol=proto_req,
|
||||||
vendor=ne.vendor or "",
|
vendor=target["vendor"],
|
||||||
device_type=ne.device_type or "",
|
device_type=target["device_type"],
|
||||||
)
|
)
|
||||||
if not cmd:
|
if not cmd:
|
||||||
result = TopologyDiscoverNeResult(
|
result = TopologyDiscoverNeResult(
|
||||||
ne_id=ne.id,
|
ne_id=target["ne_id"],
|
||||||
ne_name=ne.name or "",
|
ne_name=target["ne_name"],
|
||||||
ne_ip=ne.ip_address or "",
|
ne_ip=target["ne_ip"],
|
||||||
ok=False,
|
ok=False,
|
||||||
error="no_command_for_vendor",
|
error="no_command_for_vendor",
|
||||||
)
|
)
|
||||||
|
|
@ -390,17 +435,39 @@ def iter_discover_neighbors(
|
||||||
"edges_stale": stale_count,
|
"edges_stale": stale_count,
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
exec_out = execute_managed_ne_commands(
|
|
||||||
db,
|
exec_kwargs: dict[str, Any] = {"read_timeout_sec": 60}
|
||||||
[cmd],
|
if target["ume_ne_id"] and not str(n.managed_ne_id or "").strip():
|
||||||
ne_id=ne.id,
|
exec_kwargs["ume_ne_id"] = target["ume_ne_id"]
|
||||||
read_timeout_sec=60,
|
else:
|
||||||
)
|
exec_kwargs["ne_id"] = target["ne_id"]
|
||||||
|
try:
|
||||||
|
exec_out = execute_managed_ne_commands(db, [cmd], **exec_kwargs)
|
||||||
|
except HTTPException as exc:
|
||||||
|
result = TopologyDiscoverNeResult(
|
||||||
|
ne_id=target["ne_id"],
|
||||||
|
ne_name=target["ne_name"],
|
||||||
|
ne_ip=target["ne_ip"],
|
||||||
|
ok=False,
|
||||||
|
command=cmd,
|
||||||
|
error=str(exc.detail or "exec_failed")[:500],
|
||||||
|
)
|
||||||
|
results.append(result)
|
||||||
|
yield {
|
||||||
|
"type": "ne_result",
|
||||||
|
"index": index,
|
||||||
|
"total": total,
|
||||||
|
"result": result.model_dump(mode="json"),
|
||||||
|
"edges_added": added,
|
||||||
|
"edges_updated": updated,
|
||||||
|
"edges_stale": stale_count,
|
||||||
|
}
|
||||||
|
continue
|
||||||
if not exec_out.get("ok"):
|
if not exec_out.get("ok"):
|
||||||
result = TopologyDiscoverNeResult(
|
result = TopologyDiscoverNeResult(
|
||||||
ne_id=ne.id,
|
ne_id=target["ne_id"],
|
||||||
ne_name=ne.name or "",
|
ne_name=target["ne_name"],
|
||||||
ne_ip=ne.ip_address or "",
|
ne_ip=target["ne_ip"],
|
||||||
ok=False,
|
ok=False,
|
||||||
command=cmd,
|
command=cmd,
|
||||||
error=str(exec_out.get("detail") or exec_out.get("error") or "exec_failed")[:500],
|
error=str(exec_out.get("detail") or exec_out.get("error") or "exec_failed")[:500],
|
||||||
|
|
@ -422,14 +489,14 @@ def iter_discover_neighbors(
|
||||||
hits = parse_neighbor_output(
|
hits = parse_neighbor_output(
|
||||||
raw,
|
raw,
|
||||||
protocol=proto_tag,
|
protocol=proto_tag,
|
||||||
vendor=ne.vendor or "",
|
vendor=target["vendor"],
|
||||||
device_type=ne.device_type or "",
|
device_type=target["device_type"],
|
||||||
)
|
)
|
||||||
ne_added = 0
|
ne_added = 0
|
||||||
ne_updated = 0
|
ne_updated = 0
|
||||||
for hit in hits:
|
for hit in hits:
|
||||||
peer = _match_neighbor_to_node(
|
peer = _match_neighbor_to_node(
|
||||||
hit, nodes=nodes, nes=nes, self_node_id=n.id
|
hit, nodes=nodes, nes=nes, umes=umes, self_node_id=n.id
|
||||||
)
|
)
|
||||||
if peer is None:
|
if peer is None:
|
||||||
continue
|
continue
|
||||||
|
|
@ -469,9 +536,9 @@ def iter_discover_neighbors(
|
||||||
added += 1
|
added += 1
|
||||||
|
|
||||||
result = TopologyDiscoverNeResult(
|
result = TopologyDiscoverNeResult(
|
||||||
ne_id=ne.id,
|
ne_id=target["ne_id"],
|
||||||
ne_name=ne.name or "",
|
ne_name=target["ne_name"],
|
||||||
ne_ip=ne.ip_address or "",
|
ne_ip=target["ne_ip"],
|
||||||
ok=True,
|
ok=True,
|
||||||
command=cmd,
|
command=cmd,
|
||||||
neighbors=len(hits),
|
neighbors=len(hits),
|
||||||
|
|
|
||||||
|
|
@ -647,6 +647,8 @@ const en = {
|
||||||
discoverSummary: "Scanned {{scanned}} · added {{added}} · updated {{updated}} · missing {{stale}} · failed {{failed}}",
|
discoverSummary: "Scanned {{scanned}} · added {{added}} · updated {{updated}} · missing {{stale}} · failed {{failed}}",
|
||||||
discoverNeOk: "neighbors {{neighbors}} · added {{added}} · updated {{updated}}",
|
discoverNeOk: "neighbors {{neighbors}} · added {{added}} · updated {{updated}}",
|
||||||
discoverNeFail: "Collect failed",
|
discoverNeFail: "Collect failed",
|
||||||
|
discoverNoTargets:
|
||||||
|
"No scannable NEs on the map. Add managed NEs, or UME NEs with a CLI connect profile.",
|
||||||
removeStale: "Clear missing ({{count}})",
|
removeStale: "Clear missing ({{count}})",
|
||||||
removeStaleHint: "Delete red-marked missing edges",
|
removeStaleHint: "Delete red-marked missing edges",
|
||||||
staleRemoved: "Removed {{count}} missing edge(s)",
|
staleRemoved: "Removed {{count}} missing edge(s)",
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,7 @@ const zh = {
|
||||||
discoverSummary: "扫描 {{scanned}} 台 · 新增 {{added}} · 更新 {{updated}} · 未发现 {{stale}} · 失败 {{failed}}",
|
discoverSummary: "扫描 {{scanned}} 台 · 新增 {{added}} · 更新 {{updated}} · 未发现 {{stale}} · 失败 {{failed}}",
|
||||||
discoverNeOk: "邻居 {{neighbors}} · 新增 {{added}} · 更新 {{updated}}",
|
discoverNeOk: "邻居 {{neighbors}} · 新增 {{added}} · 更新 {{updated}}",
|
||||||
discoverNeFail: "采集失败",
|
discoverNeFail: "采集失败",
|
||||||
|
discoverNoTargets: "画布上没有可扫描网元。请添加托管网元,或添加已配置 CLI 凭据的 UME 网元。",
|
||||||
removeStale: "清除未发现 ({{count}})",
|
removeStale: "清除未发现 ({{count}})",
|
||||||
removeStaleHint: "删除红色标记的未发现链路",
|
removeStaleHint: "删除红色标记的未发现链路",
|
||||||
staleRemoved: "已删除 {{count}} 条未发现链路",
|
staleRemoved: "已删除 {{count}} 条未发现链路",
|
||||||
|
|
|
||||||
|
|
@ -995,15 +995,20 @@ export function TopologyPage() {
|
||||||
</div>
|
</div>
|
||||||
{discovering ? (
|
{discovering ? (
|
||||||
<p className="panel__hint panel__hint--live">
|
<p className="panel__hint panel__hint--live">
|
||||||
{t("topology.discoverProgressLive")
|
{discoverProgress.total <= 0
|
||||||
.replace("{{index}}", String(discoverProgress.index))
|
? t("topology.discoverNoTargets")
|
||||||
.replace("{{total}}", String(discoverProgress.total))
|
: t("topology.discoverProgressLive")
|
||||||
.replace(
|
.replace("{{index}}", String(discoverProgress.index))
|
||||||
"{{name}}",
|
.replace("{{total}}", String(discoverProgress.total))
|
||||||
discoverProgress.neName || discoverProgress.neIp || "…",
|
.replace(
|
||||||
)}
|
"{{name}}",
|
||||||
|
discoverProgress.neName || discoverProgress.neIp || "…",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!discovering && discoverReport && discoverReport.scanned === 0 ? (
|
||||||
|
<p className="panel__hint">{t("topology.discoverNoTargets")}</p>
|
||||||
|
) : null}
|
||||||
{discoverError ? <p className="topo-discover__error">{discoverError}</p> : null}
|
{discoverError ? <p className="topo-discover__error">{discoverError}</p> : null}
|
||||||
{discoverResults.length > 0 || discoverReport ? (
|
{discoverResults.length > 0 || discoverReport ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue