From 5fdd33fe3b99b570bc588d8e286088df9e41f032 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 30 Aug 2016 16:38:19 +0200 Subject: [PATCH] Repair VirtualBox VM support Fix #650 --- gns3server/controller/gns3vm/__init__.py | 5 +- .../controller/gns3vm/virtualbox_gns3_vm.py | 88 +++++++++++-------- gns3server/run.py | 1 + .../gns3vm/test_virtualbox_gns3_vm.py | 59 +++++++++++++ 4 files changed, 114 insertions(+), 39 deletions(-) create mode 100644 tests/controller/gns3vm/test_virtualbox_gns3_vm.py diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index 1451adc4..8764a735 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -217,7 +217,10 @@ class GNS3VM: @asyncio.coroutine def auto_stop_vm(self): if self.enable and self.auto_stop: - yield from self._stop() + try: + yield from self._stop() + except GNS3VMError as e: + log.warn(str(e)) @asyncio.coroutine def _start(self): diff --git a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py index d066c975..544147f8 100644 --- a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py +++ b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json.decoder +import aiohttp import logging import asyncio import socket @@ -34,8 +36,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): def __init__(self, controller): - super().__init__(controller) self._engine = "virtualbox" + super().__init__(controller) self._virtualbox_manager = VirtualBox() @asyncio.coroutine @@ -43,7 +45,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): try: result = yield from self._virtualbox_manager.execute(subcommand, args, timeout) - return (''.join(result)) + return ("\n".join(result)) except VirtualBoxError as e: raise GNS3VMError("Error while executing VBoxManage command: {}".format(e)) @@ -157,20 +159,20 @@ class VirtualBoxGNS3VM(BaseGNS3VM): # get a NAT interface number nat_interface_number = yield from self._look_for_interface("nat") if nat_interface_number < 0: - raise GNS3VMError("The GNS3 VM must have a NAT interface configured in order to start") + raise GNS3VMError("The GNS3 VM: {} must have a NAT interface configured in order to start".format(self.vmname)) hostonly_interface_number = yield from self._look_for_interface("hostonly") if hostonly_interface_number < 0: - raise GNS3VMError("The GNS3 VM must have a host only interface configured in order to start") + raise GNS3VMError("The GNS3 VM: {} must have a host only interface configured in order to start".format(self.vmname)) vboxnet = yield from self._look_for_vboxnet(hostonly_interface_number) if vboxnet is None: - raise GNS3VMError("VirtualBox host-only network could not be found for interface {}".format(hostonly_interface_number)) + raise GNS3VMError("VirtualBox host-only network could not be found for interface {} on GNS3 VM".format(hostonly_interface_number)) if not (yield from self._check_dhcp_server(vboxnet)): - raise GNS3VMError("DHCP must be enabled on VirtualBox host-only network: {}".format(vboxnet)) + raise GNS3VMError("DHCP must be enabled on VirtualBox host-only network: {} for GNS3 VM".format(vboxnet)) - vm_state = yield from self._get_vbox_vm_state() + vm_state = yield from self._get_state() log.info('"{}" state is {}'.format(self._vmname, vm_state)) if vm_state in ("poweroff", "saved"): # start the VM if it is not running @@ -178,15 +180,13 @@ class VirtualBoxGNS3VM(BaseGNS3VM): if self._headless: args.extend(["--type", "headless"]) yield from self._execute("startvm", args) - log.info("GNS3 VM has been started") - self.running = True ip_address = "127.0.0.1" try: # get a random port on localhost with socket.socket() as s: s.bind((ip_address, 0)) - port = s.getsockname()[1] + api_port = s.getsockname()[1] except OSError as e: raise GNS3VMError("Error while getting random port: {}".format(e)) @@ -196,39 +196,51 @@ class VirtualBoxGNS3VM(BaseGNS3VM): yield from self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"]) # add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to port 3080 in the VM - log.info("Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format(port, nat_interface_number)) + log.info("Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format(api_port, nat_interface_number)) yield from self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), - "GNS3VM,tcp,{},{},,3080".format(ip_address, port)]) + "GNS3VM,tcp,{},{},,3080".format(ip_address, api_port)]) - original_port = self.port - self.port = port + self.ip_address = yield from self._get_ip(hostonly_interface_number, api_port) + self.port = 3080 + log.info("GNS3 VM has been started with IP {}".format(self.ip_address)) + self.running = True - # TODO: retrieve interfaces on server - # # ask the server all a list of all its interfaces along with IP addresses - # status, json_data = self._waitForServer(vm_server, "interfaces", retry=120) - # if status == 401: - # self.error.emit("Wrong user or password for the GNS3 VM".format(status), True) - # return False - # if status != 200: - # msg = "Server {} has replied with status code {} when retrieving the network interfaces".format( - # vm_server.url(), status) - # log.error(msg) - # self.error.emit(msg, True) - # return False + @asyncio.coroutine + def _get_ip(self, hostonly_interface_number, api_port): + """ + Get the IP from VirtualBox. - # find the ip address for the first hostonly interface - hostonly_ip_address_found = False - for interface in json_data: - if "name" in interface and interface["name"] == "eth{}".format(hostonly_interface_number - 1): - if "ip_address" in interface: - self.ip_address = interface["ip_address"] - self.port = original_port - log.info("GNS3 VM IP address set to {}".format(interface["ip_address"])) - hostonly_ip_address_found = True - break + Due to VirtualBox limitation the only way is to send request each + second to a GNS3 endpoint in order to get the list of the interfaces and + their IP and after that match it with VirtualBox host only. + """ + remaining_try = 240 + while remaining_try > 0: + json_data = None + session = aiohttp.ClientSession() + try: + resp = None + resp = yield from session.get('http://127.0.0.1:{}/v2/compute/network/interfaces'.format(api_port)) + except OSError: + pass - if not hostonly_ip_address_found: - raise GNS3VMError("Not IP address could be found in the GNS3 VM for eth{}".format(hostonly_interface_number - 1)) + if resp: + try: + json_data = yield from resp.json() + except json.decoder.JSONDecodeError: + pass + resp.close() + + session.close() + + if json_data: + for interface in json_data: + if "name" in interface and interface["name"] == "eth{}".format(hostonly_interface_number - 1): + if "ip_address" in interface: + return interface["ip_address"] + remaining_try -= 1 + yield from asyncio.sleep(1) + raise GNS3VMError("Could not get the GNS3 VM ip make sure the VM receive an IP from VirtualBox") @asyncio.coroutine def stop(self): diff --git a/gns3server/run.py b/gns3server/run.py index 72bab049..17fe1e94 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -107,6 +107,7 @@ def parse_arguments(argv): parser.add_argument("--daemon", action="store_true", help="start as a daemon") parser.add_argument("--pid", help="store process pid") parser.add_argument("--profil", help="Settings profil (blank will use default settings files)") + parser.add_argument("--discovery", action="store_true", help="Make server discoverable on the network") args = parser.parse_args(argv) if args.config: diff --git a/tests/controller/gns3vm/test_virtualbox_gns3_vm.py b/tests/controller/gns3vm/test_virtualbox_gns3_vm.py new file mode 100644 index 00000000..d534c497 --- /dev/null +++ b/tests/controller/gns3vm/test_virtualbox_gns3_vm.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pytest +from tests.utils import asyncio_patch + +from gns3server.controller.gns3vm.virtualbox_gns3_vm import VirtualBoxGNS3VM + + +@pytest.fixture +def gns3vm(controller): + vm = VirtualBoxGNS3VM(controller) + vm.vmname = "GNS3 VM" + return vm + + +def test_look_for_interface(gns3vm, async_run): + showvminfo = """ +nic1="hostonly" +nictype1="82540EM" +nicspeed1="0" +nic2="nat" +nictype2="82540EM" +nicspeed2="0" +nic3="none" +nic4="none" +nic5="none" +nic6="none" +nic7="none" +nic8="none" +vcpwidth=1024 +vcpheight=768 +vcprate=512 +vcpfps=25 +GuestMemoryBalloon=0 + """ + + with asyncio_patch("gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM._execute", return_value=showvminfo) as mock: + res = async_run(gns3vm._look_for_interface("nat")) + mock.assert_called_with('showvminfo', ['GNS3 VM', '--machinereadable']) + assert res == 2 + + with asyncio_patch("gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM._execute", return_value=showvminfo) as mock: + res = async_run(gns3vm._look_for_interface("dummy")) + assert res == -1