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