diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 8794ddb0..fff4e435 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -33,7 +33,7 @@ from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer from gns3server.utils.asyncio.raw_command_server import AsyncioRawCommandServer from gns3server.utils.asyncio import wait_for_file_creation from gns3server.utils.asyncio import monitor_process -from gns3server.utils.get_resource import get_resource +from gns3server.utils import macaddress_to_int, int_to_macaddress from gns3server.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError from ..base_node import BaseNode @@ -83,6 +83,7 @@ class DockerVM(BaseNode): self._environment = environment self._cid = None self._ethernet_adapters = [] + self._mac_address = "" self._temporary_directory = None self._telnet_servers = [] self._vnc_process = None @@ -106,6 +107,8 @@ class DockerVM(BaseNode): else: self.adapters = adapters + self.mac_address = "" # this will generate a MAC address + log.debug("{module}: {name} [{image}] initialized.".format(module=self.manager.module_name, name=self.name, image=self._image)) @@ -119,6 +122,7 @@ class DockerVM(BaseNode): "project_id": self._project.id, "image": self._image, "adapters": self.adapters, + "mac_address": self.mac_address, "console": self.console, "console_type": self.console_type, "console_resolution": self.console_resolution, @@ -149,6 +153,36 @@ class DockerVM(BaseNode): def ethernet_adapters(self): return self._ethernet_adapters + @property + def mac_address(self): + """ + Returns the MAC address for this Docker container. + + :returns: adapter type (string) + """ + + return self._mac_address + + @mac_address.setter + def mac_address(self, mac_address): + """ + Sets the MAC address for this Docker container. + + :param mac_address: MAC address + """ + + if not mac_address: + # use the node UUID to generate a random MAC address + self._mac_address = "02:42:%s:%s:%s:00" % (self.id[2:4], self.id[4:6], self.id[6:8]) + else: + self._mac_address = mac_address + + log.info('Docker container "{name}" [{id}]: MAC address changed to {mac_addr}'.format( + name=self._name, + id=self._id, + mac_addr=self._mac_address) + ) + @property def start_command(self): return self._start_command @@ -350,6 +384,7 @@ class DockerVM(BaseNode): "Privileged": True, "Binds": self._mount_binds(image_infos), }, + "UsernsMode": "host", "Volumes": {}, "Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573 "Cmd": [], @@ -914,15 +949,33 @@ class DockerVM(BaseNode): bridge_name = 'bridge{}'.format(adapter_number) await self._ubridge_send('bridge create {}'.format(bridge_name)) self._bridges.add(bridge_name) - await self._ubridge_send('bridge add_nio_tap bridge{adapter_number} {hostif}'.format(adapter_number=adapter_number, - hostif=adapter.host_ifc)) + await self._ubridge_send('bridge add_nio_tap bridge{adapter_number} {hostif}'.format( + adapter_number=adapter_number, + hostif=adapter.host_ifc) + ) + + mac_address = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number) + custom_adapter = self._get_custom_adapter_settings(adapter_number) + custom_mac_address = custom_adapter.get("mac_address") + if custom_mac_address: + mac_address = custom_mac_address + + try: + await self._ubridge_send('docker set_mac_addr {ifc} {mac}'.format(ifc=adapter.host_ifc, mac=mac_address)) + except UbridgeError: + log.warning("Could not set MAC address %s on interface %s", mac_address, adapter.host_ifc) + log.debug("Move container %s adapter %s to namespace %s", self.name, adapter.host_ifc, self._namespace) try: - await self._ubridge_send('docker move_to_ns {ifc} {ns} eth{adapter}'.format(ifc=adapter.host_ifc, - ns=self._namespace, - adapter=adapter_number)) + await self._ubridge_send('docker move_to_ns {ifc} {ns} eth{adapter}'.format( + ifc=adapter.host_ifc, + ns=self._namespace, + adapter=adapter_number) + ) except UbridgeError as e: raise UbridgeNamespaceError(e) + else: + log.info("Created adapter %s with MAC address %s in namespace %s", adapter_number, mac_address, self._namespace) if nio: await self._connect_nio(adapter_number, nio) diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index fd3e625b..eb5bad69 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -26,8 +26,8 @@ import os from .compute import ComputeConflict, ComputeError from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory from ..utils.images import images_directories +from ..utils import macaddress_to_int, int_to_macaddress from ..config import Config -from ..utils.qt import qt_font_to_style import logging @@ -663,7 +663,13 @@ class Node: break port_name = "eth{}".format(adapter_number) port_name = custom_adapter_settings.get("port_name", port_name) - self._ports.append(PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name)) + mac_address = custom_adapter_settings.get("mac_address") + if not mac_address and "mac_address" in self._properties: + mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number) + + port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name) + port.mac_address = mac_address + self._ports.append(port) elif self._node_type in ("ethernet_switch", "ethernet_hub"): # Basic node we don't want to have adapter number port_number = 0 diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 74dc4990..8f623092 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -188,7 +188,7 @@ def load_topology(path): try: _check_topology_schema(topo) except aiohttp.web.HTTPConflict as e: - log.error("Can't load the topology %s", path) + log.error("Can't load the topology %s, please check using the debug mode...", path) raise e if changed: diff --git a/gns3server/handlers/api/compute/docker_handler.py b/gns3server/handlers/api/compute/docker_handler.py index 68516c8f..c726d52f 100644 --- a/gns3server/handlers/api/compute/docker_handler.py +++ b/gns3server/handlers/api/compute/docker_handler.py @@ -317,7 +317,7 @@ class DockerHandler: props = [ "name", "console", "aux", "console_type", "console_resolution", "console_http_port", "console_http_path", "start_command", - "environment", "adapters", "extra_hosts", "extra_volumes" + "environment", "adapters", "mac_address", "custom_adapters", "extra_hosts", "extra_volumes" ] changed = False diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py index 6cea166a..07125809 100644 --- a/gns3server/schemas/docker.py +++ b/gns3server/schemas/docker.py @@ -85,6 +85,12 @@ DOCKER_CREATE_SCHEMA = { "minimum": 0, "maximum": 99, }, + "mac_address": { + "description": "Docker container base MAC address", + "type": ["string", "null"], + "minLength": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + }, "environment": { "description": "Docker environment variables", "type": ["string", "null"], @@ -187,6 +193,12 @@ DOCKER_OBJECT_SCHEMA = { "minimum": 0, "maximum": 99, }, + "mac_address": { + "description": "Docker container base MAC address", + "type": ["string", "null"], + "minLength": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + }, "usage": { "description": "How to use the Docker container", "type": "string", diff --git a/gns3server/schemas/docker_template.py b/gns3server/schemas/docker_template.py index 0e04bbd1..838e1023 100644 --- a/gns3server/schemas/docker_template.py +++ b/gns3server/schemas/docker_template.py @@ -38,6 +38,15 @@ DOCKER_TEMPLATE_PROPERTIES = { "maximum": 99, "default": 1 }, + "mac_address": { + "description": "Docker container base MAC address", + "type": ["string", "null"], + "anyOf": [ + {"pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$"}, + {"pattern": "^$"} + ], + "default": "", + }, "start_command": { "description": "Docker CMD entry", "type": "string", diff --git a/gns3server/version.py b/gns3server/version.py index 5d76983b..b15708b1 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.49" -__version_info__ = (2, 2, 49, 0) +__version__ = "2.2.50.dev1" +__version_info__ = (2, 2, 50, 99) if "dev" in __version__: try: diff --git a/requirements.txt b/requirements.txt index 829a7733..2dec22a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ jsonschema>=4.23,<4.24 -aiohttp>=3.9.5,<3.10 +aiohttp>=3.10.3,<3.11 aiohttp-cors>=0.7.0,<0.8 aiofiles>=24.1.0,<25.0 Jinja2>=3.1.4,<3.2 diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 81825eba..3afea049 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -48,6 +48,7 @@ async def vm(compute_project, manager): vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm._cid = "e90e34656842" vm.allocate_aux = False + vm.mac_address = '02:42:3d:b7:93:00' return vm @@ -60,6 +61,7 @@ def test_json(vm, compute_project): 'project_id': compute_project.id, 'node_id': vm.id, 'adapters': 1, + 'mac_address': '02:42:3d:b7:93:00', 'console': vm.console, 'console_type': 'telnet', 'console_resolution': '1024x768', @@ -107,6 +109,7 @@ async def test_create(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -145,6 +148,7 @@ async def test_create_with_tag(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -187,6 +191,7 @@ async def test_create_vnc(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -316,6 +321,7 @@ async def test_create_start_cmd(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/ls"], @@ -414,6 +420,7 @@ async def test_create_image_not_available(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -457,6 +464,7 @@ async def test_create_with_user(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -540,6 +548,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -579,6 +588,7 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -618,6 +628,7 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -657,6 +668,7 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -695,6 +707,7 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -733,6 +746,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -779,6 +793,7 @@ async def test_create_with_extra_volumes(compute_project, manager): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1027,6 +1042,7 @@ async def test_update(vm): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1095,6 +1111,7 @@ async def test_update_running(vm): ], "Privileged": True }, + "UsernsMode": "host", "Volumes": {}, "NetworkDisabled": True, "Hostname": "test", @@ -1185,7 +1202,37 @@ async def test_add_ubridge_connection(vm): call.send('bridge start bridge0') ] assert 'bridge0' in vm._bridges - # We need to check any_order ortherwise mock is confused by asyncio + # We need to check any_order otherwise mock is confused by asyncio + vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True) + + +async def test_add_ubridge_connections_with_base_mac_address(vm): + + vm._ubridge_hypervisor = MagicMock() + vm._namespace = 42 + vm.adapters = 2 + vm.mac_address = "02:42:42:42:42:00" + + nio_params = { + "type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"} + + nio = vm.manager.create_nio(nio_params) + await vm._add_ubridge_connection(nio, 0) + + nio = vm.manager.create_nio(nio_params) + await vm._add_ubridge_connection(nio, 1) + + calls = [ + call.send('bridge create bridge0'), + call.send('bridge create bridge1'), + call.send('docker set_mac_addr tap-gns3-e0 02:42:42:42:42:00'), + call.send('docker set_mac_addr tap-gns3-e0 02:42:42:42:42:01') + ] + + # We need to check any_order otherwise mock is confused by asyncio vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True)