diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index a676a487..b5030c26 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import stat import logging import aiohttp import shutil @@ -224,11 +225,13 @@ class BaseVM: """ Delete the VM (including all its files). """ + def set_rw(operation, name, exc): + os.chmod(name, stat.S_IWRITE) directory = self.project.vm_working_directory(self) if os.path.exists(directory): try: - yield from wait_run_in_executor(shutil.rmtree, directory) + yield from wait_run_in_executor(shutil.rmtree, directory, onerror=set_rw) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not delete the VM working directory: {}".format(e)) diff --git a/gns3server/modules/docker/docker_vm.py b/gns3server/modules/docker/docker_vm.py index 95656efa..526d8299 100644 --- a/gns3server/modules/docker/docker_vm.py +++ b/gns3server/modules/docker/docker_vm.py @@ -80,6 +80,7 @@ class DockerVM(BaseVM): self._console_http_path = console_http_path self._console_http_port = console_http_port self._console_websocket = None + self._volumes = [] if adapters is None: self.adapters = 1 @@ -203,6 +204,8 @@ class DockerVM(BaseVM): network_config = self._create_network_config() binds.append("{}:/etc/network:rw".format(network_config)) + self._volumes = ["/etc/network"] + volumes = image_infos.get("ContainerConfig", {}).get("Volumes") if volumes is None: return binds @@ -210,6 +213,7 @@ class DockerVM(BaseVM): source = os.path.join(self.working_dir, os.path.relpath(volume, "/")) os.makedirs(source, exist_ok=True) binds.append("{}:{}".format(source, volume)) + self._volumes.append(volume) return binds @@ -380,6 +384,25 @@ class DockerVM(BaseVM): self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux))) log.debug("Docker container '%s' started listen for auxilary telnet on %d", self.name, self.aux) + @asyncio.coroutine + def _fix_permissions(self): + """ + Because docker run as root we need to fix permission and ownership to allow user to interact + with it from their filesystem and do operation like file delete + """ + for volume in self._volumes: + log.debug("Docker container '{name}' [{image}] fix ownership on {path}".format( + name=self._name, image=self._image, path=volume)) + process = yield from asyncio.subprocess.create_subprocess_exec( + "docker", + "exec", + self._cid, + "/gns3/bin/busybox", + "sh", + "-c", + "chmod -R u+rX {path} && chown {uid}:{gid} -R {path}".format(uid=os.getuid(), gid=os.getgid(), path=volume)) + yield from process.wait() + @asyncio.coroutine def _start_vnc(self): """ @@ -504,6 +527,8 @@ class DockerVM(BaseVM): if state == "paused": yield from self.unpause() + yield from self._fix_permissions() + # t=5 number of seconds to wait before killing the container try: yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5}) diff --git a/gns3server/modules/docker/resources/init.sh b/gns3server/modules/docker/resources/init.sh index 0d67def1..6f2b67a5 100755 --- a/gns3server/modules/docker/resources/init.sh +++ b/gns3server/modules/docker/resources/init.sh @@ -60,3 +60,4 @@ ifup -a -f # continue normal docker startup PATH="$OLD_PATH" exec "$@" + diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index fc0c2d00..c7dcc880 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -16,24 +16,26 @@ # along with this program. If not, see . +import functools import asyncio import sys import os @asyncio.coroutine -def wait_run_in_executor(func, *args): +def wait_run_in_executor(func, *args, **kwargs): """ Run blocking code in a different thread and wait for the result. :param func: Run this function in a different thread :param args: Parameters of the function + :param kwargs: Keyword parameters of the function :returns: Return the result of the function """ loop = asyncio.get_event_loop() - future = loop.run_in_executor(None, func, *args) + future = loop.run_in_executor(None, functools.partial(func, *args, **kwargs)) yield from asyncio.wait([future]) return future.result() diff --git a/tests/modules/docker/test_docker_vm.py b/tests/modules/docker/test_docker_vm.py index 0d6f8d38..d4b0bfe3 100644 --- a/tests/modules/docker/test_docker_vm.py +++ b/tests/modules/docker/test_docker_vm.py @@ -105,7 +105,7 @@ def test_create(loop, project, manager): "Image": "ubuntu:latest", "Env": [ "GNS3_MAX_ETHERNET=eth0" - ], + ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] }) @@ -479,12 +479,14 @@ def test_restart(loop, vm): def test_stop(loop, vm): vm._ubridge_hypervisor = MagicMock() vm._ubridge_hypervisor.is_running.return_value = True + vm._fix_permissions = MagicMock() with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="running"): with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query: loop.run_until_complete(asyncio.async(vm.stop())) mock_query.assert_called_with("POST", "containers/e90e34656842/stop", params={"t": 5}) assert vm._ubridge_hypervisor.stop.called + assert vm._fix_permissions.called def test_stop_paused_container(loop, vm): @@ -869,6 +871,7 @@ def test_mount_binds(vm, tmpdir): "{}:{}".format(dst, "/test/experimental") ] + assert vm._volumes == ["/etc/network", "/test/experimental"] assert os.path.exists(dst) @@ -893,6 +896,7 @@ def test_start_aux(vm, loop): with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=MagicMock()) as mock_exec: loop.run_until_complete(asyncio.async(vm._start_aux())) + mock_exec.assert_called_with('docker', 'exec', '-i', 'e90e34656842', '/gns3/bin/busybox', 'script', '-qfc', '/gns3/bin/busybox sh', '/dev/null', stderr=asyncio.subprocess.STDOUT, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE) def test_create_network_interfaces(vm): @@ -907,3 +911,12 @@ def test_create_network_interfaces(vm): assert "eth0" in content assert "eth4" in content assert "eth5" not in content + + +def test_fix_permission(vm, loop): + vm._volumes = ["/etc"] + process = MagicMock() + with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=process) as mock_exec: + loop.run_until_complete(vm._fix_permissions()) + mock_exec.assert_called_with('docker', 'exec', 'e90e34656842', '/gns3/bin/busybox', 'sh', '-c', 'chmod -R u+rX /etc && chown {}:{} -R /etc'.format(os.getuid(), os.getgid())) + assert process.wait.called diff --git a/tests/modules/test_project.py b/tests/modules/test_project.py index 7412d261..c170879a 100644 --- a/tests/modules/test_project.py +++ b/tests/modules/test_project.py @@ -147,6 +147,9 @@ def test_commit(manager, loop): def test_commit_permission_issue(manager, loop): + """ + GNS3 will fix the permission and continue to delete + """ project = Project() vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_vm(vm) @@ -155,9 +158,7 @@ def test_commit_permission_issue(manager, loop): assert len(project._vms_to_destroy) == 1 assert os.path.exists(directory) os.chmod(directory, 0) - with pytest.raises(aiohttp.web.HTTPInternalServerError): - loop.run_until_complete(asyncio.async(project.commit())) - os.chmod(directory, 700) + loop.run_until_complete(asyncio.async(project.commit())) def test_project_delete(loop):