From d1c29c8bb78e0ee5e00cad04d804416604b5c47d Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 18 Jul 2020 21:03:55 +0930 Subject: [PATCH] Resource constraints for Docker VMs. --- gns3server/compute/docker/docker_vm.py | 28 +++- .../handlers/api/compute/docker_handler.py | 33 ++-- gns3server/schemas/docker.py | 16 ++ gns3server/schemas/docker_template.py | 10 ++ tests/compute/docker/test_docker_vm.py | 144 ++++++++++++++++-- 5 files changed, 200 insertions(+), 31 deletions(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 940aeab6..6b3a644e 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -71,7 +71,7 @@ class DockerVM(BaseNode): 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=[], memory=0, cpus=0): super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type) @@ -94,6 +94,8 @@ class DockerVM(BaseNode): self._console_websocket = None self._extra_hosts = extra_hosts self._extra_volumes = extra_volumes or [] + self._memory = memory + self._cpus = cpus self._permissions_fixed = False self._display = None self._closing = False @@ -132,6 +134,8 @@ class DockerVM(BaseNode): "node_directory": self.working_path, "extra_hosts": self.extra_hosts, "extra_volumes": self.extra_volumes, + "memory": self.memory, + "cpus": self.cpus } def _get_free_display_port(self): @@ -211,6 +215,22 @@ class DockerVM(BaseNode): def extra_volumes(self, extra_volumes): self._extra_volumes = extra_volumes + @property + def memory(self): + return self._memory + + @memory.setter + def memory(self, memory): + self._memory = memory + + @property + def cpus(self): + return self._cpus + + @cpus.setter + def cpus(self, cpus): + self._cpus = cpus + async def _get_container_state(self): """ Returns the container state (e.g. running, paused etc.) @@ -328,6 +348,10 @@ class DockerVM(BaseNode): if image_infos is None: raise DockerError("Cannot get information for image '{}', please try again.".format(self._image)) + available_cpus = psutil.cpu_count(logical=True) + if self._cpus > available_cpus: + raise DockerError("You have allocated too many CPUs for the Docker container (max available is {} CPUs)".format(available_cpus)) + params = { "Hostname": self._name, "Name": self._name, @@ -340,6 +364,8 @@ class DockerVM(BaseNode): "CapAdd": ["ALL"], "Privileged": True, "Binds": self._mount_binds(image_infos), + "Memory": self._memory * (1024 * 1024), # convert memory to bytes + "NanoCpus": int(self._cpus * 1e9) # convert cpus to nano cpus }, "Volumes": {}, "Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573 diff --git a/gns3server/handlers/api/compute/docker_handler.py b/gns3server/handlers/api/compute/docker_handler.py index e510a9f2..b7935071 100644 --- a/gns3server/handlers/api/compute/docker_handler.py +++ b/gns3server/handlers/api/compute/docker_handler.py @@ -49,20 +49,22 @@ class DockerHandler: async def create(request, response): docker_manager = Docker.instance() container = await docker_manager.create_node(request.json.pop("name"), - request.match_info["project_id"], - request.json.get("node_id"), - image=request.json.pop("image"), - start_command=request.json.get("start_command"), - environment=request.json.get("environment"), - adapters=request.json.get("adapters"), - console=request.json.get("console"), - console_type=request.json.get("console_type"), - console_resolution=request.json.get("console_resolution", "1024x768"), - console_http_port=request.json.get("console_http_port", 80), - 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")) + request.match_info["project_id"], + request.json.get("node_id"), + image=request.json.pop("image"), + start_command=request.json.get("start_command"), + environment=request.json.get("environment"), + adapters=request.json.get("adapters"), + console=request.json.get("console"), + console_type=request.json.get("console_type"), + console_resolution=request.json.get("console_resolution", "1024x768"), + console_http_port=request.json.get("console_http_port", 80), + 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"), + memory=request.json.get("memory", 0), + cpus=request.json.get("cpus", 0)) for name, value in request.json.items(): if name != "node_id": if hasattr(container, name) and getattr(container, name) != value: @@ -317,7 +319,8 @@ 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", "extra_hosts", "extra_volumes", + "memory", "cpus" ] changed = False diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py index 6cea166a..db56a61a 100644 --- a/gns3server/schemas/docker.py +++ b/gns3server/schemas/docker.py @@ -103,6 +103,14 @@ DOCKER_CREATE_SCHEMA = { "type": "string" } }, + "memory": { + "description": "Maximum amount of memory the container can use in MB", + "type": "integer", + }, + "cpus": { + "description": "Maximum amount of CPU resources the container can use", + "type": "number", + }, "container_id": { "description": "Docker container ID Read only", "type": "string", @@ -214,6 +222,14 @@ DOCKER_OBJECT_SCHEMA = { "type": "string", } }, + "memory": { + "description": "Maximum amount of memory the container can use in MB", + "type": "integer", + }, + "cpus": { + "description": "Maximum amount of CPU resources the container can use", + "type": "number", + }, "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 dd569bcb..40735cb2 100644 --- a/gns3server/schemas/docker_template.py +++ b/gns3server/schemas/docker_template.py @@ -82,6 +82,16 @@ DOCKER_TEMPLATE_PROPERTIES = { "type": "array", "default": [] }, + "memory": { + "description": "Maximum amount of memory the container can use in MB", + "type": "integer", + "default": 0 + }, + "cpus": { + "description": "Maximum amount of CPU resources the container can use", + "type": "number", + "default": 0 + }, "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 1cefc7c2..1f639fb3 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -66,6 +66,8 @@ def test_json(vm, compute_project): 'console_http_path': '/', 'extra_hosts': None, 'extra_volumes': [], + 'memory': 0, + 'cpus': 0, 'aux': vm.aux, 'start_command': vm.start_command, 'environment': vm.environment, @@ -104,7 +106,9 @@ async def test_create(compute_project, manager): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -143,7 +147,9 @@ async def test_create_with_tag(compute_project, manager): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -186,7 +192,9 @@ async def test_create_vnc(compute_project, manager): "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), '/tmp/.X11-unix/:/tmp/.X11-unix/' ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -301,7 +309,9 @@ async def test_create_start_cmd(compute_project, manager): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "Entrypoint": ["/gns3/init.sh"], @@ -400,7 +410,9 @@ async def test_create_image_not_available(compute_project, manager): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -444,7 +456,9 @@ async def test_create_with_user(compute_project, manager): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -528,7 +542,9 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -568,7 +584,9 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -608,7 +626,9 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -648,7 +668,9 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")), "{}:/gns3volumes/vol".format(os.path.join(vm.working_dir, "vol")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -687,7 +709,9 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -726,7 +750,9 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc".format(os.path.join(vm.working_dir, "etc")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -773,7 +799,9 @@ async def test_create_with_extra_volumes(compute_project, manager): "{}:/gns3volumes/vol/1".format(os.path.join(vm.working_dir, "vol", "1")), "{}:/gns3volumes/vol/2".format(os.path.join(vm.working_dir, "vol", "2")), ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -996,7 +1024,9 @@ async def test_update(vm): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -1065,7 +1095,9 @@ async def test_update_running(vm): "{}:/gns3:ro".format(get_resource("compute/docker/resources")), "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) ], - "Privileged": True + "Privileged": True, + "Memory": 0, + "NanoCpus": 0 }, "Volumes": {}, "NetworkDisabled": True, @@ -1422,3 +1454,85 @@ async def test_read_console_output_with_binary_mode(vm): with asyncio_patch('gns3server.compute.docker.docker_vm.DockerVM.stop'): await vm._read_console_output(input_stream, output_stream) output_stream.feed_data.assert_called_once_with(b"test") + + +async def test_cpus(compute_project, manager): + + 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:latest", cpus=0.5) + await vm.create() + mock.assert_called_with("POST", "containers/create", data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": + { + "CapAdd": ["ALL"], + "Binds": [ + "{}:/gns3:ro".format(get_resource("compute/docker/resources")), + "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) + ], + "Privileged": True, + "Memory": 0, + "NanoCpus": 500000000 + }, + "Volumes": {}, + "NetworkDisabled": True, + "Name": "test", + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": [ + "container=docker", + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" + ], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"] + }) + assert vm._cid == "e90e34656806" + + +async def test_memory(compute_project, manager): + + 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:latest", memory=32) + await vm.create() + mock.assert_called_with("POST", "containers/create", data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": + { + "CapAdd": ["ALL"], + "Binds": [ + "{}:/gns3:ro".format(get_resource("compute/docker/resources")), + "{}:/gns3volumes/etc/network".format(os.path.join(vm.working_dir, "etc", "network")) + ], + "Privileged": True, + "Memory": 33554432, # 32MB in bytes + "NanoCpus": 0 + }, + "Volumes": {}, + "NetworkDisabled": True, + "Name": "test", + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": [ + "container=docker", + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" + ], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"] + }) + assert vm._cid == "e90e34656806"