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):