diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 940aeab64..6afba5e63 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -28,6 +28,7 @@ import subprocess import os import re +import json from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer from gns3server.utils.asyncio.raw_command_server import AsyncioRawCommandServer @@ -65,13 +66,14 @@ class DockerVM(BaseNode): :param console_resolution: Resolution of the VNC display :param console_http_port: Port to redirect HTTP queries :param console_http_path: Url part with the path of the web interface - :param extra_hosts: Hosts which will be written into /etc/hosts into docker conainer + :param extra_hosts: Hosts which will be written into /etc/hosts into docker container :param extra_volumes: Additional directories to make persistent + :param extra_parameters: Additional parameters used to create the docker container """ def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None, adapters=None, environment=None, console_type="telnet", console_resolution="1024x768", - console_http_port=80, console_http_path="/", extra_hosts=None, extra_volumes=[]): + console_http_port=80, console_http_path="/", extra_hosts=None, extra_volumes=[], extra_parameters=None): super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type) @@ -94,6 +96,7 @@ def __init__(self, name, node_id, project, manager, image, console=None, aux=Non self._console_websocket = None self._extra_hosts = extra_hosts self._extra_volumes = extra_volumes or [] + self._extra_parameters = extra_parameters self._permissions_fixed = False self._display = None self._closing = False @@ -132,6 +135,7 @@ def __json__(self): "node_directory": self.working_path, "extra_hosts": self.extra_hosts, "extra_volumes": self.extra_volumes, + "extra_parameters": self.extra_parameters } def _get_free_display_port(self): @@ -211,6 +215,14 @@ def extra_volumes(self): def extra_volumes(self, extra_volumes): self._extra_volumes = extra_volumes + @property + def extra_parameters(self): + return self._extra_parameters + + @extra_parameters.setter + def extra_parameters(self, extra_parameters): + self._extra_parameters = extra_parameters + async def _get_container_state(self): """ Returns the container state (e.g. running, paused etc.) @@ -400,6 +412,9 @@ async def create(self): extra_hosts = self._format_extra_hosts(self._extra_hosts) if extra_hosts: params["Env"].append("GNS3_EXTRA_HOSTS={}".format(extra_hosts)) + + if self._extra_parameters: + params = self._merge_extra_parameters(params) result = await self.manager.query("POST", "containers/create", data=params) self._cid = result['Id'] @@ -424,6 +439,31 @@ def _format_extra_hosts(self, extra_hosts): except ValueError: raise DockerError("Can't apply `ExtraHosts`, wrong format: {}".format(extra_hosts)) return "\n".join(["{}\t{}".format(h[1], h[0]) for h in hosts]) + + def _merge_extra_parameters(self,params): + """ + Merge the user supplied extra parameters into the default Docker create parameters + """ + try: + #TODO: Determine additional validation needed, docker params can be quite complicated and could break the host if + # an a person does something stupid. Validation might be too complex for us to consider all the dangers. + # Also, I create an array of params that cannot/should'nt be overwritten , not sure if it's correct. + extra_params = json.loads(self._extra_parameters) + + merged = {**params, **extra_params} # merge values , common values will be overwritten so I will manually merge binds(see - if "HostConfig") + readonly_params = ["Hostname","Name","Entrypoint","Cmd","Image","Env","NetworkDisabled","Tty","OpenStdin","StdinOnce" ] + for param in readonly_params: # prevent certain parameters from being overwritten + merged[param] = params[param] + + if "HostConfig" in extra_params: # If extra params HostConfig sub section exists it would have overwritten GNS3 binds + merged["HostConfig"] = {**params["HostConfig"] , **extra_params["HostConfig"]} # merge the two HostConfig sections + if "Binds" in extra_params["HostConfig"]: # Add back the original GNS3 binds to the extra parameters binds + merged["HostConfig"]["Binds"].extend(params["HostConfig"]["Binds"]) + params=merged # Set params equal to our new merged config + + except ValueError: # if the variable text is not a valid json object then log the error + raise DockerError("Can't apply `ExtraParameters`, invalid json text:\n {}".format(self._extra_parameters)) + return params async def update(self): """ diff --git a/gns3server/handlers/api/compute/docker_handler.py b/gns3server/handlers/api/compute/docker_handler.py index e510a9f2a..d70fe7247 100644 --- a/gns3server/handlers/api/compute/docker_handler.py +++ b/gns3server/handlers/api/compute/docker_handler.py @@ -62,7 +62,8 @@ async def create(request, response): console_http_path=request.json.get("console_http_path", "/"), aux=request.json.get("aux"), extra_hosts=request.json.get("extra_hosts"), - extra_volumes=request.json.get("extra_volumes")) + extra_volumes=request.json.get("extra_volumes"), + extra_parameters=request.json.get("extra_parameters")) for name, value in request.json.items(): if name != "node_id": if hasattr(container, name) and getattr(container, name) != value: @@ -317,7 +318,7 @@ async def update(request, response): props = [ "name", "console", "aux", "console_type", "console_resolution", "console_http_port", "console_http_path", "start_command", - "environment", "adapters", "extra_hosts", "extra_volumes" + "environment", "adapters", "extra_hosts", "extra_volumes", "extra_parameters" ] changed = False diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py index 6cea166a5..0d3da5b68 100644 --- a/gns3server/schemas/docker.py +++ b/gns3server/schemas/docker.py @@ -103,6 +103,11 @@ "type": "string" } }, + "extra_parameters": { + "description": "Docker extra create parameters (used in docker create)", + "type": ["string", "null"], + "minLength": 0, + }, "container_id": { "description": "Docker container ID Read only", "type": "string", @@ -214,6 +219,11 @@ "type": "string", } }, + "extra_parameters": { + "description": "Docker extra parameters ()", + "type": ["string", "null"], + "minLength": 0, + }, "node_directory": { "description": "Path to the node working directory Read only", "type": "string" diff --git a/gns3server/schemas/docker_template.py b/gns3server/schemas/docker_template.py index 0e04bbd13..08a0f37e1 100644 --- a/gns3server/schemas/docker_template.py +++ b/gns3server/schemas/docker_template.py @@ -87,6 +87,11 @@ "type": "array", "default": [] }, + "extra_parameters": { + "description": "Docker extra create parameters (used in docker create)", + "type": "string", + "default": "" + }, "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA } diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 1cefc7c22..90d2db905 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -66,6 +66,7 @@ def test_json(vm, compute_project): 'console_http_path': '/', 'extra_hosts': None, 'extra_volumes': [], + 'extra_parameters': None, 'aux': vm.aux, 'start_command': vm.start_command, 'environment': vm.environment, @@ -224,6 +225,52 @@ async def test_create_with_extra_hosts(compute_project, manager): assert "GNS3_EXTRA_HOSTS=199.199.199.1\ttest\n199.199.199.1\ttest2" in called_kwargs["data"]["Env"] assert vm._extra_hosts == extra_hosts +async def test_create_with_extra_parameters(compute_project, manager): + + extra_parameters = """{ + "StopSignal": "SIGRTMIN+3", + "HostConfig": { + "CapAdd": [ + "ALL" + ], + "Runtime": "runc", + "Privileged": false, + "Binds": [ + "/sys/fs/cgroup:/sys/fs/cgroup:ro", + "/sys/fs/fuse:/sys/fs/fuse" + ], + "Tmpfs": { + "/tmp": "", + "/run": "", + "/run/lock": "" + } + } +}""" + ObjTmpfs = { + "/tmp": "", + "/run": "", + "/run/lock": "" + } + response = { + "Id": "e90e34656806", + "Warnings": [] + } + + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", extra_parameters=extra_parameters) + await vm.create() + called_kwargs = mock.call_args[1] + assert called_kwargs["data"]["StopSignal"] == "SIGRTMIN+3" + assert ObjTmpfs == called_kwargs["data"]["HostConfig"]["Tmpfs"] # test object to Hostconfig append + assert "/sys/fs/cgroup:/sys/fs/cgroup:ro" in called_kwargs["data"]["HostConfig"]["Binds"] # test adding custom binds + assert "/sys/fs/fuse:/sys/fs/fuse" in called_kwargs["data"]["HostConfig"]["Binds"] + assert len(called_kwargs["data"]["HostConfig"]["Binds"]) > 2 # test other binds are not overwriten + assert called_kwargs["data"]["HostConfig"]["Privileged"] == False # test setting privileged + assert called_kwargs["data"]["HostConfig"]["Runtime"] == "runc" # test the addition of the runtime param + + + async def test_create_with_extra_hosts_wrong_format(compute_project, manager): extra_hosts = "test" diff --git a/tests/handlers/api/compute/test_docker.py b/tests/handlers/api/compute/test_docker.py index 217fb3b09..227625123 100644 --- a/tests/handlers/api/compute/test_docker.py +++ b/tests/handlers/api/compute/test_docker.py @@ -37,7 +37,8 @@ def base_params(): "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024", - "extra_hosts": "test:127.0.0.1" + "extra_hosts": "test:127.0.0.1", + "extra_parameters": """{"StopSignal": "SIGRTMIN+3"}""" } return params @@ -78,6 +79,7 @@ async def test_docker_create(compute_api, compute_project, base_params): assert response.json["environment"] == "YES=1\nNO=0" assert response.json["console_resolution"] == "1280x1024" assert response.json["extra_hosts"] == "test:127.0.0.1" + assert response.json["extra_parameters"] == """{"StopSignal": "SIGRTMIN+3"}""" async def test_docker_start(compute_api, vm): @@ -174,7 +176,8 @@ async def test_docker_update(compute_api, vm, free_console_port): "console": free_console_port, "start_command": "yes", "environment": "GNS3=1\nGNS4=0", - "extra_hosts": "test:127.0.0.1" + "extra_hosts": "test:127.0.0.1", + "extra_parameters": """{"StopSignal": "SIGRTMIN+3"}""" } with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock: @@ -186,6 +189,7 @@ async def test_docker_update(compute_api, vm, free_console_port): assert response.json["start_command"] == "yes" assert response.json["environment"] == "GNS3=1\nGNS4=0" assert response.json["extra_hosts"] == "test:127.0.0.1" + assert response.json["extra_parameters"] == """{"StopSignal": "SIGRTMIN+3"}""" async def test_docker_start_capture(compute_api, vm):