Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker enhancement to grant the user the ability to modify the contai… #1877

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions gns3server/compute/docker/docker_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.)
Expand Down Expand Up @@ -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']
Expand All @@ -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):
"""
Expand Down
5 changes: 3 additions & 2 deletions gns3server/handlers/api/compute/docker_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions gns3server/schemas/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions gns3server/schemas/docker_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
47 changes: 47 additions & 0 deletions tests/compute/docker/test_docker_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 6 additions & 2 deletions tests/handlers/api/compute/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down