nodes: per-node netmiko_device_type (controller-only)

netmiko_device_type follows the CONTROLLER_ONLY_PROPERTIES pattern
(like console_auto_start): a node created from a template inherits the
template value, PUT /nodes can override it inside a topology, updates
never round-trip to the compute, and the value persists in the project
topology file.
This commit is contained in:
YueGuobin 2026-08-16 12:45:52 +08:00
parent 9260108f53
commit d64f47afa4
No known key found for this signature in database
3 changed files with 48 additions and 0 deletions

View File

@ -57,6 +57,7 @@ class Node:
"ports",
"category",
"console_auto_start",
"netmiko_device_type",
]
def __init__(self, project, compute, name, node_id=None, node_type=None, template_id=None, **kwargs):
@ -112,6 +113,7 @@ class Node:
self._port_segment_size = 0
self._first_port_name = None
self._console_auto_start = False
self._netmiko_device_type = None
# This properties will be recomputed
ignore_properties = ("width", "height", "hover_symbol")
@ -212,6 +214,14 @@ class Node:
def console_auto_start(self, val):
self._console_auto_start = val
@property
def netmiko_device_type(self):
return self._netmiko_device_type
@netmiko_device_type.setter
def netmiko_device_type(self, val):
self._netmiko_device_type = val
@property
def properties(self):
return self._properties
@ -833,6 +843,7 @@ class Node:
"console": self._console,
"console_type": self._console_type,
"console_auto_start": self._console_auto_start,
"netmiko_device_type": self._netmiko_device_type,
"aux": self._aux,
"aux_type": self._aux_type,
"properties": self._properties,

View File

@ -116,6 +116,11 @@ class NodeBase(BaseModel):
console_auto_start: Optional[bool] = Field(
False, description="Automatically start the console when the node has started"
)
netmiko_device_type: Optional[str] = Field(
None,
description="Device type for Netmiko-based automation tools, overrides the template value",
pattern=r"^[a-z0-9_]+$",
)
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
aux_type: Optional[ConsoleType] = None
properties: Optional[dict] = Field(default_factory=dict, description="Properties specific to an emulator")

View File

@ -142,6 +142,7 @@ def test_json(node, compute):
"tags": [],
"custom_adapters": [],
"console_auto_start": False,
"netmiko_device_type": None,
"ports": [
{
"adapter_number": 0,
@ -179,6 +180,7 @@ def test_json(node, compute):
"custom_adapters": [],
"tags": [],
"console_auto_start": False,
"netmiko_device_type": None,
}
@ -383,6 +385,36 @@ async def test_update_only_controller(node, compute):
assert not node._project.emit_notification.called
@pytest.mark.asyncio
async def test_update_netmiko_device_type(node, compute):
"""
netmiko_device_type is a controller-only property: updating it must not
call the compute and must be visible in the node json.
"""
compute.put = AsyncioMagicMock()
node._project.emit_notification = AsyncioMagicMock()
node._project.dump = MagicMock()
await node.update(netmiko_device_type="cisco_ios_telnet")
assert not compute.put.called
assert node.netmiko_device_type == "cisco_ios_telnet"
assert node.asdict()["netmiko_device_type"] == "cisco_ios_telnet"
def test_netmiko_device_type_from_template_kwargs(compute, project):
"""
A node created with the template properties as kwargs inherits
netmiko_device_type without sending it to the compute.
"""
node = Node(project, compute, "test", node_type="vpcs", netmiko_device_type="nokia_srl")
assert node.netmiko_device_type == "nokia_srl"
# controller-only: must not leak into the compute properties
assert "netmiko_device_type" not in node.properties
assert node.asdict(topology_dump=True)["netmiko_device_type"] == "nokia_srl"
@pytest.mark.asyncio
async def test_update_no_changes(node, compute):
"""