Skip to content

Commit

Permalink
Convert topologies < 3.0 to have valid node hostnames
Browse files Browse the repository at this point in the history
  • Loading branch information
grossmj committed Jul 20, 2024
1 parent ea339af commit 999f41b
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 2 deletions.
2 changes: 1 addition & 1 deletion gns3server/controller/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ def update_allocated_node_name(self, base_name):

if base_name is None:
return None
base_name = re.sub(r"[ ]", "", base_name)
base_name = re.sub(r"[ ]", "", base_name) # remove spaces in node name
if base_name in self._allocated_node_names:
base_name = re.sub(r"[0-9]+$", "{0}", base_name)

Expand Down
32 changes: 31 additions & 1 deletion gns3server/controller/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .node import Node
from .link import Link

from gns3server.utils.hostname import is_ios_hostname_valid, is_rfc1123_hostname_valid, to_rfc1123_hostname, to_ios_hostname
from gns3server.schemas.controller.topology import Topology
from gns3server.schemas.compute.dynamips_nodes import DynamipsCreate

Expand All @@ -43,7 +44,7 @@
log = logging.getLogger(__name__)


GNS3_FILE_FORMAT_REVISION = 9
GNS3_FILE_FORMAT_REVISION = 10


class DynamipsNodeValidation(DynamipsCreate):
Expand Down Expand Up @@ -186,6 +187,10 @@ def load_topology(path):
if variables:
topo["variables"] = [var for var in variables if var.get("name")]

# Version before GNS3 3.0
if topo["revision"] < 10:
topo = _convert_2_2_0(topo, path)

try:
_check_topology_schema(topo, path)
except ControllerError as e:
Expand All @@ -201,6 +206,31 @@ def load_topology(path):
return topo


def _convert_2_2_0(topo, topo_path):
"""
Convert topologies from GNS3 2.2.x to 3.0
Changes:
* Removed acpi_shutdown option from Qemu, VMware and VirtualBox
"""

topo["revision"] = 10

for node in topo.get("topology", {}).get("nodes", []):
# make sure console_type is not None but "none" string
if "properties" in node:
if node["node_type"] in ("qemu", "docker") and not is_rfc1123_hostname_valid(node["name"]):
new_name = to_rfc1123_hostname(node["name"])
log.info(f"Convert node name {node['name']} to {new_name} (RFC1123)")
node["name"] = new_name
if node["node_type"] in ("dynamips", "iou") and not is_ios_hostname_valid(node["name"] ):
new_name = to_ios_hostname(node["name"])
log.info(f"Convert node name {node['name']} to {new_name} (IOS)")
node["name"] = new_name
return topo


def _convert_2_1_0(topo, topo_path):
"""
Convert topologies from GNS3 2.1.x to 2.2
Expand Down
53 changes: 53 additions & 0 deletions gns3server/utils/hostname.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ def is_ios_hostname_valid(hostname: str) -> bool:
return False


def to_ios_hostname(name):
"""
Convert name to an IOS hostname
"""

# Replace invalid characters with hyphens
name = re.sub(r'[^a-zA-Z0-9-]', '-', name)

# Ensure the hostname starts with a letter
if not re.search(r'^[a-zA-Z]', name):
name = 'a' + name

# Ensure the hostname ends with a letter or digit
if not re.search(r'[a-zA-Z0-9]$', name):
name = name.rstrip('-') + '0'

# Truncate the hostname to 63 characters
name = name[:63]

return name


def is_rfc1123_hostname_valid(hostname: str) -> bool:
"""
Check if a hostname is valid according to RFC 1123
Expand All @@ -57,3 +79,34 @@ def is_rfc1123_hostname_valid(hostname: str) -> bool:

allowed = re.compile(r"(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$")
return all(allowed.match(label) for label in labels)


def to_rfc1123_hostname(name: str) -> str:
"""
Convert name to RFC 1123 hostname
"""

# Replace invalid characters with hyphens
name = re.sub(r'[^a-zA-Z0-9-.]', '-', name)

# Remove trailing dot if it exists
name = name.rstrip('.')

# Ensure each label is not longer than 63 characters
labels = name.split('.')
labels = [label[:63] for label in labels]

# Remove leading and trailing hyphens from each label if they exist
labels = [label.strip('-') for label in labels]

# Check if the TLD is all-numeric and if so, replace it with "invalid"
if re.match(r"[0-9]+$", labels[-1]):
labels[-1] = 'invalid'

# Join the labels back together
name = '.'.join(labels)

# Ensure the total length is not longer than 253 characters
name = name[:253]

return name
121 changes: 121 additions & 0 deletions tests/utils/test_hostname.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.

