Skip to content

Commit 9a3bd2e

Browse files
authored
Merge pull request #2406 from GNS3/feature/convert-invalid-node-names
Convert node hostnames for topologies
2 parents ea339af + 2416069 commit 9a3bd2e

File tree

10 files changed

+653
-6
lines changed

10 files changed

+653
-6
lines changed

gns3server/controller/project.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ def update_allocated_node_name(self, base_name):
495495

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

gns3server/controller/topology.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .node import Node
3636
from .link import Link
3737

38+
from gns3server.utils.hostname import is_ios_hostname_valid, is_rfc1123_hostname_valid, to_rfc1123_hostname, to_ios_hostname
3839
from gns3server.schemas.controller.topology import Topology
3940
from gns3server.schemas.compute.dynamips_nodes import DynamipsCreate
4041

@@ -43,7 +44,7 @@
4344
log = logging.getLogger(__name__)
4445

4546

46-
GNS3_FILE_FORMAT_REVISION = 9
47+
GNS3_FILE_FORMAT_REVISION = 10
4748

4849

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

190+
# Version before GNS3 3.0
191+
if topo["revision"] < 10:
192+
topo = _convert_2_2_0(topo, path)
193+
189194
try:
190195
_check_topology_schema(topo, path)
191196
except ControllerError as e:
@@ -201,6 +206,30 @@ def load_topology(path):
201206
return topo
202207

203208

209+
def _convert_2_2_0(topo, topo_path):
210+
"""
211+
Convert topologies from GNS3 2.2.x to 3.0
212+
213+
Changes:
214+
* Convert Qemu and Docker node names to be a valid RFC1123 hostnames.
215+
* Convert Dynamips and IOU node names to be a valid IOS hostnames.
216+
"""
217+
218+
topo["revision"] = 10
219+
220+
for node in topo.get("topology", {}).get("nodes", []):
221+
if "properties" in node:
222+
if node["node_type"] in ("qemu", "docker") and not is_rfc1123_hostname_valid(node["name"]):
223+
new_name = to_rfc1123_hostname(node["name"])
224+
log.info(f"Convert node name {node['name']} to {new_name} (RFC1123)")
225+
node["name"] = new_name
226+
if node["node_type"] in ("dynamips", "iou") and not is_ios_hostname_valid(node["name"] ):
227+
new_name = to_ios_hostname(node["name"])
228+
log.info(f"Convert node name {node['name']} to {new_name} (IOS)")
229+
node["name"] = new_name
230+
return topo
231+
232+
204233
def _convert_2_1_0(topo, topo_path):
205234
"""
206235
Convert topologies from GNS3 2.1.x to 2.2

gns3server/utils/hostname.py

+53
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ def is_ios_hostname_valid(hostname: str) -> bool:
3232
return False
3333

3434

35+
def to_ios_hostname(name):
36+
"""
37+
Convert name to an IOS hostname
38+
"""
39+
40+
# Replace invalid characters with hyphens
41+
name = re.sub(r'[^a-zA-Z0-9-]', '-', name)
42+
43+
# Ensure the hostname starts with a letter
44+
if not re.search(r'^[a-zA-Z]', name):
45+
name = 'a' + name
46+
47+
# Ensure the hostname ends with a letter or digit
48+
if not re.search(r'[a-zA-Z0-9]$', name):
49+
name = name.rstrip('-') + '0'
50+
51+
# Truncate the hostname to 63 characters
52+
name = name[:63]
53+
54+
return name
55+
56+
3557
def is_rfc1123_hostname_valid(hostname: str) -> bool:
3658
"""
3759
Check if a hostname is valid according to RFC 1123
@@ -57,3 +79,34 @@ def is_rfc1123_hostname_valid(hostname: str) -> bool:
5779

5880
allowed = re.compile(r"(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$")
5981
return all(allowed.match(label) for label in labels)
82+
83+
84+
def to_rfc1123_hostname(name: str) -> str:
85+
"""
86+
Convert name to RFC 1123 hostname
87+
"""
88+
89+
# Replace invalid characters with hyphens
90+
name = re.sub(r'[^a-zA-Z0-9-.]', '-', name)
91+
92+
# Remove trailing dot if it exists
93+
name = name.rstrip('.')
94+
95+
# Ensure each label is not longer than 63 characters
96+
labels = name.split('.')
97+
labels = [label[:63] for label in labels]
98+
99+
# Remove leading and trailing hyphens from each label if they exist
100+
labels = [label.strip('-') for label in labels]
101+
102+
# Check if the TLD is all-numeric and if so, replace it with "invalid"
103+
if re.match(r"[0-9]+$", labels[-1]):
104+
labels[-1] = 'invalid'
105+
106+
# Join the labels back together
107+
name = '.'.join(labels)
108+
109+
# Ensure the total length is not longer than 253 characters
110+
name = name[:253]
111+
112+
return name

