Merge pull request #3026 from tikD1337/fix/node-names-adapter-type

fix: node names from templates with "+" and custom adapters with a null type
This commit is contained in:
Jeremy Grossmann 2026-10-23 14:08:47 +02:00 committed by GitHub
commit 2c10782635
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 87 additions and 2 deletions

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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

View File

@ -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):

View File

@ -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):
"""

View File

@ -14,6 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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