mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
feat(cli): UME lazy exec via shared profiles and ume_ne_id MCP support
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ba1a40f725
commit
38a1c7f3a9
21 changed files with 1504 additions and 54 deletions
54
tests/test_cli_resolve.py
Normal file
54
tests/test_cli_resolve.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from netx_api.cli_resolve import infer_device_type_vendor
|
||||
from netx_api.models import CliConnectProfile, UmeInventoryNE
|
||||
|
||||
|
||||
class CliResolveTests(unittest.TestCase):
|
||||
def test_infer_zte_ne_type(self) -> None:
|
||||
profile = CliConnectProfile(device_type_default="zte_zxros", vendor_default="ZTE")
|
||||
dt, vendor = infer_device_type_vendor("ZXCTN 6180H", profile)
|
||||
self.assertEqual(dt, "zte_zxros")
|
||||
self.assertEqual(vendor, "ZTE")
|
||||
|
||||
def test_resolve_ume_target(self) -> None:
|
||||
from netx_api.cli_resolve import resolve_cli_target
|
||||
|
||||
db = MagicMock()
|
||||
inv = UmeInventoryNE(ne_id="ume-1", ip_address="10.0.0.1", ne_type="ZXCTN", user_label="NE-A")
|
||||
profile = CliConnectProfile(
|
||||
id="p1",
|
||||
name="default",
|
||||
is_default=True,
|
||||
username="ca-oper",
|
||||
password_enc="",
|
||||
device_type_default="zte_zxros",
|
||||
vendor_default="ZTE",
|
||||
hop_enabled=True,
|
||||
hop_vendor="bastion",
|
||||
hop_host="10.34.145.27",
|
||||
hop_username="ZTE-FIVIE",
|
||||
hop_password_enc="enc",
|
||||
hop_command_template="{hop_user}@{target_user}@{target_ip}",
|
||||
)
|
||||
db.get.side_effect = lambda model, key: {
|
||||
(UmeInventoryNE, "ume-1"): inv,
|
||||
(type(None), "ume-1"): None,
|
||||
}.get((model, key))
|
||||
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = profile
|
||||
|
||||
with unittest.mock.patch("netx_api.cli_resolve.decrypt_secret", return_value="vault-pass"):
|
||||
creds, device = resolve_cli_target(db, ume_ne_id="ume-1")
|
||||
|
||||
self.assertEqual(creds["ip_address"], "10.0.0.1")
|
||||
self.assertEqual(creds["username"], "ca-oper")
|
||||
self.assertEqual(creds["hop_host"], "10.34.145.27")
|
||||
self.assertEqual(device["source"], "ume")
|
||||
self.assertEqual(device["ume_ne_id"], "ume-1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -12,12 +12,13 @@ import pytest
|
|||
from netx_mcp.http_tools import HTTP_MCP_TOOLS, call_http_tool
|
||||
|
||||
|
||||
def test_http_mcp_tool_list_has_twelve_tools() -> None:
|
||||
def test_http_mcp_tool_list_has_thirteen_tools() -> None:
|
||||
names = [str(t.get("name") or "") for t in HTTP_MCP_TOOLS]
|
||||
assert len(names) == 12
|
||||
assert len(names) == 13
|
||||
assert "queryUmeAlarms" in names
|
||||
assert "queryUmeAlarmsRaw" in names
|
||||
assert "execManagedNe" in names
|
||||
assert "listCliTargets" in names
|
||||
|
||||
|
||||
def test_call_query_ume_alarms_forwards_http() -> None:
|
||||
|
|
@ -81,7 +82,7 @@ def test_stdio_initialize_and_tools_list() -> None:
|
|||
list_line = proc.stdout.readline()
|
||||
list_resp = json.loads(list_line)
|
||||
tools = list_resp["result"]["tools"]
|
||||
assert len(tools) == 12
|
||||
assert len(tools) == 13
|
||||
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
|
|
|
|||
|
|
@ -83,15 +83,14 @@ class NeExecValidationTests(unittest.TestCase):
|
|||
class NeExecRunTests(unittest.TestCase):
|
||||
@patch("netx_api.ne_exec.credentials_configured", return_value=True)
|
||||
@patch("netx_api.ne_exec._collect_on_device", return_value="ok-output")
|
||||
@patch("netx_api.ne_exec.get_device_credentials", return_value={"ip_address": "1.1.1.1"})
|
||||
def test_execute_success(self, _creds, _collect, _configured) -> None:
|
||||
row = MagicMock()
|
||||
row.id = "ne-1"
|
||||
db = MagicMock()
|
||||
db.get.return_value = row
|
||||
with patch("netx_api.ne_exec.row_to_out") as row_out:
|
||||
row_out.return_value.model_dump.return_value = {
|
||||
@patch("netx_api.ne_exec.resolve_cli_target")
|
||||
def test_execute_success(self, resolve, _collect, _configured) -> None:
|
||||
resolve.return_value = (
|
||||
{"ip_address": "1.1.1.1"},
|
||||
{
|
||||
"source": "managed",
|
||||
"id": "ne-1",
|
||||
"ume_ne_id": None,
|
||||
"name": "R2",
|
||||
"vendor": "Cisco",
|
||||
"device_type": "cisco_ios",
|
||||
|
|
@ -101,26 +100,28 @@ class NeExecRunTests(unittest.TestCase):
|
|||
"connect_status": "pass",
|
||||
"hop_enabled": False,
|
||||
"hop_vendor": "zte",
|
||||
}
|
||||
out = execute_managed_ne_commands(db, "ne-1", ["show version"])
|
||||
},
|
||||
)
|
||||
db = MagicMock()
|
||||
out = execute_managed_ne_commands(db, ["show version"], ne_id="ne-1")
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(out["output"], "ok-output")
|
||||
self.assertEqual(out["commands"], ["show version"])
|
||||
|
||||
@patch("netx_api.ne_exec.credentials_configured", return_value=True)
|
||||
@patch("netx_api.ne_exec._collect_on_device", return_value="ok-output")
|
||||
@patch("netx_api.ne_exec.get_device_credentials", return_value={"ip_address": "1.1.1.1"})
|
||||
def test_execute_skips_device_when_any_command_invalid(self, _creds, collect, _configured) -> None:
|
||||
@patch("netx_api.ne_exec.resolve_cli_target")
|
||||
def test_execute_skips_device_when_any_command_invalid(self, resolve, collect, _configured) -> None:
|
||||
db = MagicMock()
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
execute_managed_ne_commands(
|
||||
db,
|
||||
"ne-1",
|
||||
["show interface", "configure terminal"],
|
||||
ne_id="ne-1",
|
||||
)
|
||||
self.assertEqual(ctx.exception.detail, "command_blocked")
|
||||
collect.assert_not_called()
|
||||
db.get.assert_not_called()
|
||||
resolve.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue