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:
YueGuobin committed 2026-08-21 21:41:42 +08:00
1 parent 300c53e6fb
commit 92d4e3d378
5 files changed
+125

No files matched your search

+47
View File
@@ -143,6 +143,8 @@ def test_json(node, compute):
"custom_adapters": [],
"console_auto_start": False,
"netmiko_device_type": None,
"default_username": None,
"default_password": None,
"ports": [
{
"adapter_number": 0,
@@ -181,6 +183,8 @@ def test_json(node, compute):
"tags": [],
"console_auto_start": False,
"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"
@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
async def test_update_no_changes(node, compute):
"""
+42
View File
@@ -204,6 +204,48 @@ async def test_add_node_local(controller):
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
async def test_add_node_non_local(controller):
"""