diff --git a/gns3server/modules/docker/docker_vm.py b/gns3server/modules/docker/docker_vm.py index 3ae58910..56768197 100644 --- a/gns3server/modules/docker/docker_vm.py +++ b/gns3server/modules/docker/docker_vm.py @@ -159,6 +159,10 @@ class DockerVM(BaseVM): binds.append("{}:/gns3:ro".format(get_resource("modules/docker/resources"))) + # We mount our own etc/network + network_config = self._create_network_config() + binds.append("{}:/etc/network:rw".format(network_config)) + volumes = image_infos.get("ContainerConfig", {}).get("Volumes") if volumes is None: return binds @@ -169,6 +173,37 @@ class DockerVM(BaseVM): return binds + def _create_network_config(self): + """ + If network config is empty we create a sample config + """ + path = os.path.join(self.working_dir, "etc", "network") + os.makedirs(path, exist_ok=True) + os.makedirs(os.path.join(path, "if-up.d"), exist_ok=True) + os.makedirs(os.path.join(path, "if-down.d"), exist_ok=True) + + if not os.path.exists(os.path.join(path, "interfaces")): + with open(os.path.join(path, "interfaces"), "w+") as f: + f.write("""# +# This is a sample network config uncomment lines to configure the network +# + +""") + for adapter in range(0, self.adapters): + f.write(""" +# Static config for eth{adapter} +#auto eth{adapter} +#iface eth{adapter} inet static +#\taddress 192.168.{adapter}.2 +#\tnetmask 255.255.255.0 +#\tgateway 192.168.{adapter}.1 +#\tup echo nameserver 192.168.{adapter}.1 > /etc/resolv.conf + +# DHCP config for eth{adapter} +# auto eth{adapter} +# iface eth{adapter} inet dhcp""".format(adapter=adapter)) + return path + @asyncio.coroutine def create(self): """Creates the Docker container.""" @@ -199,7 +234,6 @@ class DockerVM(BaseVM): "Entrypoint": image_infos.get("Config", {"Entrypoint": []})["Entrypoint"] } - if params["Entrypoint"] is None: params["Entrypoint"] = [] if self._start_command: @@ -287,7 +321,7 @@ class DockerVM(BaseVM): # We can not use the API because docker doesn't expose a websocket api for exec # https://github.com/GNS3/gns3-gui/issues/1039 process = yield from asyncio.subprocess.create_subprocess_exec( - "docker", "exec", "-i", self._cid, "/bin/sh", "-i", + "docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "sh", "-i", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, stdin=asyncio.subprocess.PIPE) diff --git a/gns3server/modules/docker/resources/bin/busybox b/gns3server/modules/docker/resources/bin/busybox new file mode 100755 index 00000000..a9acbe24 Binary files /dev/null and b/gns3server/modules/docker/resources/bin/busybox differ diff --git a/gns3server/modules/docker/resources/bin/udhcpc b/gns3server/modules/docker/resources/bin/udhcpc new file mode 100755 index 00000000..c9d2f2be --- /dev/null +++ b/gns3server/modules/docker/resources/bin/udhcpc @@ -0,0 +1,3 @@ +#!/tmp/gns3/bin/sh + +exec busybox udhcpc -s /gns3/etc/udhcpc/default.script "$@" diff --git a/gns3server/modules/docker/resources/bin/udhcpc6 b/gns3server/modules/docker/resources/bin/udhcpc6 new file mode 100755 index 00000000..3d49d801 --- /dev/null +++ b/gns3server/modules/docker/resources/bin/udhcpc6 @@ -0,0 +1,3 @@ +#!/tmp/gns3/bin/sh + +exec busybox udhcpc6 -s /gns3/etc/udhcpc/default.script "$@" diff --git a/gns3server/modules/docker/resources/etc/udhcpc/default.script b/gns3server/modules/docker/resources/etc/udhcpc/default.script new file mode 100755 index 00000000..404fb190 --- /dev/null +++ b/gns3server/modules/docker/resources/etc/udhcpc/default.script @@ -0,0 +1,138 @@ +#!/tmp/gns3/bin/sh + +# script for udhcpc +# Copyright (c) 2008 Natanael Copa + +UDHCPC="/gns3/etc/udhcpc" +UDHCPC_CONF="$UDHCPC/udhcpc.conf" + +RESOLV_CONF="/etc/resolv.conf" +[ -f $UDHCPC_CONF ] && . $UDHCPC_CONF + +export broadcast +export dns +export domain +export interface +export ip +export mask +export metric +export router +export subnet + +#export PATH=/usr/bin:/bin:/usr/sbin:/sbin + +run_scripts() { + local dir=$1 + if [ -d $dir ]; then + for i in $dir/*; do + [ -f $i ] && $i + done + fi +} + +deconfig() { + ip addr flush dev $interface +} + +is_wifi() { + test -e /sys/class/net/$interface/phy80211 +} + +if_index() { + if [ -e /sys/class/net/$interface/ifindex ]; then + cat /sys/class/net/$interface/ifindex + else + ip link show dev $interface | head -n1 | cut -d: -f1 + fi +} + +calc_metric() { + local base= + if is_wifi; then + base=300 + else + base=200 + fi + echo $(( $base + $(if_index) )) +} + +routes() { + [ -z "$router" ] && return + local gw= num= + while ip route del default via dev $interface 2>/dev/null; do + : + done + num=0 + for gw in $router; do + ip route add 0.0.0.0/0 via $gw dev $interface \ + metric $(( $num + ${IF_METRIC:-$(calc_metric)} )) + num=$(( $num + 1 )) + done +} + +resolvconf() { + local i + [ -n "$IF_PEER_DNS" ] && [ "$IF_PEER_DNS" != "yes" ] && return + if [ "$RESOLV_CONF" = "no" ] || [ "$RESOLV_CONF" = "NO" ] \ + || [ -z "$RESOLV_CONF" ]; then + return + fi + echo -n > "$RESOLV_CONF" + [ -n "$domain" ] && echo "search $domain" >> "$RESOLV_CONF" + for i in $dns; do + echo "nameserver $i" >> "$RESOLV_CONF" + done +} + +bound() { + ip addr add $ip/$mask ${broadcast:+broadcast $broadcast} dev $interface + ip link set dev $interface up + routes + resolvconf +} + +renew() { + if ! ip addr show dev $interface | grep $ip/$mask; then + ip addr flush dev $interface + ip addr add $ip/$mask ${broadcast:+broadcast $broadcast} dev $interface + fi + + local i + for i in $router; do + if ! ip route show | grep ^default | grep $i; then + routes + break + fi + done + + if ! grep "^search $domain"; then + resolvconf + return + fi + for i in $dns; do + if ! grep "^nameserver $i"; then + resolvconf + return + fi + done +} + +case "$1" in + deconfig|renew|bound) + run_scripts $UDHCPC/pre-$1 + $1 + run_scripts $UDHCPC/post-$1 + ;; + leasefail) + echo "udhcpc failed to get a DHCP lease" >&2 + ;; + nak) + echo "udhcpc received DHCP NAK" >&2 + ;; + *) + echo "Error: this script should be called from udhcpc" >&2 + exit 1 + ;; +esac +exit 0 + diff --git a/gns3server/modules/docker/resources/init.sh b/gns3server/modules/docker/resources/init.sh index abd3aa4f..bdaa1c42 100755 --- a/gns3server/modules/docker/resources/init.sh +++ b/gns3server/modules/docker/resources/init.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/gns3/bin/busybox sh # # Copyright (C) 2016 GNS3 Technologies Inc. # @@ -19,6 +19,16 @@ # This script is injected into the container and launch before # the start command of the container # +OLD_PATH="$PATH" +PATH=/gns3/bin:/tmp/gns3/bin + +# bootstrap busybox commands +if [ ! -d /tmp/gns3/bin ]; then + busybox mkdir -p /tmp/gns3/bin + /gns3/bin/busybox --install -s /tmp/gns3/bin + # remove commands already available in /gns3/bin + (cd /tmp/gns3/bin; rm -f `cd /gns3/bin; echo *`) +fi # Wait 2 seconds to settle the network interfaces sleep 2 @@ -42,5 +52,9 @@ sed -n 's/^ *\(eth[0-9]*\):.*/\1/p' < /proc/net/dev | while read dev; do ip link set dev $dev up done + +ifup -a -f + # continue normal docker startup +PATH="$OLD_PATH" exec "$@" diff --git a/tests/modules/docker/test_docker_vm.py b/tests/modules/docker/test_docker_vm.py index 2d6113f5..0a6a3a82 100644 --- a/tests/modules/docker/test_docker_vm.py +++ b/tests/modules/docker/test_docker_vm.py @@ -89,7 +89,10 @@ def test_create(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("modules/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -126,6 +129,7 @@ def test_create_vnc(loop, project, manager): "CapAdd": ["ALL"], "Binds": [ "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")), '/tmp/.X11-unix/:/tmp/.X11-unix/' ], "Privileged": True @@ -161,7 +165,10 @@ def test_create_start_cmd(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("modules/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -194,7 +201,10 @@ def test_create_environment(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("modules/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Env": ["YES=1", "NO=0"], @@ -241,7 +251,10 @@ def test_create_image_not_available(loop, project, manager): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("modules/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -452,7 +465,10 @@ def test_update(loop, vm): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("modules/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -490,7 +506,10 @@ def test_update_running(loop, vm): "HostConfig": { "CapAdd": ["ALL"], - "Binds": ["{}:/gns3:ro".format(get_resource("modules/docker/resources"))], + "Binds": [ + "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")) + ], "Privileged": True }, "Volumes": {}, @@ -763,6 +782,7 @@ def test_mount_binds(vm, tmpdir): dst = os.path.join(vm.working_dir, "test/experimental") assert vm._mount_binds(image_infos) == [ "{}:/gns3:ro".format(get_resource("modules/docker/resources")), + "{}:/etc/network:rw".format(os.path.join(vm.working_dir, "etc", "network")), "{}:{}".format(dst, "/test/experimental") ] @@ -789,3 +809,17 @@ 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())) + + +def test_create_network_interfaces(vm): + + vm.adapters = 5 + network_config = vm._create_network_config() + assert os.path.exists(os.path.join(network_config, "interfaces")) + assert os.path.exists(os.path.join(network_config, "if-up.d")) + + with open(os.path.join(network_config, "interfaces")) as f: + content = f.read() + assert "eth0" in content + assert "eth4" in content + assert "eth5" not in content