Merge branch '1.5' into 2.0

This commit is contained in:
Julien Duponchelle 2016-06-02 13:47:40 +02:00
commit cb7dbfb256
No known key found for this signature in database
GPG Key ID: CE8B29639E07F5E8
9 changed files with 110 additions and 25 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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