From b9cf9158cae6972ad630345f7d8c5ec1c9f44b26 Mon Sep 17 00:00:00 2001 From: tikD1337 <199419268+tikD1337@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:56:48 +0200 Subject: [PATCH 1/2] fix(compute): ignore null settings of custom adapters When only a port is renamed in a template's custom adapters, the adapter is saved with "adapter_type": null. The emulators read it with custom_adapter.get("adapter_type", self._adapter_type), which returns None for a key set to null: QEMU was then started with "-device None,..." and died ("'None' is not a valid device model name"), GNS3 only reporting "Could not start Telnet QEMU console". VirtualBox and VMware read the adapter type the same way. Drop the null settings in _get_custom_adapter_settings so the node's own adapter type (and MAC address) applies. Co-Authored-By: Claude Opus 5.5 --- gns3server/compute/base_node.py | 4 +++- tests/compute/qemu/test_qemu_vm.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 0913e1758..90bab3c25 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -1561,5 +1561,7 @@ class BaseNode: if self.custom_adapters: for custom_adapter in self.custom_adapters: if custom_adapter["adapter_number"] == adapter_number: - return custom_adapter + # settings left unset are null (e.g. "adapter_type" when only the + # port is renamed): drop them so callers fall back to the node's own + return {key: value for key, value in custom_adapter.items() if value is not None} return {} diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index f48418d4b..4d154a3e2 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -798,6 +798,21 @@ async def test_build_command_with_virtio_net_pci_adapter(vm): assert "virtio-net-pci,mac=00:00:ab:0e:0f:09,speed=10000,duplex=full,bus=pci-bridge1,addr=0x00,netdev=gns3-0" in cmd +@pytest.mark.asyncio +async def test_build_command_custom_adapter_without_type(vm): + """ + A custom adapter that only renames a port is saved with "adapter_type": null: + the node's adapter type applies instead of "-device None", which QEMU rejects. + """ + + vm.adapters = 1 + vm.mac_address = "00:00:ab:0e:0f:09" + vm.custom_adapters = [{"adapter_number": 0, "port_name": "sfp1", "adapter_type": None, "mac_address": None}] + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + cmd = await vm._build_command() + assert "e1000,mac=00:00:ab:0e:0f:09,bus=pci-bridge1,addr=0x00,netdev=gns3-0" in cmd + + @pytest.mark.asyncio async def test_build_command_with_invalid_options(vm): From dd69a01fba9fd0b5190127b5a51e9c1fc04cd426 Mon Sep 17 00:00:00 2001 From: tikD1337 <199419268+tikD1337@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:56:49 +0200 Subject: [PATCH 2/2] fix(controller): make node names from template names valid hostnames Creating a node from a template whose name has characters invalid in a hostname, such as "MikroTik CCR1072-1G-8S+", failed on the compute with "'MikroTikCCR1072-1G-8S+-1' is an invalid name to create a Qemu node": the template name was only stripped of spaces. Replace the other invalid characters by hyphens, following the rules the compute validates node names with (RFC 1123 for Qemu and Docker, IOS for Dynamips and IOU), as project import already does for existing nodes. Co-Authored-By: Claude Opus 5.5 --- gns3server/controller/project.py | 3 ++- gns3server/utils/hostname.py | 19 +++++++++++++++++++ tests/controller/test_project.py | 29 +++++++++++++++++++++++++++++ tests/utils/test_hostname.py | 19 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index e548d34e6..501e9f4e5 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -38,6 +38,7 @@ from ..config import Config from ..utils.application_id import get_next_application_id, is_iol_runner_environment from ..utils.asyncio import aiozipstream, locking, wait_run_in_executor from ..utils.asyncio.pool import Pool +from ..utils.hostname import template_name_to_node_name from ..utils.packet_filter_validation import validate_bpf_syntax from ..utils.path import check_path_allowed, get_default_project_directory from .compute import ComputeError @@ -622,7 +623,7 @@ class Project: log.info(f'Creating node from template "{template_name}" on compute "{compute.name}" [{compute.id}]') default_name_format = template.pop("default_name_format", "{name}-{0}") if name is None: - name = default_name_format.replace("{name}", template_name) + name = default_name_format.replace("{name}", template_name_to_node_name(template_name, node_type)) # 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 {} diff --git a/gns3server/utils/hostname.py b/gns3server/utils/hostname.py index bb9291e3f..e2dac7750 100644 --- a/gns3server/utils/hostname.py +++ b/gns3server/utils/hostname.py @@ -110,3 +110,22 @@ def to_rfc1123_hostname(name: str) -> str: name = name[:253] return name + + +def template_name_to_node_name(name: str, node_type: str) -> str: + """ + Convert a template name to the base of the names of the nodes created from it. + + Spaces are removed. For the node types whose names the compute validates as + hostnames, the other invalid characters (e.g. "+" in a model name) are replaced + by hyphens: RFC 1123 for Qemu and Docker, IOS for Dynamips and IOU. + """ + + name = name.replace(" ", "") + if node_type in ("qemu", "docker"): + name = re.sub(r"[^a-zA-Z0-9.-]", "-", name) + elif node_type in ("dynamips", "iou"): + name = re.sub(r"[^a-zA-Z0-9-]", "-", name) + else: + return name + return name.strip("-.") or node_type diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index b41c7cb53..9ea1c6726 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -253,6 +253,35 @@ async def test_add_node_from_template_seeds_default_credentials(controller): assert "default_password" not in node.properties +@pytest.mark.asyncio +async def test_add_node_from_template_name_invalid_in_hostname(controller): + """ + A template name with characters invalid in a hostname (e.g. "+" in a model name) + gives node names the compute accepts. + """ + + 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": "MikroTik CCR1072-1G-8S+", + "template_type": "qemu", + "compute_id": "local", + "default_name_format": "{name}-{0}", + "properties": {}, + } + + node = await project.add_node_from_template(template) + assert node.name == "MikroTikCCR1072-1G-8S-1" + + @pytest.mark.asyncio async def test_add_node_non_local(controller): """ diff --git a/tests/utils/test_hostname.py b/tests/utils/test_hostname.py index e802a40a5..3648a6825 100644 --- a/tests/utils/test_hostname.py +++ b/tests/utils/test_hostname.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import pytest + from gns3server.utils import hostname @@ -118,3 +120,20 @@ def test_rfc1123_conversion_hostname_with_all_numeric_tld_replaced(): def rfc1123_hostname_with_multiple_consecutive_invalid_characters(): assert hostname.to_rfc1123_hostname("hostname!!!.example..com") == "hostname---.example.com" + + +@pytest.mark.parametrize( + "name,node_type,expected", + [ + ("MikroTik CCR1072-1G-8S+", "qemu", "MikroTikCCR1072-1G-8S"), + ("MikroTik RB450G", "qemu", "MikroTikRB450G"), + ("Cisco IOSv 15.9", "qemu", "CiscoIOSv15.9"), + ("ubuntu:22.04", "docker", "ubuntu-22.04"), + ("IOU L2 15.2", "iou", "IOUL215-2"), + ("c7200+", "dynamips", "c7200"), + ("PC 1+", "vpcs", "PC1+"), + ("+++", "qemu", "qemu"), + ], +) +def test_template_name_to_node_name(name, node_type, expected): + assert hostname.template_name_to_node_name(name, node_type) == expected