1
0
mirror of https://github.com/GNS3/gns3-server synced 2025-03-01 17:02:04 +00:00

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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
import stat
import logging import logging
import aiohttp import aiohttp
import shutil import shutil
@ -224,11 +225,13 @@ class BaseVM:
""" """
Delete the VM (including all its files). 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) directory = self.project.vm_working_directory(self)
if os.path.exists(directory): if os.path.exists(directory):
try: 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: except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not delete the VM working directory: {}".format(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_path = console_http_path
self._console_http_port = console_http_port self._console_http_port = console_http_port
self._console_websocket = None self._console_websocket = None
self._volumes = []
if adapters is None: if adapters is None:
self.adapters = 1 self.adapters = 1
@ -203,6 +204,8 @@ class DockerVM(BaseVM):
network_config = self._create_network_config() network_config = self._create_network_config()
binds.append("{}:/etc/network:rw".format(network_config)) binds.append("{}:/etc/network:rw".format(network_config))
self._volumes = ["/etc/network"]
volumes = image_infos.get("ContainerConfig", {}).get("Volumes") volumes = image_infos.get("ContainerConfig", {}).get("Volumes")
if volumes is None: if volumes is None:
return binds return binds
@ -210,6 +213,7 @@ class DockerVM(BaseVM):
source = os.path.join(self.working_dir, os.path.relpath(volume, "/")) source = os.path.join(self.working_dir, os.path.relpath(volume, "/"))
os.makedirs(source, exist_ok=True) os.makedirs(source, exist_ok=True)
binds.append("{}:{}".format(source, volume)) binds.append("{}:{}".format(source, volume))
self._volumes.append(volume)
return binds 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))) 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) 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 @asyncio.coroutine
def _start_vnc(self): def _start_vnc(self):
""" """
@ -504,6 +527,8 @@ class DockerVM(BaseVM):
if state == "paused": if state == "paused":
yield from self.unpause() yield from self.unpause()
yield from self._fix_permissions()
# t=5 number of seconds to wait before killing the container # t=5 number of seconds to wait before killing the container
try: try:
yield from self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5}) 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 # continue normal docker startup
PATH="$OLD_PATH" PATH="$OLD_PATH"
exec "$@" exec "$@"

View File

@ -16,24 +16,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import functools
import asyncio import asyncio
import sys import sys
import os import os
@asyncio.coroutine @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 Run blocking code in a different thread and wait
for the result. for the result.
:param func: Run this function in a different thread :param func: Run this function in a different thread
:param args: Parameters of the function :param args: Parameters of the function
:param kwargs: Keyword parameters of the function
:returns: Return the result of the function :returns: Return the result of the function
""" """
loop = asyncio.get_event_loop() 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]) yield from asyncio.wait([future])
return future.result() return future.result()

View File

@ -105,7 +105,7 @@ def test_create(loop, project, manager):
"Image": "ubuntu:latest", "Image": "ubuntu:latest",
"Env": [ "Env": [
"GNS3_MAX_ETHERNET=eth0" "GNS3_MAX_ETHERNET=eth0"
], ],
"Entrypoint": ["/gns3/init.sh"], "Entrypoint": ["/gns3/init.sh"],
"Cmd": ["/bin/sh"] "Cmd": ["/bin/sh"]
}) })
@ -479,12 +479,14 @@ def test_restart(loop, vm):
def test_stop(loop, vm): def test_stop(loop, vm):
vm._ubridge_hypervisor = MagicMock() vm._ubridge_hypervisor = MagicMock()
vm._ubridge_hypervisor.is_running.return_value = True 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.DockerVM._get_container_state", return_value="running"):
with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query: with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query:
loop.run_until_complete(asyncio.async(vm.stop())) loop.run_until_complete(asyncio.async(vm.stop()))
mock_query.assert_called_with("POST", "containers/e90e34656842/stop", params={"t": 5}) mock_query.assert_called_with("POST", "containers/e90e34656842/stop", params={"t": 5})
assert vm._ubridge_hypervisor.stop.called assert vm._ubridge_hypervisor.stop.called
assert vm._fix_permissions.called
def test_stop_paused_container(loop, vm): def test_stop_paused_container(loop, vm):
@ -869,6 +871,7 @@ def test_mount_binds(vm, tmpdir):
"{}:{}".format(dst, "/test/experimental") "{}:{}".format(dst, "/test/experimental")
] ]
assert vm._volumes == ["/etc/network", "/test/experimental"]
assert os.path.exists(dst) 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: with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=MagicMock()) as mock_exec:
loop.run_until_complete(asyncio.async(vm._start_aux())) 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): def test_create_network_interfaces(vm):
@ -907,3 +911,12 @@ def test_create_network_interfaces(vm):
assert "eth0" in content assert "eth0" in content
assert "eth4" in content assert "eth4" in content
assert "eth5" not 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): def test_commit_permission_issue(manager, loop):
"""
GNS3 will fix the permission and continue to delete
"""
project = Project() project = Project()
vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) vm = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager)
project.add_vm(vm) project.add_vm(vm)
@ -155,9 +158,7 @@ def test_commit_permission_issue(manager, loop):
assert len(project._vms_to_destroy) == 1 assert len(project._vms_to_destroy) == 1
assert os.path.exists(directory) assert os.path.exists(directory)
os.chmod(directory, 0) os.chmod(directory, 0)
with pytest.raises(aiohttp.web.HTTPInternalServerError): loop.run_until_complete(asyncio.async(project.commit()))
loop.run_until_complete(asyncio.async(project.commit()))
os.chmod(directory, 700)
def test_project_delete(loop): def test_project_delete(loop):