fix: dial per-node console hosts in MCP device config tools

The device tools (device_config_send / device_show_run / vpcs_config_set)
built their Nornir inventory with only the console port and inherited the
group hostname, which is always the controller host. Console ports of
nodes on remote computes are bound on the compute's own host, so those
nodes were unreachable from the MCP tools - only local-compute nodes
worked.

Pass the per-node console_host through the project inventory and inject
it as a host-level Nornir hostname (which overrides the group's
controller-wide default). Missing or wildcard (0.0.0.0/::) values keep
the group inheritance, so single-machine setups behave exactly as
before. VPCS builds its own host entries, so it carries the hostname
over explicitly.
This commit is contained in:
YueGuobin committed 2026-10-16 10:29:19 +08:00
1 parent b79dcaa49d
commit f47cb56495
6 files changed
+145 -6

No files matched your search

@@ -67,7 +67,7 @@ class GNS3TopologyTool(BaseTool):
Output: Dictionary with:
- `project_id`, `name`, `status`: Project metadata
- `nodes`: Dict of node details (node_id, name, ports, console_port,
type, etc.)
console_host, type, etc.)
- `links`: List of link connections
Use this to understand network structure before making changes.
@@ -43,9 +43,9 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None)
Build an inventory-style dict keyed by node name.
Shape (per node name):
{server, name, node_id, console_port, console_type, type, ports,
status, x, y, tags, netmiko_device_type, default_username,
default_password}
{server, name, node_id, console_port, console_host, console_type,
type, ports, status, x, y, tags, netmiko_device_type,
default_username, default_password}
"""
inventory: dict[str, Any] = {}
for n in nodes:
@@ -54,6 +54,9 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None)
"name": n.get("name"),
"node_id": n.get("node_id"),
"console_port": n.get("console"),
# Where the console port is bound: the compute's own host for
# remote computes, not the controller running this code.
"console_host": n.get("console_host"),
"console_type": n.get("console_type"),
"type": n.get("node_type"),
"ports": n.get("ports"),
@@ -451,7 +451,7 @@ class VPCSCommands(BaseTool):
continue
# VPCS devices use gns3_vpcs_telnet device type
hosts_data[device_name] = {
host_entry = {
"port": port,
"platform": "vpcs",
"groups": ["vpcs_devices"], # All VPCS devices share one group
@@ -465,6 +465,12 @@ class VPCSCommands(BaseTool):
}
},
}
# Remote computes bind console ports on their own host; carry the
# per-node hostname from the topology so it overrides the group's
# controller-wide default.
if "hostname" in device_ports[device_name]:
host_entry["hostname"] = device_ports[device_name]["hostname"]
hosts_data[device_name] = host_entry
return hosts_data
@@ -68,7 +68,12 @@ def get_device_ports_from_topology(
}
}
}
Devices that don't exist or missing console_port will not be included
Devices that don't exist or missing console_port will not be included.
Nodes whose console is bound on a remote compute also carry a
host-level "hostname" (copied from the topology's console_host) so
netmiko dials the compute hosting the console port instead of the
controller; for everything else the field is omitted and the shared
group hostname (the controller host) applies.
"""
# Log received parameters
logger.info("Called with device_names=%s, project_id=%s", device_names, project_id)
@@ -167,6 +172,15 @@ def get_device_ports_from_topology(
"connection_options": {"netmiko": {"extras": {"device_type": device_type}}},
}
# Per-node console hostname. Remote computes bind console ports
# on their own host, not on the controller running this code, so
# the group's controller-wide hostname cannot reach them. A
# host-level hostname (which wins over the group in nornir) fixes
# that; unset or wildcard values keep the group inheritance.
console_host = node_info.get("console_host")
if console_host and console_host not in ("0.0.0.0", "::"):
host_entry["hostname"] = console_host
# Per-node default credentials (seeded from the template appliance
# metadata) override the group's empty fallback. Only inject when
# set, so credential-less devices keep inheriting the group values.