diff --git a/gns3server/controller/appliance.py b/gns3server/controller/appliance.py index 629765b8b..fa964abfa 100644 --- a/gns3server/controller/appliance.py +++ b/gns3server/controller/appliance.py @@ -71,6 +71,17 @@ class Appliance: @property def type(self): + if self._data.get("registry_version", 0) >= 8: + # registry version 8: the node type comes from the settings template_type, + # the default settings take precedence over the other sets + settings_list = self._data.get("settings") or [] + for settings in settings_list: + if settings.get("default"): + return settings.get("template_type", "qemu") + if settings_list: + return settings_list[0].get("template_type", "qemu") + return "qemu" + if "iou" in self._data: return "iou" elif "dynamips" in self._data: diff --git a/gns3server/controller/appliance_manager.py b/gns3server/controller/appliance_manager.py index 0420e78cc..5f78196fb 100644 --- a/gns3server/controller/appliance_manager.py +++ b/gns3server/controller/appliance_manager.py @@ -362,7 +362,14 @@ class ApplianceManager: symbol_theme = controller.symbols.theme category = appliance["category"] if category == "guest": - if "docker" in appliance: + if appliance.get("registry_version", 0) >= 8: + # registry version 8: look for a Docker settings set instead of a top-level block + settings_types = [ + settings.get("template_type") for settings in appliance.get("settings") or [] + ] + if "docker" in settings_types: + return controller.symbols.get_default_symbol("docker_guest", symbol_theme) + elif "docker" in appliance: return controller.symbols.get_default_symbol("docker_guest", symbol_theme) elif "qemu" in appliance: return controller.symbols.get_default_symbol("qemu_guest", symbol_theme) diff --git a/gns3server/schemas/controller/appliances.py b/gns3server/schemas/controller/appliances.py index 44906df18..593fcc810 100644 --- a/gns3server/schemas/controller/appliances.py +++ b/gns3server/schemas/controller/appliances.py @@ -481,6 +481,7 @@ class DockerPropertiesV8(BaseModel): extra_volumes: Optional[List[str]] = Field( None, title='Additional directories to make persistent' ) + custom_adapters: Optional[List[CustomAdapterItem]] = Field(None, title='Custom adapters') extra_configs: Optional[List[ExtraConfig]] = Field( None, title='Configuration files injected into the container (bind-mounted read-only)' ) diff --git a/tests/controller/test_appliance.py b/tests/controller/test_appliance.py new file mode 100644 index 000000000..98a1ca3b0 --- /dev/null +++ b/tests/controller/test_appliance.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# +# Copyright (C) 2026 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gns3server.controller.appliance import Appliance +from gns3server.controller.appliance_to_template import ApplianceToTemplate +from gns3server.schemas.controller.appliances import ApplianceModel + + +# v8 mirror of the XRd Control Plane appliance shape (docker, custom_adapters, no versions) +XRD_V8 = { + "registry_version": 8, + "appliance_id": "e4a3a5fe-3a13-521b-abd1-ab9483e83aa2", + "name": "XRd Control Plane", + "category": "router", + "description": "Cisco IOS XRd Control Plane", + "vendor_name": "Cisco", + "vendor_url": "https://www.cisco.com/", + "product_name": "XRd Control Plane", + "status": "experimental", + "availability": "service-contract", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "XRd usage", + "symbol": ":/symbols/router.svg", + "settings": [ + { + "name": "Default template settings", + "default": True, + "template_type": "docker", + "template_properties": { + "adapters": 24, + "image": "ios-xr/xrd-control-plane:24.4.1", + "console_type": "docker_exec", + "environment": "GNS3_SKIP_INIT=1\nGNS3_CONSOLE_CMD=/pkg/bin/xr_cli.sh", + "extra_volumes": ["/xr-storage"], + "custom_adapters": [ + {"adapter_number": 0, "port_name": "MgmtEth0/RP0/CPU0/0"}, + {"adapter_number": 1, "port_name": "Gi0/0/0/0"}, + ], + "extra_configs": [ + {"target": "/firstboot.cfg", "content": "!\nend\n"} + ], + }, + } + ], +} + + +def _appliance(data, builtin=True): + return Appliance("test.gns3a", data, builtin=builtin) + + +def test_v8_docker_appliance_validates_with_custom_adapters(): + # the discriminated union routes to ApplianceV8 and accepts custom_adapters + model = ApplianceModel.model_validate(XRD_V8) + assert model.registry_version == 8 + assert model.settings[0].template_properties.custom_adapters[0].port_name == "MgmtEth0/RP0/CPU0/0" + + +def test_v8_docker_appliance_type(): + assert _appliance(XRD_V8).type == "docker" + + +def test_v8_type_from_default_settings(): + # the default set wins over the other sets + appliance = { + "registry_version": 8, + "name": "mixed", + "status": "stable", + "settings": [ + {"name": "a", "template_type": "docker", "template_properties": {"image": "x"}}, + {"name": "b", "default": True, "template_type": "qemu", "template_properties": {"ram": 512}}, + ], + } + assert _appliance(appliance).type == "qemu" + + +def test_v8_qemu_appliance_type_without_default(): + appliance = { + "registry_version": 8, + "name": "single", + "status": "stable", + "settings": [{"name": "a", "template_type": "qemu", "template_properties": {"ram": 512}}], + } + assert _appliance(appliance).type == "qemu" + + +def test_v6_docker_appliance_type(): + appliance = { + "registry_version": 6, + "name": "v6 docker", + "status": "stable", + "docker": {"image": "test:latest"}, + } + assert _appliance(appliance).type == "docker" + + +def test_v8_docker_install_conversion(): + """ + The vendor NOS v8 shape (docker_exec console, env knobs, extra volumes, + custom adapters, extra configs) converts into a docker template. + """ + + template = ApplianceToTemplate().new_template(_appliance(XRD_V8).asdict(), None, "local") + + assert template["template_type"] == "docker" + assert template["image"] == "ios-xr/xrd-control-plane:24.4.1" + assert template["console_type"] == "docker_exec" + assert template["environment"] == "GNS3_SKIP_INIT=1\nGNS3_CONSOLE_CMD=/pkg/bin/xr_cli.sh" + assert template["extra_volumes"] == ["/xr-storage"] + assert template["custom_adapters"] == [ + {"adapter_number": 0, "port_name": "MgmtEth0/RP0/CPU0/0"}, + {"adapter_number": 1, "port_name": "Gi0/0/0/0"}, + ] + assert template["extra_configs"] == [{"target": "/firstboot.cfg", "content": "!\nend\n"}]