from gns3server.utils import hostname


def test_ios_hostname_valid_with_valid_hostnames():
assert hostname.is_ios_hostname_valid("router1")
assert hostname.is_ios_hostname_valid("switch-2")
assert hostname.is_ios_hostname_valid("a1-b2-c3")


def test_ios_hostname_valid_with_invalid_hostnames():
assert not hostname.is_ios_hostname_valid("-router")
assert not hostname.is_ios_hostname_valid("router-")
assert not hostname.is_ios_hostname_valid("123router")
assert not hostname.is_ios_hostname_valid("router@123")
assert not hostname.is_ios_hostname_valid("router.router")


def test_ios_hostname_valid_with_long_hostnames():
assert hostname.is_ios_hostname_valid("a" * 63)
assert not hostname.is_ios_hostname_valid("a" * 64)


def test_ios_hostname_conversion_with_valid_characters():
assert hostname.to_ios_hostname("validHostname123") == "validHostname123"


def test_ios_hostname_conversion_starts_with_digit():
assert hostname.to_ios_hostname("1InvalidStart") == "a1InvalidStart"


def test_ios_hostname_conversion_starts_with_special_character():
assert hostname.to_ios_hostname("@InvalidStart") == "a-InvalidStart"


def test_ios_hostname_conversion_ends_with_special_character():
assert hostname.to_ios_hostname("InvalidEnd-") == "InvalidEnd0"


def test_ios_hostname_conversion_contains_special_characters():
assert hostname.to_ios_hostname("Invalid@Hostname!") == "Invalid-Hostname0"


def test_ios_hostname_conversion_exceeds_max_length():
long_name = "a" * 64
assert hostname.to_ios_hostname(long_name) == "a" * 63


def test_ios_hostname_conversion_just_right_length():
exact_length_name = "a" * 63
assert hostname.to_ios_hostname(exact_length_name) == "a" * 63


def test_rfc1123_hostname_validity_with_valid_hostnames():
assert hostname.is_rfc1123_hostname_valid("example.com")
assert hostname.is_rfc1123_hostname_valid("subdomain.example.com")
assert hostname.is_rfc1123_hostname_valid("example-hyphen.com")
assert hostname.is_rfc1123_hostname_valid("example.com.")
assert hostname.is_rfc1123_hostname_valid("123.com")


def test_rfc1123_hostname_validity_with_invalid_hostnames():
assert not hostname.is_rfc1123_hostname_valid("-example.com")
assert not hostname.is_rfc1123_hostname_valid("example-.com")
assert not hostname.is_rfc1123_hostname_valid("example..com")
assert not hostname.is_rfc1123_hostname_valid("example_com")
assert not hostname.is_rfc1123_hostname_valid("example.123")


def test_rfc1123_hostname_validity_with_long_hostnames():
long_hostname = "a" * 63 + "." + "b" * 63 + "." + "c" * 63 + "." + "d" * 61 # 253 characters
too_long_hostname = long_hostname + "e"
assert hostname.is_rfc1123_hostname_valid(long_hostname)
assert not hostname.is_rfc1123_hostname_valid(too_long_hostname)


def test_rfc1123_conversion_hostname_with_valid_characters():
assert hostname.to_rfc1123_hostname("valid-hostname.example.com") == "valid-hostname.example.com"


def test_rfc1123_conversion_hostname_with_invalid_characters_replaced():
assert hostname.to_rfc1123_hostname("invalid_hostname!@#$.example") == "invalid-hostname.example"


def test_rfc1123_conversion_hostname_with_trailing_dot_removed():
assert hostname.to_rfc1123_hostname("hostname.example.com.") == "hostname.example.com"


def test_rfc1123_conversion_hostname_with_labels_exceeding_63_characters():
long_label = "a" * 64 + ".example.com"
expected_label = "a" * 63 + ".example.com"
assert hostname.to_rfc1123_hostname(long_label) == expected_label


def test_rfc1123_conversion_hostname_with_total_length_exceeding_253_characters():
long_hostname = "a" * 50 + "." + "b" * 50 + "." + "c" * 50 + "." + "d" * 50 + "." + "e" * 50
assert len(hostname.to_rfc1123_hostname(long_hostname)) <= 253


def test_rfc1123_conversion_hostname_with_all_numeric_tld_replaced():
assert hostname.to_rfc1123_hostname("hostname.123") == "hostname.invalid"


def rfc1123_hostname_with_multiple_consecutive_invalid_characters():
assert hostname.to_rfc1123_hostname("hostname!!!.example..com") == "hostname---.example.com"

0 comments on commit 999f41b

Please sign in to comment.