tests/topologies/1_5_docker_remote/after/1_5_docker_remote.gns3

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
"label": {
2424
"rotation": 0,
2525
"style": "font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #000000;fill-opacity: 1.0;",
26-
"text": "remote_busybox-1",
26+
"text": "remote-busybox-1",
2727
"x": -20,
2828
"y": -25
2929
},
30-
"name": "remote_busybox-1",
30+
"name": "remote-busybox-1",
3131
"node_id": "d397ef5a-84f1-4b6b-9d44-671937ec7781",
3232
"node_type": "docker",
3333
"port_name_format": "Ethernet{0}",

tests/topologies/1_5_docker_remote/before/1_5_docker_remote.gns3

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"label": {
1212
"color": "#ff000000",
1313
"font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
14-
"text": "remote_busybox-1",
14+
"text": "remote-busybox-1",
1515
"x": -20.4453125,
1616
"y": -25.0
1717
},
@@ -32,7 +32,7 @@
3232
"console_resolution": "1024x768",
3333
"console_type": "telnet",
3434
"image": "busybox:latest",
35-
"name": "remote_busybox-1"
35+
"name": "remote-busybox-1"
3636
},
3737
"server_id": 2,
3838
"type": "DockerVM",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
{
2+
"auto_close": true,
3+
"auto_open": false,
4+
"auto_start": false,
5+
"drawing_grid_size": 25,
6+
"grid_size": 75,
7+
"name": "test-hostnames",
8+
"project_id": "8b83e3ac-6b6a-4d6b-9938-bd630a6e458e",
9+
"revision": 10,
10+
"scene_height": 1000,
11+
"scene_width": 2000,
12+
"show_grid": false,
13+
"show_interface_labels": false,
14+
"show_layers": false,
15+
"snap_to_grid": false,
16+
"supplier": null,
17+
"topology": {
18+
"computes": [],
19+
"drawings": [],
20+
"links": [],
21+
"nodes": [
22+
{
23+
"compute_id": "local",
24+
"console": 5000,
25+
"console_auto_start": false,
26+
"console_type": "telnet",
27+
"custom_adapters": [],
28+
"first_port_name": null,
29+
"height": 45,
30+
"label": {
31+
"rotation": 0,
32+
"style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;",
33+
"text": "42Router_A-1",
34+
"x": -20,
35+
"y": -25
36+
},
37+
"locked": false,
38+
"name": "a42Router-A-1",
39+
"node_id": "adb89fbb-92ba-419b-96ca-1ad0f03ce3f6",
40+
"node_type": "dynamips",
41+
"port_name_format": "Ethernet{0}",
42+
"port_segment_size": 0,
43+
"properties": {
44+
"auto_delete_disks": false,
45+
"aux": null,
46+
"clock_divisor": 8,
47+
"disk0": 0,
48+
"disk1": 0,
49+
"dynamips_id": 1,
50+
"exec_area": 64,
51+
"idlemax": 500,
52+
"idlepc": "0x60aa1da0",
53+
"idlesleep": 30,
54+
"image": "c3745-adventerprisek9-mz.124-25d.image",
55+
"image_md5sum": "ddbaf74274822b50fa9670e10c75b08f",
56+
"iomem": 5,
57+
"mac_addr": "c401.fff5.0000",
58+
"mmap": true,
59+
"nvram": 256,
60+
"platform": "c3745",
61+
"ram": 256,
62+
"slot0": "GT96100-FE",
63+
"slot1": "NM-1FE-TX",
64+
"slot2": "NM-4T",
65+
"slot3": null,
66+
"slot4": null,
67+
"sparsemem": true,
68+
"system_id": "FTX0945W0MY",
69+
"usage": "",
70+
"wic0": "WIC-1T",
71+
"wic1": "WIC-1T",
72+
"wic2": "WIC-1T"
73+
},
74+
"symbol": ":/symbols/classic/router.svg",
75+
"template_id": "24f09d1a-64e1-4dc4-ae49-e785c1dbc0c5",
76+
"width": 66,
77+
"x": -130,
78+
"y": -64,
79+
"z": 1
80+
},
81+
{
82+
"compute_id": "local",
83+
"console": 5001,
84+
"console_auto_start": false,
85+
"console_type": "telnet",
86+
"custom_adapters": [
87+
{
88+
"adapter_number": 0,
89+
"adapter_type": "e1000"
90+
},
91+
{
92+
"adapter_number": 1,
93+
"adapter_type": "e1000"
94+
},
95+
{
96+
"adapter_number": 2,
97+
"adapter_type": "e1000"
98+
},
99+
{
100+
"adapter_number": 3,
101+
"adapter_type": "e1000"
102+
},
103+
{
104+
"adapter_number": 4,
105+
"adapter_type": "e1000"
106+
},
107+
{
108+
"adapter_number": 5,
109+
"adapter_type": "e1000"
110+
},
111+
{
112+
"adapter_number": 6,
113+
"adapter_type": "e1000"
114+
},
115+
{
116+
"adapter_number": 7,
117+
"adapter_type": "e1000"
118+
},
119+
{
120+
"adapter_number": 8,
121+
"adapter_type": "e1000"
122+
},
123+
{
124+
"adapter_number": 9,
125+
"adapter_type": "e1000"
126+
},
127+
{
128+
"adapter_number": 10,
129+
"adapter_type": "e1000"
130+
},
131+
{
132+
"adapter_number": 11,
133+
"adapter_type": "e1000"
134+
},
135+
{
136+
"adapter_number": 12,
137+
"adapter_type": "e1000"
138+
},
139+
{
140+
"adapter_number": 13,
141+
"adapter_type": "e1000"
142+
},
143+
{
144+
"adapter_number": 14,
145+
"adapter_type": "e1000"
146+
},
147+
{
148+
"adapter_number": 15,
149+
"adapter_type": "e1000"
150+
}
151+
],
152+
"first_port_name": "",
153+
"height": 48,
154+
"label": {
155+
"rotation": 0,
156+
"style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;",
157+
"text": "Switch_10.0.0.1",
158+
"x": -36,
159+
"y": -25
160+
},
161+
"locked": false,
162+
"name": "Switch-10.0.0.invalid",
163+
"node_id": "ccda4e49-770f-4237-956b-cc7281630468",
164+
"node_type": "qemu",
165+
"port_name_format": "Gi0/{0}",
166+
"port_segment_size": 4,
167+
"properties": {
168+
"adapter_type": "e1000",
169+
"adapters": 16,
170+
"bios_image": "",
171+
"bios_image_md5sum": null,
172+
"boot_priority": "c",
173+
"cdrom_image": "",
174+
"cdrom_image_md5sum": null,
175+
"cpu_throttling": 0,
176+
"cpus": 1,
177+
"create_config_disk": false,
178+
"hda_disk_image": "vios_l2-adventerprisek9-m.03.2017.qcow2",
179+
"hda_disk_image_md5sum": "8f14b50083a14688dec2fc791706bb3e",
180+
"hda_disk_interface": "virtio",
181+
"hdb_disk_image": "",
182+
"hdb_disk_image_md5sum": null,
183+
"hdb_disk_interface": "none",
184+
"hdc_disk_image": "",
185+
"hdc_disk_image_md5sum": null,
186+
"hdc_disk_interface": "none",
187+
"hdd_disk_image": "",
188+
"hdd_disk_image_md5sum": null,
189+
"hdd_disk_interface": "none",
190+
"initrd": "",
191+
"initrd_md5sum": null,
192+
"kernel_command_line": "",
193+
"kernel_image": "",
194+
"kernel_image_md5sum": null,
195+
"legacy_networking": false,
196+
"linked_clone": true,
197+
"mac_address": "0c:da:4e:49:00:00",
198+
"on_close": "power_off",
199+
"options": "",
200+
"platform": "x86_64",
201+
"process_priority": "normal",
202+
"qemu_path": "/bin/qemu-system-x86_64",
203+
"ram": 768,
204+
"replicate_network_connection_state": true,
205+
"tpm": false,
206+
"uefi": false,
207+
"usage": "There is no default password and enable password. There is no default configuration present. SUPER UPDATED!"
208+
},
209+
"symbol": ":/symbols/classic/multilayer_switch.svg",
210+
"template_id": "9db64790-65f4-4d38-a1ac-2f6ce45b70db",
211+
"width": 51,
212+
"x": -13,
213+
"y": 54,
214+
"z": 1
215+
}
216+
]
217+
},
218+
"type": "topology",
219+
"variables": null,
220+
"version": "2.2.49",
221+
"zoom": 100
222+
}

0 commit comments

Comments
 (0)