diff --git a/CHANGELOG b/CHANGELOG index 006554dc..059c85b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,20 @@ # Change Log +## 1.5.0rc1 01/06/2016 + +* Save an restore docker permission +* Export the list of volumes to a env variable accessible in the container +* Fix a crash when docker start command is None +* Ubridge 0.9.4 is require +* Generate a MAC address using the project + node UUID. Ref #522. +* Catch extra args in windows signal handler +* Allow to block network traffic originating from the host OS for vmnet interfaces (Windows only). +* Fix an import error when you have no GNS3 VM +* Warn if you can not export a file due to permission issue +* Do not delete adapters when stopping a VMware VM. Ref #1066. +* Allocate a new vmnet interface if vmnet 0 1 or 8 is set to a custom adapter. Set adapter type to all adapters regardless if already configured or added by GNS3. +* Set default VMware VM adapter type to e1000. + ## 1.5.0b1 23/05/2016 * Allow an IOS router to stop even the Dynamips hypervisor command fail to be sent. Ref #488. diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 45de421f..52d0681f 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import stat import logging import aiohttp import shutil @@ -230,11 +231,13 @@ class BaseNode: """ Delete the node (including all its files). """ + def set_rw(operation, name, exc): + os.chmod(name, stat.S_IWRITE) directory = self.project.node_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 node working directory: {}".format(e)) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index c1febaa7..81db36fe 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -85,6 +85,7 @@ class DockerVM(BaseNode): 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 @@ -135,8 +136,9 @@ class DockerVM(BaseNode): @start_command.setter def start_command(self, command): - command = command.strip() - if len(command) == 0: + if command: + command = command.strip() + if command is None or len(command) == 0: self._start_command = None else: self._start_command = command @@ -206,6 +208,8 @@ class DockerVM(BaseNode): 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 @@ -213,6 +217,7 @@ class DockerVM(BaseNode): 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 @@ -293,6 +298,8 @@ class DockerVM(BaseNode): # Give the information to the container on how many interface should be inside params["Env"].append("GNS3_MAX_ETHERNET=eth{}".format(self.adapters - 1)) + # Give the information to the container the list of volume path mounted + params["Env"].append("GNS3_VOLUMES={}".format(":".join(self._volumes))) if self._environment: params["Env"] += [e.strip() for e in self._environment.split("\n")] @@ -385,6 +392,25 @@ class DockerVM(BaseNode): 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", + "(/gns3/bin/busybox find \"{path}\" -depth -print0 | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c '%a:%u:%g:%n' > \"{path}/.gns3_perms\") && /gns3/bin/busybox chmod -R u+rX \"{path}\" && /gns3/bin/busybox chown {uid}:{gid} -R \"{path}\"".format(uid=os.getuid(), gid=os.getgid(), path=volume)) + yield from process.wait() + @asyncio.coroutine def _start_vnc(self): """ @@ -508,6 +534,8 @@ class DockerVM(BaseNode): 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/compute/docker/resources/init.sh b/gns3server/compute/docker/resources/init.sh index 0d67def1..d1b11d79 100755 --- a/gns3server/compute/docker/resources/init.sh +++ b/gns3server/compute/docker/resources/init.sh @@ -28,6 +28,20 @@ if [ ! -d /tmp/gns3/bin ]; then /gns3/bin/busybox --install -s /tmp/gns3/bin fi +# Restore file permission +for i in $(echo "$GNS3_VOLUMES" | tr ":" "\n") +do + if [ -f "$i/.gns3_perms" ] + then + while IFS=: read PERMS OWNER GROUP FILE + do + chmod "$PERMS" "$FILE" + chown "${OWNER}:${GROUP}" "$FILE" + done < "$i/.gns3_perms" + fi +done + + # /etc/hosts [ -s /etc/hosts ] || cat > /etc/hosts << __EOF__ 127.0.1.1 $HOSTNAME @@ -60,3 +74,4 @@ ifup -a -f # continue normal docker startup PATH="$OLD_PATH" exec "$@" + diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 9f988a9f..0dd3e5a1 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -28,7 +28,6 @@ import subprocess import shlex import asyncio import socket -import random import gns3server from gns3server.utils import parse_version @@ -493,9 +492,10 @@ class QemuVM(BaseNode): """ if not mac_address: - self._mac_address = "12:34:%02x:%02x:%02x:00" % (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + # use the node UUID to generate a random MAC address + self._mac_address = "00:%s:%s:%s:%s:00" % (self.project.id[-4:-2], self.project.id[-2:], self.id[-4:-2], self.id[-2:]) else: - self._mac_address = mac_address[:8] + ":%02x:%02x:00" % (random.randint(0, 255), random.randint(0, 255)) + self._mac_address = mac_address log.info('QEMU VM "{name}" [{id}]: MAC address changed to {mac_addr}'.format(name=self._name, id=self._id, diff --git a/gns3server/ubridge/hypervisor.py b/gns3server/ubridge/hypervisor.py index dee11e83..4feafbda 100644 --- a/gns3server/ubridge/hypervisor.py +++ b/gns3server/ubridge/hypervisor.py @@ -120,15 +120,15 @@ class Hypervisor(UBridgeHypervisor): @asyncio.coroutine def _check_ubridge_version(self): """ - Checks if the ubridge executable version is >= 0.9.3 + Checks if the ubridge executable version is >= 0.9.4 """ try: output = yield from subprocess_check_output(self._path, "-v", cwd=self._working_dir) match = re.search("ubridge version ([0-9a-z\.]+)", output) if match: version = match.group(1) - if parse_version(version) < parse_version("0.9.3"): - raise UbridgeError("uBridge executable version must be >= 0.9.3") + if parse_version(version) < parse_version("0.9.4"): + raise UbridgeError("uBridge executable version must be >= 0.9.4") else: raise UbridgeError("Could not determine uBridge version for {}".format(self._path)) except (OSError, subprocess.SubprocessError) as e: 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/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index d242ee59..dd9507d7 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -105,8 +105,9 @@ def test_create(loop, project, manager): "Hostname": "test", "Image": "ubuntu:latest", "Env": [ - "GNS3_MAX_ETHERNET=eth0" - ], + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" + ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] }) @@ -142,8 +143,9 @@ def test_create_with_tag(loop, project, manager): "Hostname": "test", "Image": "ubuntu:16.04", "Env": [ - "GNS3_MAX_ETHERNET=eth0" - ], + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" + ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] }) @@ -184,8 +186,9 @@ def test_create_vnc(loop, project, manager): "Image": "ubuntu:latest", "Env": [ "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network", "DISPLAY=:42" - ], + ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] }) @@ -226,8 +229,9 @@ def test_create_start_cmd(loop, project, manager): "Hostname": "test", "Image": "ubuntu:latest", "Env": [ - "GNS3_MAX_ETHERNET=eth0" - ] + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" + ] }) assert vm._cid == "e90e34656806" @@ -258,6 +262,7 @@ def test_create_environment(loop, project, manager): }, "Env": [ "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network", "YES=1", "NO=0" ], @@ -315,8 +320,9 @@ def test_create_image_not_available(loop, project, manager): "Hostname": "test", "Image": "ubuntu:latest", "Env": [ - "GNS3_MAX_ETHERNET=eth0" - ], + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" + ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] }) @@ -479,12 +485,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.compute.docker.DockerVM._get_container_state", return_value="running"): with asyncio_patch("gns3server.compute.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): @@ -532,7 +540,8 @@ def test_update(loop, vm): "Hostname": "test", "Image": "ubuntu:latest", "Env": [ - "GNS3_MAX_ETHERNET=eth0" + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] @@ -599,7 +608,8 @@ def test_update_running(loop, vm): "Hostname": "test", "Image": "ubuntu:latest", "Env": [ - "GNS3_MAX_ETHERNET=eth0" + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network" ], "Entrypoint": ["/gns3/init.sh"], "Cmd": ["/bin/sh"] @@ -869,6 +879,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 +904,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 +919,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', '(/gns3/bin/busybox find "/etc" -depth -print0 | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c \'%a:%u:%g:%n\' > "/etc/.gns3_perms") && /gns3/bin/busybox chmod -R u+rX "/etc" && /gns3/bin/busybox chown {}:{} -R "/etc"'.format(os.getuid(), os.getgid())) + assert process.wait.called diff --git a/tests/compute/test_project.py b/tests/compute/test_project.py index 1ee6f1fb..82bd868f 100644 --- a/tests/compute/test_project.py +++ b/tests/compute/test_project.py @@ -129,6 +129,9 @@ def test_commit(manager, loop): def test_commit_permission_issue(manager, loop): + """ + GNS3 will fix the permission and continue to delete + """ project = Project(project_id=str(uuid4())) node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) project.add_node(node) @@ -137,9 +140,7 @@ def test_commit_permission_issue(manager, loop): assert len(project._nodes_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):