From f524e9a71380a94c84ad37fded743683d35464a3 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 16 Aug 2026 12:50:01 +0800 Subject: [PATCH] appliance: seed netmiko_device_type from the appliance file Both the v1-6 and v8 appliance models accept an optional top-level netmiko_device_type, and ApplianceToTemplate copies it into the created template so installed appliances carry the automation hint end to end. --- .../controller/appliance_to_template.py | 6 ++++ gns3server/schemas/controller/appliances.py | 6 ++++ .../controller/test_appliance_to_template.py | 31 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/gns3server/controller/appliance_to_template.py b/gns3server/controller/appliance_to_template.py index 79a9fb37e..6f7d74e76 100644 --- a/gns3server/controller/appliance_to_template.py +++ b/gns3server/controller/appliance_to_template.py @@ -58,6 +58,9 @@ class ApplianceToTemplate: if "tags" in appliance_config: new_template["tags"] = appliance_config.get("tags") + if appliance_config.get("netmiko_device_type"): + new_template["netmiko_device_type"] = appliance_config["netmiko_device_type"] + if new_template.get("symbol") is None: if appliance_config["category"] == "guest": if "docker" in appliance_config: @@ -172,6 +175,9 @@ class ApplianceToTemplate: if "tags" in appliance_config: new_template["tags"] = appliance_config.get("tags") + if appliance_config.get("netmiko_device_type"): + new_template["netmiko_device_type"] = appliance_config["netmiko_device_type"] + if not new_template.get("symbol"): # apply a default symbol based on the category and template type if appliance_config["category"] == "guest": diff --git a/gns3server/schemas/controller/appliances.py b/gns3server/schemas/controller/appliances.py index 593fcc810..a98544a19 100644 --- a/gns3server/schemas/controller/appliances.py +++ b/gns3server/schemas/controller/appliances.py @@ -632,6 +632,9 @@ class ApplianceV1_6(BaseModel): maintainer_email: Optional[Union[EmailStr, Annotated[str, Field(max_length=0)]]] = Field(None, title='Maintainer email') usage: Optional[str] = Field(None, title='How to use the appliance') symbol: Optional[str] = Field(None, title='An optional symbol for the appliance') + netmiko_device_type: Optional[str] = Field( + None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$' + ) first_port_name: Optional[str] = Field(None, title='Optional name of the first networking port example: eth0') port_name_format: Optional[str] = Field(None, title='Optional formating of the networking port example: eth{0}') port_segment_size: Optional[int] = Field( @@ -685,6 +688,9 @@ class ApplianceV8(BaseModel): default_username: Optional[str] = Field(None, title='Default username for the appliance') default_password: Optional[str] = Field(None, title='Default password for the appliance') symbol: Optional[str] = Field(None, title='An optional symbol for the appliance') + netmiko_device_type: Optional[str] = Field( + None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$' + ) tags: Optional[List[str]] = Field(None, title='User-defined metadata tags for the appliance') settings: List[TemplateSetting] = Field(..., title='Settings for running the appliance') images: Optional[List[ApplianceImage]] = Field(None, title='Images for this appliance') diff --git a/tests/controller/test_appliance_to_template.py b/tests/controller/test_appliance_to_template.py index 4a06b27d8..d3713d67b 100644 --- a/tests/controller/test_appliance_to_template.py +++ b/tests/controller/test_appliance_to_template.py @@ -16,9 +16,11 @@ # along with this program. If not, see . import pytest +import pydantic from gns3server.controller.appliance_to_template import ApplianceToTemplate from gns3server.controller.controller_error import ControllerError +from gns3server.schemas.controller.appliances import ApplianceModel # reduced mirror of the upstream vyos.gns3a (registry version 8, qemu, 2 settings sets) @@ -93,6 +95,21 @@ VYOS_V8 = { } +def test_v8_netmiko_device_type_copied_to_template(): + appliance = dict(VYOS_V8, netmiko_device_type="vyos_ssh") + + template = ApplianceToTemplate().new_template(appliance, VYOS_V8["versions"][1], "local") + assert template["netmiko_device_type"] == "vyos_ssh" + + +def test_v8_netmiko_device_type_validates(): + model = ApplianceModel.model_validate(dict(VYOS_V8, netmiko_device_type="vyos_ssh")) + assert model.netmiko_device_type == "vyos_ssh" + + with pytest.raises(pydantic.ValidationError): + ApplianceModel.model_validate(dict(VYOS_V8, netmiko_device_type="Not Valid!")) + + def test_v8_version_referenced_settings_with_inheritance(): """ A version referencing a named settings set must select it and inherit @@ -283,3 +300,17 @@ def test_v6_path_unchanged(): assert template["console_type"] == "docker_exec" assert template["adapters"] == 35 assert template["usage"] == "v6 usage" + + +def test_v6_netmiko_device_type_copied_to_template(): + appliance = { + "registry_version": 6, + "name": "SRLinux", + "category": "router", + "symbol": ":/symbols/router.svg", + "netmiko_device_type": "nokia_srl", + "docker": {"adapters": 35, "image": "ghcr.io/nokia/srlinux:latest"}, + } + + template = ApplianceToTemplate().new_template(appliance, None, "local") + assert template["netmiko_device_type"] == "nokia_srl"