Fix permissions and ownership when stopping container

Fix #550
This commit is contained in:
Julien Duponchelle 2016-05-31 21:08:41 +02:00
parent 6f7b06e66f
commit d3436756b2
No known key found for this signature in database
GPG Key ID: CE8B29639E07F5E8
6 changed files with 52 additions and 7 deletions

View File

@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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))

View File

@ -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})

View File

@ -60,3 +60,4 @@ ifup -a -f
# continue normal docker startup
PATH="$OLD_PATH"
exec "$@"

View File

@ -16,24 +16,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()

View File

@ -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

View File

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