mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-15 14:31:18 +03:00
copilot: prefer netmiko_device_type over the device_type:<type> tag
The vendored gns3fy Node model and its nodes_inventory() now carry the node's netmiko_device_type field, and get_device_ports_from_topology() resolves the Netmiko device type from it first, falling back to the device_type:<type> tag. Nodes created from a template inherit the value from the template automatically, so automation tooling gets the correct Netmiko driver without tags.
This commit is contained in:
parent
f524e9a713
commit
435aa4c257
@ -535,6 +535,8 @@ class Gns3Connector:
|
|||||||
|
|
||||||
- `tags` (list): List of tags for the template (e.g.,
|
- `tags` (list): List of tags for the template (e.g.,
|
||||||
["device_type:cisco_ios_telnet", "platform:cisco_ios"])
|
["device_type:cisco_ios_telnet", "platform:cisco_ios"])
|
||||||
|
- `netmiko_device_type` (str): Netmiko device type (e.g. "cisco_ios_telnet");
|
||||||
|
preferred over the device_type:<type> tag
|
||||||
- Any other template attributes supported by GNS3 API
|
- Any other template attributes supported by GNS3 API
|
||||||
"""
|
"""
|
||||||
# Get existing template
|
# Get existing template
|
||||||
@ -571,6 +573,8 @@ class Gns3Connector:
|
|||||||
|
|
||||||
- `tags` (list): List of tags for the template (e.g.,
|
- `tags` (list): List of tags for the template (e.g.,
|
||||||
["device_type:cisco_ios_telnet", "platform:cisco_ios"])
|
["device_type:cisco_ios_telnet", "platform:cisco_ios"])
|
||||||
|
- `netmiko_device_type` (str): Netmiko device type (e.g. "cisco_ios_telnet");
|
||||||
|
preferred over the device_type:<type> tag
|
||||||
- Any other template attributes supported by GNS3 API
|
- Any other template attributes supported by GNS3 API
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
@ -579,7 +583,8 @@ class Gns3Connector:
|
|||||||
>>> connector.create_template(
|
>>> connector.create_template(
|
||||||
... name="cisco_router",
|
... name="cisco_router",
|
||||||
... template_type="dynamips",
|
... template_type="dynamips",
|
||||||
... tags=["device_type:cisco_ios_telnet", "platform:cisco_ios"]
|
... tags=["device_type:cisco_ios_telnet", "platform:cisco_ios"],
|
||||||
|
... netmiko_device_type="cisco_ios_telnet"
|
||||||
... )
|
... )
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
@ -1366,6 +1371,7 @@ class Node:
|
|||||||
template_id: str | None = None
|
template_id: str | None = None
|
||||||
properties: Any | None = None
|
properties: Any | None = None
|
||||||
tags: list[str] | None = None
|
tags: list[str] | None = None
|
||||||
|
netmiko_device_type: str | None = None
|
||||||
|
|
||||||
template: str | None = None
|
template: str | None = None
|
||||||
links: list[Link] = field(default_factory=list, repr=False)
|
links: list[Link] = field(default_factory=list, repr=False)
|
||||||
@ -2482,6 +2488,7 @@ class Project:
|
|||||||
"x": _n.x,
|
"x": _n.x,
|
||||||
"y": _n.y,
|
"y": _n.y,
|
||||||
"tags": _n.tags if _n.tags else [],
|
"tags": _n.tags if _n.tags else [],
|
||||||
|
"netmiko_device_type": _n.netmiko_device_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -62,7 +62,7 @@ def get_device_ports_from_topology(
|
|||||||
"groups": ["network_devices"], # For inheriting shared settings
|
"groups": ["network_devices"], # For inheriting shared settings
|
||||||
"connection_options": {
|
"connection_options": {
|
||||||
"netmiko": {
|
"netmiko": {
|
||||||
"extras": {"device_type": "huawei_telnet"} # Extracted from tags
|
"extras": {"device_type": "huawei_telnet"} # netmiko_device_type field, tag fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,18 +102,21 @@ def get_device_ports_from_topology(
|
|||||||
logger.warning("Device '%s' missing console_port", device_name)
|
logger.warning("Device '%s' missing console_port", device_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract device_type and platform from tags
|
# Extract device_type and platform.
|
||||||
device_type = None
|
# Precedence: the netmiko_device_type field (node/template/appliance
|
||||||
|
# level, set in GNS3 server >= 3.x) wins over the device_type:<type>
|
||||||
|
# tag, which remains as a fallback.
|
||||||
|
device_type = node_info.get("netmiko_device_type")
|
||||||
platform = None
|
platform = None
|
||||||
tags = node_info.get("tags", [])
|
tags = node_info.get("tags", [])
|
||||||
|
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag.startswith("device_type:"):
|
if tag.startswith("device_type:") and device_type is None:
|
||||||
device_type = tag.split(":", 1)[1].strip()
|
device_type = tag.split(":", 1)[1].strip()
|
||||||
elif tag.startswith("platform:"):
|
elif tag.startswith("platform:"):
|
||||||
platform = tag.split(":", 1)[1].strip()
|
platform = tag.split(":", 1)[1].strip()
|
||||||
|
|
||||||
# Return error if device_type not found in tags
|
# Return error if device_type not found anywhere
|
||||||
# Using a default would cause command execution errors
|
# Using a default would cause command execution errors
|
||||||
if device_type is None:
|
if device_type is None:
|
||||||
tested_device_types = (
|
tested_device_types = (
|
||||||
@ -121,8 +124,9 @@ def get_device_ports_from_topology(
|
|||||||
"gns3_ruijie_telnet (custom Ruijie)"
|
"gns3_ruijie_telnet (custom Ruijie)"
|
||||||
)
|
)
|
||||||
error_msg = (
|
error_msg = (
|
||||||
f"Device '{device_name}': device_type tag not found. "
|
f"Device '{device_name}': no device type found. "
|
||||||
f"Please add 'device_type:<type>' tag to this device in GNS3. "
|
f"Set the template/node 'netmiko_device_type' field (e.g. 'cisco_ios_telnet'), "
|
||||||
|
f"or add a 'device_type:<type>' tag to this device in GNS3. "
|
||||||
f"To configure via Web UI: right-click the device -> Configure -> Tags -> add 'device_type:<type>'. "
|
f"To configure via Web UI: right-click the device -> Configure -> Tags -> add 'device_type:<type>'. "
|
||||||
f"Tested types: {tested_device_types}. "
|
f"Tested types: {tested_device_types}. "
|
||||||
f"Current tags: {tags}"
|
f"Current tags: {tags}"
|
||||||
@ -134,7 +138,7 @@ def get_device_ports_from_topology(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Device '%s': extracted device_type=%s from tags",
|
"Device '%s': device_type=%s",
|
||||||
device_name,
|
device_name,
|
||||||
device_type,
|
device_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -69,3 +69,84 @@ def test_node_accepts_docker_exec_console():
|
|||||||
status="started",
|
status="started",
|
||||||
)
|
)
|
||||||
assert node.console_type == "docker_exec"
|
assert node.console_type == "docker_exec"
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_accepts_netmiko_device_type():
|
||||||
|
"""
|
||||||
|
The vendored Node model must keep the netmiko_device_type field so the
|
||||||
|
device-port tools can prefer it over the device_type:<type> tag.
|
||||||
|
"""
|
||||||
|
pytest.importorskip("jwt", reason="ai-features extras not installed")
|
||||||
|
from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Node
|
||||||
|
|
||||||
|
node = Node(
|
||||||
|
name="SR1",
|
||||||
|
project_id="5f517ce3-1bc6-4245-b866-1a2fbd0ee5a7",
|
||||||
|
node_id="0d15c2e6-8f83-4b79-8875-9dbc3e5f2f1e",
|
||||||
|
node_type="docker",
|
||||||
|
console_type="docker_exec",
|
||||||
|
status="started",
|
||||||
|
netmiko_device_type="nokia_srl",
|
||||||
|
)
|
||||||
|
assert node.netmiko_device_type == "nokia_srl"
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_ports_prefer_netmiko_field_over_tag(monkeypatch):
|
||||||
|
"""
|
||||||
|
netmiko_device_type on the node wins over the device_type:<type> tag;
|
||||||
|
the tag stays as fallback when the field is missing.
|
||||||
|
"""
|
||||||
|
pytest.importorskip("jwt", reason="ai-features extras not installed")
|
||||||
|
from gns3server.agent.gns3_copilot.utils import get_gns3_device_port
|
||||||
|
from gns3server.agent.gns3_copilot import gns3_client
|
||||||
|
|
||||||
|
class _FakeTopology:
|
||||||
|
def _run(self, project_id=None, jwt_token=None, url=None):
|
||||||
|
return {
|
||||||
|
"nodes": {
|
||||||
|
"SR1": {
|
||||||
|
"console_port": 5000,
|
||||||
|
"tags": ["device_type:cisco_ios_telnet"],
|
||||||
|
"netmiko_device_type": "nokia_srl",
|
||||||
|
},
|
||||||
|
"R1": {
|
||||||
|
"console_port": 5001,
|
||||||
|
"tags": ["device_type:cisco_ios_telnet", "platform:cisco_ios"],
|
||||||
|
"netmiko_device_type": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# the function does a lazy from-import inside the body
|
||||||
|
monkeypatch.setattr(gns3_client, "GNS3TopologyTool", _FakeTopology)
|
||||||
|
hosts = get_gns3_device_port.get_device_ports_from_topology(["SR1", "R1"])
|
||||||
|
|
||||||
|
# field wins over tag
|
||||||
|
assert hosts["SR1"]["connection_options"]["netmiko"]["extras"]["device_type"] == "nokia_srl"
|
||||||
|
# tag fallback when the field is absent
|
||||||
|
assert hosts["R1"]["connection_options"]["netmiko"]["extras"]["device_type"] == "cisco_ios_telnet"
|
||||||
|
assert hosts["R1"]["platform"] == "cisco_ios"
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_ports_error_without_any_device_type(monkeypatch):
|
||||||
|
pytest.importorskip("jwt", reason="ai-features extras not installed")
|
||||||
|
from gns3server.agent.gns3_copilot.utils import get_gns3_device_port
|
||||||
|
from gns3server.agent.gns3_copilot import gns3_client
|
||||||
|
|
||||||
|
class _FakeTopology:
|
||||||
|
def _run(self, project_id=None, jwt_token=None, url=None):
|
||||||
|
return {
|
||||||
|
"nodes": {
|
||||||
|
"R2": {
|
||||||
|
"console_port": 5002,
|
||||||
|
"tags": ["platform:cisco_ios"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# the function does a lazy from-import inside the body
|
||||||
|
monkeypatch.setattr(gns3_client, "GNS3TopologyTool", _FakeTopology)
|
||||||
|
hosts = get_gns3_device_port.get_device_ports_from_topology(["R2"])
|
||||||
|
|
||||||
|
assert "error" in hosts["R2"]
|
||||||
|
assert "netmiko_device_type" in hosts["R2"]["error"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user