mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
nodes: per-node default credentials seeded from the template
Add default_username/default_password as controller-only node properties (the netmiko_device_type pattern): they are not sent to the compute, persist with the project topology and can be updated or cleared per node. Creating a node from a template seeds them from the template appliance metadata, and the metadata itself is dropped there so it never leaks into the node properties.
This commit is contained in:
parent
300c53e6fb
commit
92d4e3d378
@ -58,6 +58,8 @@ class Node:
|
|||||||
"category",
|
"category",
|
||||||
"console_auto_start",
|
"console_auto_start",
|
||||||
"netmiko_device_type",
|
"netmiko_device_type",
|
||||||
|
"default_username",
|
||||||
|
"default_password",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, project, compute, name, node_id=None, node_type=None, template_id=None, **kwargs):
|
def __init__(self, project, compute, name, node_id=None, node_type=None, template_id=None, **kwargs):
|
||||||
@ -114,6 +116,8 @@ class Node:
|
|||||||
self._first_port_name = None
|
self._first_port_name = None
|
||||||
self._console_auto_start = False
|
self._console_auto_start = False
|
||||||
self._netmiko_device_type = None
|
self._netmiko_device_type = None
|
||||||
|
self._default_username = None
|
||||||
|
self._default_password = None
|
||||||
|
|
||||||
# This properties will be recomputed
|
# This properties will be recomputed
|
||||||
ignore_properties = ("width", "height", "hover_symbol")
|
ignore_properties = ("width", "height", "hover_symbol")
|
||||||
@ -222,6 +226,22 @@ class Node:
|
|||||||
def netmiko_device_type(self, val):
|
def netmiko_device_type(self, val):
|
||||||
self._netmiko_device_type = val
|
self._netmiko_device_type = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_username(self):
|
||||||
|
return self._default_username
|
||||||
|
|
||||||
|
@default_username.setter
|
||||||
|
def default_username(self, val):
|
||||||
|
self._default_username = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_password(self):
|
||||||
|
return self._default_password
|
||||||
|
|
||||||
|
@default_password.setter
|
||||||
|
def default_password(self, val):
|
||||||
|
self._default_password = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def properties(self):
|
def properties(self):
|
||||||
return self._properties
|
return self._properties
|
||||||
@ -844,6 +864,8 @@ class Node:
|
|||||||
"console_type": self._console_type,
|
"console_type": self._console_type,
|
||||||
"console_auto_start": self._console_auto_start,
|
"console_auto_start": self._console_auto_start,
|
||||||
"netmiko_device_type": self._netmiko_device_type,
|
"netmiko_device_type": self._netmiko_device_type,
|
||||||
|
"default_username": self._default_username,
|
||||||
|
"default_password": self._default_password,
|
||||||
"aux": self._aux,
|
"aux": self._aux,
|
||||||
"aux_type": self._aux_type,
|
"aux_type": self._aux_type,
|
||||||
"properties": self._properties,
|
"properties": self._properties,
|
||||||
|
|||||||
@ -584,6 +584,12 @@ class Project:
|
|||||||
default_name_format = template.pop("default_name_format", "{name}-{0}")
|
default_name_format = template.pop("default_name_format", "{name}-{0}")
|
||||||
if name is None:
|
if name is None:
|
||||||
name = default_name_format.replace("{name}", template_name)
|
name = default_name_format.replace("{name}", template_name)
|
||||||
|
# the appliance metadata stays template level: only the default
|
||||||
|
# credentials are seeded on the node (where they can be overridden)
|
||||||
|
appliance_metadata = template.pop("appliance_metadata", None) or {}
|
||||||
|
for field in ("default_username", "default_password"):
|
||||||
|
if appliance_metadata.get(field):
|
||||||
|
template[field] = appliance_metadata[field]
|
||||||
node_id = str(uuid.uuid4())
|
node_id = str(uuid.uuid4())
|
||||||
node = await self.add_node(compute, name, node_id, node_type=node_type, **template)
|
node = await self.add_node(compute, name, node_id, node_type=node_type, **template)
|
||||||
return node
|
return node
|
||||||
|
|||||||
@ -121,6 +121,14 @@ class NodeBase(BaseModel):
|
|||||||
description="Device type for Netmiko-based automation tools, overrides the template value",
|
description="Device type for Netmiko-based automation tools, overrides the template value",
|
||||||
pattern=r"^[a-z0-9_]+$|^$",
|
pattern=r"^[a-z0-9_]+$|^$",
|
||||||
)
|
)
|
||||||
|
default_username: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Default username to log into the node, seeded from the template appliance metadata",
|
||||||
|
)
|
||||||
|
default_password: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Default password to log into the node, seeded from the template appliance metadata",
|
||||||
|
)
|
||||||
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
|
||||||
aux_type: Optional[ConsoleType] = None
|
aux_type: Optional[ConsoleType] = None
|
||||||
properties: Optional[dict] = Field(default_factory=dict, description="Properties specific to an emulator")
|
properties: Optional[dict] = Field(default_factory=dict, description="Properties specific to an emulator")
|
||||||
|
|||||||
@ -143,6 +143,8 @@ def test_json(node, compute):
|
|||||||
"custom_adapters": [],
|
"custom_adapters": [],
|
||||||
"console_auto_start": False,
|
"console_auto_start": False,
|
||||||
"netmiko_device_type": None,
|
"netmiko_device_type": None,
|
||||||
|
"default_username": None,
|
||||||
|
"default_password": None,
|
||||||
"ports": [
|
"ports": [
|
||||||
{
|
{
|
||||||
"adapter_number": 0,
|
"adapter_number": 0,
|
||||||
@ -181,6 +183,8 @@ def test_json(node, compute):
|
|||||||
"tags": [],
|
"tags": [],
|
||||||
"console_auto_start": False,
|
"console_auto_start": False,
|
||||||
"netmiko_device_type": None,
|
"netmiko_device_type": None,
|
||||||
|
"default_username": None,
|
||||||
|
"default_password": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -420,6 +424,49 @@ def test_netmiko_device_type_from_template_kwargs(compute, project):
|
|||||||
assert node.asdict(topology_dump=True)["netmiko_device_type"] == "nokia_srl"
|
assert node.asdict(topology_dump=True)["netmiko_device_type"] == "nokia_srl"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_default_credentials(node, compute):
|
||||||
|
"""
|
||||||
|
default_username/default_password are controller-only properties: updating
|
||||||
|
them must not call the compute and must be persisted in the node json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
compute.put = AsyncioMagicMock()
|
||||||
|
node._project.emit_notification = AsyncioMagicMock()
|
||||||
|
node._project.dump = MagicMock()
|
||||||
|
|
||||||
|
await node.update(default_username="admin", default_password="secret")
|
||||||
|
assert not compute.put.called
|
||||||
|
assert node.default_username == "admin"
|
||||||
|
assert node.default_password == "secret"
|
||||||
|
assert node.asdict()["default_username"] == "admin"
|
||||||
|
assert node.asdict(topology_dump=True)["default_password"] == "secret"
|
||||||
|
|
||||||
|
# credentials never leak into the compute properties
|
||||||
|
assert "default_username" not in node.properties
|
||||||
|
assert "default_password" not in node.properties
|
||||||
|
|
||||||
|
# both fields can be cleared with an empty string
|
||||||
|
await node.update(default_username="", default_password="")
|
||||||
|
assert node.default_username == ""
|
||||||
|
assert node.default_password == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_credentials_from_template_kwargs(compute, project):
|
||||||
|
"""
|
||||||
|
A node created from a template with appliance metadata inherits the
|
||||||
|
default credentials without sending them to the compute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node = Node(project, compute, "test", node_type="vpcs",
|
||||||
|
default_username="root", default_password="cisco123")
|
||||||
|
assert node.default_username == "root"
|
||||||
|
assert node.default_password == "cisco123"
|
||||||
|
assert "default_username" not in node.properties
|
||||||
|
assert "default_password" not in node.properties
|
||||||
|
assert node.asdict(topology_dump=True)["default_username"] == "root"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_no_changes(node, compute):
|
async def test_update_no_changes(node, compute):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -204,6 +204,48 @@ async def test_add_node_local(controller):
|
|||||||
project.emit_notification.assert_any_call("node.created", node.asdict())
|
project.emit_notification.assert_any_call("node.created", node.asdict())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_node_from_template_seeds_default_credentials(controller):
|
||||||
|
"""
|
||||||
|
The appliance metadata stays template level: creating a node from a
|
||||||
|
template seeds the default credentials on the node and must not leak
|
||||||
|
the metadata into the node properties sent to the compute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
compute = MagicMock()
|
||||||
|
compute.id = "local"
|
||||||
|
controller._computes["local"] = compute
|
||||||
|
project = Project(controller=controller, name="Test")
|
||||||
|
project.emit_notification = MagicMock()
|
||||||
|
|
||||||
|
response = MagicMock()
|
||||||
|
response.json = {"console": 2048}
|
||||||
|
compute.post = AsyncioMagicMock(return_value=response)
|
||||||
|
|
||||||
|
template = {
|
||||||
|
"name": "VPCS_TEST",
|
||||||
|
"template_type": "vpcs",
|
||||||
|
"compute_id": "local",
|
||||||
|
"default_name_format": "PC{0}",
|
||||||
|
"properties": {"startup_script": "test.cfg"},
|
||||||
|
"appliance_metadata": {
|
||||||
|
"vendor_name": "Test vendor",
|
||||||
|
"default_username": "admin",
|
||||||
|
"default_password": "secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
node = await project.add_node_from_template(template)
|
||||||
|
|
||||||
|
# credentials seeded from the appliance metadata
|
||||||
|
assert node.default_username == "admin"
|
||||||
|
assert node.default_password == "secret"
|
||||||
|
# the metadata itself never reaches the node properties
|
||||||
|
assert "appliance_metadata" not in node.properties
|
||||||
|
assert "default_username" not in node.properties
|
||||||
|
assert "default_password" not in node.properties
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_node_non_local(controller):
|
async def test_add_node_non_local(controller):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user