mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:50:46 +08:00
Add LLDP collect page with conservative fabric edge lifecycle.
Move scheduled discovery under Network → LLDP links, mark absent edges missing after one successful scan, purge only after four miss cycles, and expose unmatched/raw job detail. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9674de221d
commit
d05ba0f1f7
30 changed files with 4713 additions and 1870 deletions
|
|
@ -36,11 +36,13 @@ python -m netx_mcp
|
|||
|
||||
[`mcp.json`](./mcp.json) — `command: python`,`args: ["-m", "netx_mcp"]`,`env` 见文件。
|
||||
|
||||
## 工具(12)
|
||||
## 工具(21)
|
||||
|
||||
UME:`queryUmeAlarms`, `aggregateUmeAlarms`, `runUmeDiagnostics`, `queryUmeNeInventory`, `getUmeNe`, `queryUmeAlarmsRaw`, `aggregateUmeAlarmsRaw`, `listUmeAlarmFields`, `sqlQueryUme`
|
||||
|
||||
托管网元:`listManagedNe`, `getManagedNe`, `execManagedNe`
|
||||
托管网元:`listManagedNe`, `getManagedNe`, `execManagedNe`, `listCliTargets`
|
||||
|
||||
拓扑 Fabric:`getTopologySummary`, `queryTopologyNodes`, `queryTopologyEdges`, `getTopologyNeighborhood`, `runLldpDiscover`, `getLldpDiscoverJob`, `listTopologyViews`, `getTopologyView`
|
||||
|
||||
## 兼容
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""MCP tool schemas and HTTP-backed handlers (12 tools: UME + managed NE; no Excel import batch)."""
|
||||
"""MCP tool schemas and HTTP-backed handlers (UME + managed NE + topology fabric)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -285,6 +285,81 @@ def _list_cli_targets(args: dict[str, Any]) -> dict[str, Any]:
|
|||
return http_json("GET", "/v1/cli/targets", params=params)
|
||||
|
||||
|
||||
def _get_topology_summary(args: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = args
|
||||
return http_json("GET", "/v1/topology/fabric/summary", params=None)
|
||||
|
||||
|
||||
def _query_topology_nodes(args: dict[str, Any]) -> dict[str, Any]:
|
||||
page = max(1, int(args.get("page") or 1))
|
||||
page_size = min(500, max(1, int(args.get("page_size") or 50)))
|
||||
params: dict[str, Any] = {"page": page, "page_size": page_size}
|
||||
if str(args.get("keyword") or "").strip():
|
||||
params["keyword"] = str(args.get("keyword")).strip()
|
||||
return http_json("GET", "/v1/topology/fabric/nodes", params=params)
|
||||
|
||||
|
||||
def _query_topology_edges(args: dict[str, Any]) -> dict[str, Any]:
|
||||
page = max(1, int(args.get("page") or 1))
|
||||
page_size = min(500, max(1, int(args.get("page_size") or 50)))
|
||||
params: dict[str, Any] = {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"layer": str(args.get("layer") or "physical").strip() or "physical",
|
||||
}
|
||||
if str(args.get("node_id") or "").strip():
|
||||
params["node_id"] = str(args.get("node_id")).strip()
|
||||
if str(args.get("status") or "").strip():
|
||||
params["status"] = str(args.get("status")).strip()
|
||||
if str(args.get("source") or "").strip():
|
||||
params["source"] = str(args.get("source")).strip()
|
||||
return http_json("GET", "/v1/topology/fabric/edges", params=params)
|
||||
|
||||
|
||||
def _get_topology_neighborhood(args: dict[str, Any]) -> dict[str, Any]:
|
||||
node_id = str(args.get("node_id") or "").strip()
|
||||
if not node_id:
|
||||
return {"ok": False, "error": "node_id_required", "error_code": "node_id_required"}
|
||||
params: dict[str, Any] = {
|
||||
"node_id": node_id,
|
||||
"depth": max(1, min(3, int(args.get("depth") or 1))),
|
||||
"layer": str(args.get("layer") or "physical").strip() or "physical",
|
||||
}
|
||||
return http_json("GET", "/v1/topology/fabric/neighborhood", params=params)
|
||||
|
||||
|
||||
def _run_lldp_discover(args: dict[str, Any]) -> dict[str, Any]:
|
||||
scope = str(args.get("scope") or "ne_ids").strip() or "ne_ids"
|
||||
body: dict[str, Any] = {
|
||||
"scope": scope,
|
||||
"auto_add_unmatched": bool(args.get("auto_add_unmatched") or False),
|
||||
"concurrency": max(1, min(32, int(args.get("concurrency") or 4))),
|
||||
}
|
||||
ne_ids = args.get("ne_ids")
|
||||
if isinstance(ne_ids, list):
|
||||
body["ne_ids"] = [str(x).strip() for x in ne_ids if str(x).strip()]
|
||||
return http_post_json("/v1/topology/fabric/discover", body, timeout=60.0)
|
||||
|
||||
|
||||
def _get_lldp_discover_job(args: dict[str, Any]) -> dict[str, Any]:
|
||||
job_id = str(args.get("job_id") or "").strip()
|
||||
if not job_id:
|
||||
return {"ok": False, "error": "job_id_required", "error_code": "job_id_required"}
|
||||
return http_json("GET", f"/v1/topology/fabric/discover/{job_id}", params=None)
|
||||
|
||||
|
||||
def _list_topology_views(args: dict[str, Any]) -> dict[str, Any]:
|
||||
_ = args
|
||||
return http_json("GET", "/v1/topology/views", params=None)
|
||||
|
||||
|
||||
def _get_topology_view(args: dict[str, Any]) -> dict[str, Any]:
|
||||
view_id = str(args.get("view_id") or "").strip()
|
||||
if not view_id:
|
||||
return {"ok": False, "error": "view_id_required", "error_code": "view_id_required"}
|
||||
return http_json("GET", f"/v1/topology/views/{view_id}", params=None)
|
||||
|
||||
|
||||
HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "queryUmeAlarms",
|
||||
|
|
@ -469,6 +544,96 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "getTopologySummary",
|
||||
"description": "Fabric topology summary: node/edge counts (active/stale) and last LLDP discover time.",
|
||||
"inputSchema": {"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
||||
},
|
||||
{
|
||||
"name": "queryTopologyNodes",
|
||||
"description": "Page fabric topology nodes (keyword optional). Never dumps full fabric.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keyword": {"type": "string"},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"page_size": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "queryTopologyEdges",
|
||||
"description": "Page fabric edges (physical layer by default); filter by node_id/status/source.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_id": {"type": "string"},
|
||||
"layer": {"type": "string", "default": "physical"},
|
||||
"status": {"type": "string", "enum": ["active", "stale"]},
|
||||
"source": {"type": "string", "enum": ["lldp", "manual", "stale"]},
|
||||
"page": {"type": "integer", "minimum": 1, "default": 1},
|
||||
"page_size": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "getTopologyNeighborhood",
|
||||
"description": "Get fabric neighborhood around a node (depth 1-3).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_id": {"type": "string"},
|
||||
"depth": {"type": "integer", "minimum": 1, "maximum": 3, "default": 1},
|
||||
"layer": {"type": "string", "default": "physical"},
|
||||
},
|
||||
"required": ["node_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "runLldpDiscover",
|
||||
"description": "Start async LLDP discovery into fabric (scope=ne_ids|all_inventory). Poll with getLldpDiscoverJob.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scope": {"type": "string", "enum": ["ne_ids", "all_inventory"], "default": "ne_ids"},
|
||||
"ne_ids": {"type": "array", "items": {"type": "string"}},
|
||||
"auto_add_unmatched": {"type": "boolean", "default": False},
|
||||
"concurrency": {"type": "integer", "minimum": 1, "maximum": 32, "default": 4},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "getLldpDiscoverJob",
|
||||
"description": "Get LLDP discover job status, counters, and per-NE items/unmatched.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"job_id": {"type": "string"}},
|
||||
"required": ["job_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "listTopologyViews",
|
||||
"description": "List topology views (presentation canvases over fabric).",
|
||||
"inputSchema": {"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
||||
},
|
||||
{
|
||||
"name": "getTopologyView",
|
||||
"description": "Get a topology view graph (nodes with positions + fabric edges between them).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {"view_id": {"type": "string"}},
|
||||
"required": ["view_id"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
_HANDLERS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
||||
|
|
@ -485,6 +650,14 @@ _HANDLERS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|||
"getManagedNe": _get_managed_ne,
|
||||
"execManagedNe": _exec_managed_ne,
|
||||
"listCliTargets": _list_cli_targets,
|
||||
"getTopologySummary": _get_topology_summary,
|
||||
"queryTopologyNodes": _query_topology_nodes,
|
||||
"queryTopologyEdges": _query_topology_edges,
|
||||
"getTopologyNeighborhood": _get_topology_neighborhood,
|
||||
"runLldpDiscover": _run_lldp_discover,
|
||||
"getLldpDiscoverJob": _get_lldp_discover_job,
|
||||
"listTopologyViews": _list_topology_views,
|
||||
"getTopologyView": _get_topology_view,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue