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 <noreply@anthropic.com>
This commit is contained in:
tikD1337 2026-10-23 09:56:49 +02:00
parent b9cf9158ca
commit dd69a01fba
4 changed files with 69 additions and 1 deletions

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

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