mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
copilot: log into devices using the node default credentials
The vendored gns3fy Node model dropped default_username/default_password
from the API response, and the nornir groups hardcoded empty
credentials, so drivers that require authentication (gns3_ruijie_telnet,
stock netmiko SSH/telnet) could not log in.
Carry the per-node credentials through nodes_inventory() into the
nornir hosts data at host level, where they override the group's empty
fallback. Missing or cleared ("") values keep inheriting from the
group, so no-auth drivers are unaffected.
This commit is contained in:
parent
38742ad4b6
commit
19f20e8d75
@ -1372,6 +1372,8 @@ class Node:
|
||||
properties: Any | None = None
|
||||
tags: list[str] | None = None
|
||||
netmiko_device_type: str | None = None
|
||||
default_username: str | None = None
|
||||
default_password: str | None = None
|
||||
|
||||
template: str | None = None
|
||||
links: list[Link] = field(default_factory=list, repr=False)
|
||||
@ -2489,6 +2491,8 @@ class Project:
|
||||
"y": _n.y,
|
||||
"tags": _n.tags if _n.tags else [],
|
||||
"netmiko_device_type": _n.netmiko_device_type,
|
||||
"default_username": _n.default_username,
|
||||
"default_password": _n.default_password,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -160,7 +160,7 @@ def get_device_ports_from_topology(
|
||||
# This is the Nornir best practice - each host has its own
|
||||
# connection configuration (device_type), while sharing common
|
||||
# settings (hostname, timeout) via group inheritance.
|
||||
hosts_data[device_name] = {
|
||||
host_entry = {
|
||||
"port": node_info["console_port"],
|
||||
"platform": platform,
|
||||
"groups": ["network_devices"], # For inheriting hostname, timeout, etc.
|
||||
@ -171,6 +171,16 @@ def get_device_ports_from_topology(
|
||||
},
|
||||
}
|
||||
|
||||
# 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.
|
||||
if node_info.get("default_username"):
|
||||
host_entry["username"] = node_info["default_username"]
|
||||
if node_info.get("default_password"):
|
||||
host_entry["password"] = node_info["default_password"]
|
||||
|
||||
hosts_data[device_name] = host_entry
|
||||
|
||||
logger.info("Returning %d device port mappings", len(hosts_data))
|
||||
|
||||
return hosts_data
|
||||
|
||||
@ -150,3 +150,106 @@ def test_device_ports_error_without_any_device_type(monkeypatch):
|
||||
|
||||
assert "error" in hosts["R2"]
|
||||
assert "netmiko_device_type" in hosts["R2"]["error"]
|
||||
|
||||
|
||||
def test_node_accepts_default_credentials():
|
||||
"""
|
||||
The vendored Node model must keep the default credentials so the
|
||||
device-port tools can log into devices that require authentication.
|
||||
"""
|
||||
pytest.importorskip("jwt", reason="ai-features extras not installed")
|
||||
from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Node
|
||||
|
||||
node = Node(
|
||||
name="R1",
|
||||
project_id="5f517ce3-1bc6-4245-b866-1a2fbd0ee5a7",
|
||||
node_id="0d15c2e6-8f83-4b79-8875-9dbc3e5f2f1e",
|
||||
node_type="docker",
|
||||
console_type="telnet",
|
||||
status="started",
|
||||
default_username="admin",
|
||||
default_password="admin123",
|
||||
)
|
||||
assert node.default_username == "admin"
|
||||
assert node.default_password == "admin123"
|
||||
|
||||
|
||||
def test_nodes_inventory_emits_default_credentials():
|
||||
"""
|
||||
The inventory dict consumed by get_device_ports_from_topology must
|
||||
carry the per-node default credentials.
|
||||
"""
|
||||
pytest.importorskip("jwt", reason="ai-features extras not installed")
|
||||
from types import SimpleNamespace
|
||||
from gns3server.agent.gns3_copilot.gns3_client.custom_gns3fy import Node, Project
|
||||
|
||||
project = Project(
|
||||
project_id="5f517ce3-1bc6-4245-b866-1a2fbd0ee5a7",
|
||||
connector=SimpleNamespace(base_url="http://127.0.0.1:3080"),
|
||||
)
|
||||
project.nodes = [
|
||||
Node(
|
||||
name="R1",
|
||||
project_id=project.project_id,
|
||||
node_id="0d15c2e6-8f83-4b79-8875-9dbc3e5f2f1e",
|
||||
node_type="dynamips",
|
||||
console=5000,
|
||||
default_username="admin",
|
||||
default_password="admin123",
|
||||
),
|
||||
]
|
||||
|
||||
inventory = project.nodes_inventory()
|
||||
assert inventory["R1"]["default_username"] == "admin"
|
||||
assert inventory["R1"]["default_password"] == "admin123"
|
||||
|
||||
|
||||
def test_device_ports_inject_default_credentials(monkeypatch):
|
||||
"""
|
||||
Per-node default credentials become host-level nornir values (which
|
||||
override the group's empty fallback); missing or cleared ("") values
|
||||
keep inheriting from the group.
|
||||
"""
|
||||
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": {
|
||||
"R1": {
|
||||
"console_port": 5000,
|
||||
"tags": [],
|
||||
"netmiko_device_type": "cisco_ios_telnet",
|
||||
"default_username": "admin",
|
||||
"default_password": "admin123",
|
||||
},
|
||||
# credentials cleared via PUT arrive as empty strings
|
||||
"R2": {
|
||||
"console_port": 5001,
|
||||
"tags": [],
|
||||
"netmiko_device_type": "cisco_ios_telnet",
|
||||
"default_username": "",
|
||||
"default_password": "",
|
||||
},
|
||||
# never seeded
|
||||
"R3": {
|
||||
"console_port": 5002,
|
||||
"tags": [],
|
||||
"netmiko_device_type": "cisco_ios_telnet",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
monkeypatch.setattr(gns3_client, "GNS3TopologyTool", _FakeTopology)
|
||||
hosts = get_gns3_device_port.get_device_ports_from_topology(["R1", "R2", "R3"])
|
||||
|
||||
# set credentials land at host level
|
||||
assert hosts["R1"]["username"] == "admin"
|
||||
assert hosts["R1"]["password"] == "admin123"
|
||||
# cleared ("") and absent credentials do not override the group fallback
|
||||
assert "username" not in hosts["R2"]
|
||||
assert "password" not in hosts["R2"]
|
||||
assert "username" not in hosts["R3"]
|
||||
assert "password" not in hosts["R3"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user