From 578bb5741d89381cb45b72da1211360c99467263 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Wed, 6 Aug 2014 22:43:37 +0100 Subject: [PATCH 01/29] Override check_origin from tornado.websocket --- gns3server/handlers/jsonrpc_websocket.py | 3 +++ gns3server/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py index 5b18496c..26c96e6a 100644 --- a/gns3server/handlers/jsonrpc_websocket.py +++ b/gns3server/handlers/jsonrpc_websocket.py @@ -52,6 +52,9 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): self._session_id = str(uuid.uuid4()) self.zmq_router = zmq_router + def check_origin(self, origin): + return True + @property def session_id(self): """ diff --git a/gns3server/version.py b/gns3server/version.py index c206840a..118c2a4c 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta1" +__version__ = "1.0beta2-dev1" __version_info__ = (1, 0, 0, -99) From 7cbce0f81b7d4e7e8e493f38cd95728f1b9e2d61 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 14:32:32 +0100 Subject: [PATCH 02/29] Fix test suite + Install VPCS and dynamips from GNS3 PPA + Drop netifaces-py3 from requirements.txt + Fix/update tests to use installed VPCS and dynamips --- .travis.yml | 5 +++++ requirements.txt | 1 - tests/dynamips/conftest.py | 5 ++--- tests/dynamips/test_c7200.py | 6 +++--- tests/dynamips/test_hypervisor.py | 5 ++--- tests/dynamips/test_hypervisor_manager.py | 5 ++--- tests/dynamips/test_router.py | 12 ++++++------ tests/iou/test_iou_device.py | 6 +++++- tests/vpcs/test_vpcs_device.py | 10 +++++++--- 9 files changed, 32 insertions(+), 23 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2440f1dc..b52cdc35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,14 @@ python: - "3.3" - "3.4" +before_install: + - "sudo add-apt-repository ppa:gns3/ppa -y" + - "sudo apt-get update -q" + install: - "pip install -r requirements.txt --use-mirrors" - "pip install tox" + - "sudo apt-get install vpcs dynamips" script: "python setup.py test" diff --git a/requirements.txt b/requirements.txt index ae4f8b0a..e7e14a4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ netifaces tornado pyzmq -netifaces-py3 jsonschema diff --git a/tests/dynamips/conftest.py b/tests/dynamips/conftest.py index fce9f54b..ff70cd58 100644 --- a/tests/dynamips/conftest.py +++ b/tests/dynamips/conftest.py @@ -6,10 +6,9 @@ import os @pytest.fixture(scope="module") def hypervisor(request): - cwd = os.path.dirname(os.path.abspath(__file__)) - dynamips_path = os.path.join(cwd, "dynamips.stable") + dynamips_path = '/usr/bin/dynamips' print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path)) - manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000) + manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1") hypervisor = manager.start_new_hypervisor() def stop(): diff --git a/tests/dynamips/test_c7200.py b/tests/dynamips/test_c7200.py index 7b74cc7f..ed5df69e 100644 --- a/tests/dynamips/test_c7200.py +++ b/tests/dynamips/test_c7200.py @@ -29,9 +29,9 @@ def test_router_exists(router_c7200): def test_npe(router_c7200): - assert router_c7200.npe == "npe-400" # default value - router_c7200.npe = "npe-200" - assert router_c7200.npe == "npe-200" + assert router_c7200.npe == "npe-200" # default value + router_c7200.npe = "npe-400" + assert router_c7200.npe == "npe-400" def test_midplane(router_c7200): diff --git a/tests/dynamips/test_hypervisor.py b/tests/dynamips/test_hypervisor.py index ed0ee2ab..ce7b2575 100644 --- a/tests/dynamips/test_hypervisor.py +++ b/tests/dynamips/test_hypervisor.py @@ -10,7 +10,7 @@ def test_is_started(hypervisor): def test_port(hypervisor): - assert hypervisor.port == 9000 + assert hypervisor.port == 7200 def test_host(hypervisor): @@ -25,8 +25,7 @@ def test_working_dir(hypervisor): def test_path(hypervisor): - cwd = os.path.dirname(os.path.abspath(__file__)) - dynamips_path = os.path.join(cwd, "dynamips.stable") + dynamips_path = '/usr/bin/dynamips' assert hypervisor.path == dynamips_path diff --git a/tests/dynamips/test_hypervisor_manager.py b/tests/dynamips/test_hypervisor_manager.py index f670641c..adaa79a2 100644 --- a/tests/dynamips/test_hypervisor_manager.py +++ b/tests/dynamips/test_hypervisor_manager.py @@ -7,10 +7,9 @@ import os @pytest.fixture(scope="module") def hypervisor_manager(request): - cwd = os.path.dirname(os.path.abspath(__file__)) - dynamips_path = os.path.join(cwd, "dynamips.stable") + dynamips_path = '/usr/bin/dynamips' print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path)) - manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000) + manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1") #manager.start_new_hypervisor() diff --git a/tests/dynamips/test_router.py b/tests/dynamips/test_router.py index 1affa539..ebf9835c 100644 --- a/tests/dynamips/test_router.py +++ b/tests/dynamips/test_router.py @@ -9,7 +9,7 @@ import base64 @pytest.fixture def router(request, hypervisor): - router = Router(hypervisor, "router", "c3725") + router = Router(hypervisor, "router", platform="c3725") request.addfinalizer(router.delete) return router @@ -127,9 +127,9 @@ def test_idlepc(router): def test_idlemax(router): - assert router.idlemax == 1500 # default value - router.idlemax = 500 - assert router.idlemax == 500 + assert router.idlemax == 500 # default value + router.idlemax = 1500 + assert router.idlemax == 1500 def test_idlesleep(router): @@ -172,7 +172,7 @@ def test_confreg(router): def test_console(router): - assert router.console == router.hypervisor.baseconsole + router.id + assert router.console == 2001 new_console_port = router.console + 100 router.console = new_console_port assert router.console == new_console_port @@ -180,7 +180,7 @@ def test_console(router): def test_aux(router): - assert router.aux == router.hypervisor.baseaux + router.id + assert router.aux == 2501 new_aux_port = router.aux + 100 router.aux = new_aux_port assert router.aux == new_aux_port diff --git a/tests/iou/test_iou_device.py b/tests/iou/test_iou_device.py index 0749c97f..ecf6d0ac 100644 --- a/tests/iou/test_iou_device.py +++ b/tests/iou/test_iou_device.py @@ -8,12 +8,14 @@ def iou(request): cwd = os.path.dirname(os.path.abspath(__file__)) iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") - iou_device = IOUDevice(iou_path, "/tmp") + iou_device = IOUDevice("IOU1", iou_path, "/tmp") iou_device.start() request.addfinalizer(iou_device.delete) return iou_device +@pytest.mark.skipif(os.environ["TRAVIS"] == "TRUE", + reason="IOU Image not available on Travis") def test_iou_is_started(iou): print(iou.command()) @@ -21,6 +23,8 @@ def test_iou_is_started(iou): assert iou.is_running() +@pytest.mark.skipif(os.environ["TRAVIS"] == "TRUE", + reason="IOU Image not available on Travis") def test_iou_restart(iou): iou.stop() diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py index 13609506..781166b4 100644 --- a/tests/vpcs/test_vpcs_device.py +++ b/tests/vpcs/test_vpcs_device.py @@ -6,9 +6,13 @@ import pytest @pytest.fixture(scope="session") def vpcs(request): - cwd = os.path.dirname(os.path.abspath(__file__)) - vpcs_path = os.path.join(cwd, "vpcs") - vpcs_device = VPCSDevice(vpcs_path, "/tmp") + if os.path.isfile("/usr/bin/vpcs"): + vpcs_path = "/usr/bin/vpcs" + else: + cwd = os.path.dirname(os.path.abspath(__file__)) + vpcs_path = os.path.join(cwd, "vpcs") + vpcs_device = VPCSDevice("VPCS1", vpcs_path, "/tmp") + vpcs_device.port_add_nio_binding(0, 'nio_tap:tap0') vpcs_device.start() request.addfinalizer(vpcs_device.delete) return vpcs_device From 8fc4667d2c38e51aaa031c56efb537d8bc4436b5 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 14:49:10 +0100 Subject: [PATCH 03/29] Modify the TRAVIS environment check --- tests/iou/test_iou_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/iou/test_iou_device.py b/tests/iou/test_iou_device.py index ecf6d0ac..c40a7cf9 100644 --- a/tests/iou/test_iou_device.py +++ b/tests/iou/test_iou_device.py @@ -14,7 +14,7 @@ def iou(request): return iou_device -@pytest.mark.skipif(os.environ["TRAVIS"] == "TRUE", +@pytest.mark.skipif(os.environ["TRAVIS"] == 'true', reason="IOU Image not available on Travis") def test_iou_is_started(iou): @@ -23,7 +23,7 @@ def test_iou_is_started(iou): assert iou.is_running() -@pytest.mark.skipif(os.environ["TRAVIS"] == "TRUE", +@pytest.mark.skipif(os.environ["TRAVIS"] == 'true', reason="IOU Image not available on Travis") def test_iou_restart(iou): From 9b010d6388a649b52cfa116a109e9964f24590fa Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 15:00:44 +0100 Subject: [PATCH 04/29] Update test_hypervisor.py + test_stdout: use system dynamips + test_stdout: correct host address to start dynamips on --- tests/dynamips/test_hypervisor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/dynamips/test_hypervisor.py b/tests/dynamips/test_hypervisor.py index ce7b2575..0ee5a422 100644 --- a/tests/dynamips/test_hypervisor.py +++ b/tests/dynamips/test_hypervisor.py @@ -33,9 +33,8 @@ def test_stdout(): # try to launch Dynamips on the same port # this will fail so that we can read its stdout/stderr - cwd = os.path.dirname(os.path.abspath(__file__)) - dynamips_path = os.path.join(cwd, "dynamips.stable") - hypervisor = Hypervisor(dynamips_path, "/tmp", "172.0.0.1", 7200) + dynamips_path = '/usr/bin/dynamips' + hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200) hypervisor.start() # give some time for Dynamips to start time.sleep(0.01) From 4a4a57e1a3c5a219e11e050ff4d83b0527d9b62e Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 17:54:30 +0100 Subject: [PATCH 05/29] Further test fixes + tests/dynamips/test_hypervisor.py: Increase sleep time to prevent random test failures + tests/iou/test_iou_device.py: Rework test skipping based on presence of IOU image rather than environment variable --- tests/dynamips/test_hypervisor.py | 3 ++- tests/iou/test_iou_device.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/dynamips/test_hypervisor.py b/tests/dynamips/test_hypervisor.py index 0ee5a422..36a2dafa 100644 --- a/tests/dynamips/test_hypervisor.py +++ b/tests/dynamips/test_hypervisor.py @@ -1,6 +1,7 @@ from gns3server.modules.dynamips import Hypervisor import time import os +import pytest def test_is_started(hypervisor): @@ -37,6 +38,6 @@ def test_stdout(): hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200) hypervisor.start() # give some time for Dynamips to start - time.sleep(0.01) + time.sleep(0.1) output = hypervisor.read_stdout() assert output diff --git a/tests/iou/test_iou_device.py b/tests/iou/test_iou_device.py index c40a7cf9..58581de9 100644 --- a/tests/iou/test_iou_device.py +++ b/tests/iou/test_iou_device.py @@ -3,6 +3,16 @@ import os import pytest +def no_iou(): + cwd = os.path.dirname(os.path.abspath(__file__)) + iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") + + if os.path.isfile(iou_path): + return False + else: + return True + + @pytest.fixture(scope="session") def iou(request): @@ -14,8 +24,7 @@ def iou(request): return iou_device -@pytest.mark.skipif(os.environ["TRAVIS"] == 'true', - reason="IOU Image not available on Travis") +@pytest.mark.skipif(no_iou(), reason="IOU Image not available") def test_iou_is_started(iou): print(iou.command()) @@ -23,8 +32,7 @@ def test_iou_is_started(iou): assert iou.is_running() -@pytest.mark.skipif(os.environ["TRAVIS"] == 'true', - reason="IOU Image not available on Travis") +@pytest.mark.skipif(no_iou(), reason="IOU Image not available") def test_iou_restart(iou): iou.stop() From ad287d34349926de2dd026f6c51e357ff8ae9436 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 19:14:36 +0100 Subject: [PATCH 06/29] Remove un-needed imports --- tests/dynamips/test_hypervisor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/dynamips/test_hypervisor.py b/tests/dynamips/test_hypervisor.py index 36a2dafa..e6c096b8 100644 --- a/tests/dynamips/test_hypervisor.py +++ b/tests/dynamips/test_hypervisor.py @@ -1,7 +1,5 @@ from gns3server.modules.dynamips import Hypervisor import time -import os -import pytest def test_is_started(hypervisor): From d8f622d438b1b4550dc98b1e4be19ceed3233e4e Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 19:15:26 +0100 Subject: [PATCH 07/29] Streamline TravisCI build + As we use Tox there's no need to run seperate builds for python3.3 and python3.4 + There's no need to install the requirements in main environment as all dependencies are handled in the Tox virtualenv's --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b52cdc35..baf4ca1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,10 @@ language: python -python: - - "3.3" - - "3.4" - before_install: - "sudo add-apt-repository ppa:gns3/ppa -y" - "sudo apt-get update -q" install: - - "pip install -r requirements.txt --use-mirrors" - "pip install tox" - "sudo apt-get install vpcs dynamips" From a4bc96af285f95e07f8eca83281c7cea944b63ce Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Fri, 8 Aug 2014 19:20:20 +0100 Subject: [PATCH 08/29] revert not installing requirements outside of tox --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index baf4ca1c..3a03c5fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ before_install: - "sudo apt-get update -q" install: + - "pip install -r requirements.txt --use-mirrors" - "pip install tox" - "sudo apt-get install vpcs dynamips" From 4a33b2021ca61644981009a127ccb567c36bf193 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Sat, 9 Aug 2014 12:05:31 +0100 Subject: [PATCH 09/29] Further optimise the Travis testing and improve running tests for a user + Convert setup.py test to run py.test instead of tox + Tox should now run setup.py test + TravisCI will create a job for each TOX_ENV and then execute tox to run the tests for that TOX_ENV --- .travis.yml | 16 ++++++++++------ setup.py | 11 ++++++----- tox.ini | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a03c5fb..883004a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,19 @@ language: python +env: + - TOX_ENV=py33 + - TOX_ENV=py34 + before_install: - - "sudo add-apt-repository ppa:gns3/ppa -y" - - "sudo apt-get update -q" + - sudo add-apt-repository ppa:gns3/ppa -y + - sudo apt-get update -q install: - - "pip install -r requirements.txt --use-mirrors" - - "pip install tox" - - "sudo apt-get install vpcs dynamips" + - pip install tox + - sudo apt-get install vpcs dynamips -script: "python setup.py test" +script: + - tox -e $TOX_ENV branches: only: diff --git a/setup.py b/setup.py index e64cfa3d..7e437d4d 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -class Tox(TestCommand): +class PyTest(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) @@ -29,8 +29,9 @@ class Tox(TestCommand): def run_tests(self): #import here, cause outside the eggs aren't loaded - import tox - errcode = tox.cmdline(self.test_args) + import pytest + #errcode = tox.cmdline(self.test_args) + errcode = pytest.main(self.test_args) sys.exit(errcode) setup( @@ -38,8 +39,8 @@ setup( version=__import__("gns3server").__version__, url="http://github.com/GNS3/gns3-server", license="GNU General Public License v3 (GPLv3)", - tests_require=["tox"], - cmdclass={"test": Tox}, + tests_require=["pytest"], + cmdclass={"test": PyTest}, author="Jeremy Grossmann", author_email="package-maintainer@gns3.net", description="GNS3 server to asynchronously manage emulators", diff --git a/tox.ini b/tox.ini index b10796da..200e7ce4 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,6 @@ envlist = py33, py34 [testenv] -commands = py.test [] -s tests +commands = python setup.py test deps = -rdev-requirements.txt From e5642546f19e577fe0bfe0919280693ec334f359 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Sat, 9 Aug 2014 12:26:24 +0100 Subject: [PATCH 10/29] Remove commented line, not needed anymore --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 7e437d4d..78bfa355 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,6 @@ class PyTest(TestCommand): def run_tests(self): #import here, cause outside the eggs aren't loaded import pytest - #errcode = tox.cmdline(self.test_args) errcode = pytest.main(self.test_args) sys.exit(errcode) From a8d740ef21bfd41db6fa8e1945ac7db6ee2df9e8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 11 Aug 2014 22:13:21 -0600 Subject: [PATCH 11/29] Fix version from 1.0beta2-dev1 to 1.0beta2.dev1 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 118c2a4c..bb2875b1 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta2-dev1" +__version__ = "1.0beta2.dev1" __version_info__ = (1, 0, 0, -99) From e0f0c98ffd75ed57a9e1444c2076251efd6f1ceb Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 13 Aug 2014 12:11:41 -0600 Subject: [PATCH 12/29] Do not look for vboxwrapper on non Windows platforms. --- gns3server/modules/virtualbox/__init__.py | 37 ++++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index d7e0f1f9..2e70d981 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -60,25 +60,26 @@ class VirtualBox(IModule): def __init__(self, name, *args, **kwargs): - # get the vboxwrapper location - config = Config.instance() - vbox_config = config.get_section_config(name.upper()) - self._vboxwrapper_path = vbox_config.get("vboxwrapper_path") - if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path): - paths = [os.getcwd()] + os.environ["PATH"].split(":") - # look for iouyap in the current working directory and $PATH - for path in paths: - try: - if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK): - self._vboxwrapper_path = os.path.join(path, "vboxwrapper") - break - except OSError: - continue + # get the vboxwrapper location (only non-Windows platforms) + if not sys.platform.startswith("win"): + config = Config.instance() + vbox_config = config.get_section_config(name.upper()) + self._vboxwrapper_path = vbox_config.get("vboxwrapper_path") + if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path): + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for iouyap in the current working directory and $PATH + for path in paths: + try: + if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK): + self._vboxwrapper_path = os.path.join(path, "vboxwrapper") + break + except OSError: + continue - if not self._vboxwrapper_path: - log.warning("vboxwrapper couldn't be found!") - elif not os.access(self._vboxwrapper_path, os.X_OK): - log.warning("vboxwrapper is not executable") + if not self._vboxwrapper_path: + log.warning("vboxwrapper couldn't be found!") + elif not os.access(self._vboxwrapper_path, os.X_OK): + log.warning("vboxwrapper is not executable") # a new process start when calling IModule IModule.__init__(self, name, *args, **kwargs) From ea05744e1c291f2f8143030d1d7e2823d4404add Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 17 Aug 2014 15:15:07 -0600 Subject: [PATCH 13/29] Force to rebuild the COM cache on Windows (for VirtualBox support). --- README.rst | 2 +- gns3server/modules/virtualbox/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7dd682c4..d733fa63 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ GNS3-server =========== -New GNS3 server repository (alpha stage). +New GNS3 server repository (beta stage). The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM. Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websockets. diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 2e70d981..f086ef5a 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -112,7 +112,7 @@ class VirtualBox(IModule): # http://www.py2exe.org/index.cgi/IncludingTypelibs # http://www.py2exe.org/index.cgi/UsingEnsureDispatch win32com.client.gencache.is_readonly = False - #win32com.client.gencache.Rebuild() + win32com.client.gencache.Rebuild() win32com.client.gencache.GetGeneratePath() try: from .vboxapi_py3 import VirtualBoxManager diff --git a/setup.py b/setup.py index 78bfa355..76f03952 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ setup( include_package_data=True, platforms="any", classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Information Technology", "Topic :: System :: Networking", From 77c583ca39f73e00e45e313ccc5ad1a5211d9724 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 21 Aug 2014 18:13:41 -0600 Subject: [PATCH 14/29] Check if the VirtualBox COM service is installed on Windows. --- gns3server/modules/dynamips/nodes/c7200.py | 4 ---- gns3server/modules/virtualbox/__init__.py | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index e27a22d0..df942c71 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -55,10 +55,6 @@ class C7200(Router): if npe != "npe-400": self.npe = npe - if parse_version(hypervisor.version) <= parse_version('0.2.13'): - # work around a bug when rebooting a router with NPE-400 in Dynamips <= 0.2.13 - self.npe = "npe-200" - # 4 sensors with a default temperature of 22C: # sensor 1 = I/0 controller inlet # sensor 2 = I/0 controller outlet diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index f086ef5a..f441857a 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -107,13 +107,20 @@ class VirtualBox(IModule): if sys.platform.startswith("win"): import win32com.client + if win32com.client.gencache.is_readonly is True: # dynamically generate the cache # http://www.py2exe.org/index.cgi/IncludingTypelibs # http://www.py2exe.org/index.cgi/UsingEnsureDispatch win32com.client.gencache.is_readonly = False - win32com.client.gencache.Rebuild() + #win32com.client.gencache.Rebuild() win32com.client.gencache.GetGeneratePath() + + try: + win32com.client.gencache.EnsureDispatch("VirtualBox.VirtualBox") + except: + raise VirtualBoxError("VirtualBox is not installed.") + try: from .vboxapi_py3 import VirtualBoxManager self._vboxmanager = VirtualBoxManager(None, None) @@ -749,7 +756,11 @@ class VirtualBox(IModule): """ if not self._vboxwrapper and not self._vboxmanager: - self._start_vbox_service() + try: + self._start_vbox_service() + except VirtualBoxError as e: + self.send_custom_error(str(e)) + return if self._vboxwrapper: vms = self._vboxwrapper.get_vm_list() From 569a68a486728932003404870cc33aa56e0fe1f0 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 22 Aug 2014 17:36:12 -0600 Subject: [PATCH 15/29] VirtualBox support refactoring. --- gns3server/modules/dynamips/nodes/c7200.py | 1 - gns3server/modules/virtualbox/__init__.py | 30 +- .../modules/virtualbox/vboxwrapper_client.py | 43 +- .../virtualbox/virtualbox_controller.py | 529 ++++++++++++++++++ .../modules/virtualbox/virtualbox_vm.py | 460 +-------------- gns3server/version.py | 2 +- 6 files changed, 609 insertions(+), 456 deletions(-) create mode 100644 gns3server/modules/virtualbox/virtualbox_controller.py diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index df942c71..0dd7127b 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -24,7 +24,6 @@ from ..dynamips_error import DynamipsError from .router import Router from ..adapters.c7200_io_2fe import C7200_IO_2FE from ..adapters.c7200_io_ge_e import C7200_IO_GE_E -from pkg_resources import parse_version import logging log = logging.getLogger(__name__) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index f441857a..0ff1bb28 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -24,6 +24,7 @@ import os import socket import shutil +from pkg_resources import parse_version from gns3server.modules import IModule from gns3server.config import Config from .virtualbox_vm import VirtualBoxVM @@ -106,25 +107,30 @@ class VirtualBox(IModule): """ if sys.platform.startswith("win"): + import pywintypes import win32com.client - if win32com.client.gencache.is_readonly is True: - # dynamically generate the cache - # http://www.py2exe.org/index.cgi/IncludingTypelibs - # http://www.py2exe.org/index.cgi/UsingEnsureDispatch - win32com.client.gencache.is_readonly = False - #win32com.client.gencache.Rebuild() - win32com.client.gencache.GetGeneratePath() - try: + if win32com.client.gencache.is_readonly is True: + # dynamically generate the cache + # http://www.py2exe.org/index.cgi/IncludingTypelibs + # http://www.py2exe.org/index.cgi/UsingEnsureDispatch + win32com.client.gencache.is_readonly = False + #win32com.client.gencache.Rebuild() + win32com.client.gencache.GetGeneratePath() + win32com.client.gencache.EnsureDispatch("VirtualBox.VirtualBox") - except: + except pywintypes.com_error: raise VirtualBoxError("VirtualBox is not installed.") try: from .vboxapi_py3 import VirtualBoxManager self._vboxmanager = VirtualBoxManager(None, None) + vbox_major_version, vbox_minor_version, _ = self._vboxmanager.vbox.version.split('.') + if parse_version("{}.{}".format(vbox_major_version, vbox_minor_version)) <= parse_version("4.1"): + raise VirtualBoxError("VirtualBox version must be >= 4.2") except Exception as e: + self._vboxmanager = None raise VirtualBoxError("Could not initialize the VirtualBox Manager: {}".format(e)) log.info("VirtualBox Manager has successful started: version is {} r{}".format(self._vboxmanager.vbox.version, @@ -139,7 +145,11 @@ class VirtualBox(IModule): self._vboxwrapper = VboxWrapperClient(self._vboxwrapper_path, self._tempdir, "127.0.0.1") #self._vboxwrapper.connect() - self._vboxwrapper.start() + try: + self._vboxwrapper.start() + except VirtualBoxError: + self._vboxwrapper = None + raise def stop(self, signum=None): """ diff --git a/gns3server/modules/virtualbox/vboxwrapper_client.py b/gns3server/modules/virtualbox/vboxwrapper_client.py index 43a1743d..bcaaed11 100644 --- a/gns3server/modules/virtualbox/vboxwrapper_client.py +++ b/gns3server/modules/virtualbox/vboxwrapper_client.py @@ -26,6 +26,7 @@ import tempfile import socket import re +from pkg_resources import parse_version from ..attic import wait_socket_is_ready from .virtualbox_error import VirtualBoxError @@ -53,7 +54,7 @@ class VboxWrapperClient(object): self._command = [] self._process = None self._working_dir = working_dir - self._stdout_file = "" + self._stderr_file = "" self._started = False self._host = host self._port = port @@ -139,19 +140,31 @@ class VboxWrapperClient(object): try: log.info("starting VirtualBox wrapper: {}".format(self._command)) with tempfile.NamedTemporaryFile(delete=False) as fd: - self._stdout_file = fd.name - log.info("VirtualBox wrapper process logging to {}".format(fd.name)) - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) + with open(os.devnull, "w") as null: + self._stderr_file = fd.name + log.info("VirtualBox wrapper process logging to {}".format(fd.name)) + self._process = subprocess.Popen(self._command, + stdout=null, + stderr=fd, + cwd=self._working_dir) log.info("VirtualBox wrapper started PID={}".format(self._process.pid)) + + time.sleep(0.1) # give some time for vboxwrapper to start + if self._process.poll() is not None: + raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(self.read_stderr())) + self.wait_for_vboxwrapper(self._host, self._port) self.connect() self._started = True + + version = self.send('vboxwrapper version')[0] + if parse_version(version) < parse_version("0.9.2"): + self.stop() + raise VirtualBoxError("VirtualBox wrapper version must be >= 0.9.1") + except OSError as e: log.error("could not start VirtualBox wrapper: {}".format(e)) - raise VirtualBoxError("could not start VirtualBox wrapper: {}".format(e)) + raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(e)) def wait_for_vboxwrapper(self, host, port): """ @@ -198,26 +211,26 @@ class VboxWrapperClient(object): if self._process.poll() is None: log.warn("VirtualBox wrapper process {} is still running".format(self._process.pid)) - if self._stdout_file and os.access(self._stdout_file, os.W_OK): + if self._stderr_file and os.access(self._stderr_file, os.W_OK): try: - os.remove(self._stdout_file) + os.remove(self._stderr_file) except OSError as e: log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e)) self._started = False - def read_stdout(self): + def read_stderr(self): """ - Reads the standard output of the VirtualBox wrapper process. + Reads the standard error output of the VirtualBox wrapper process. Only use when the process has been stopped or has crashed. """ output = "" - if self._stdout_file and os.access(self._stdout_file, os.R_OK): + if self._stderr_file and os.access(self._stderr_file, os.R_OK): try: - with open(self._stdout_file, errors="replace") as file: + with open(self._stderr_file, errors="replace") as file: output = file.read() except OSError as e: - log.warn("could not read {}: {}".format(self._stdout_file, e)) + log.warn("could not read {}: {}".format(self._stderr_file, e)) return output def is_running(self): diff --git a/gns3server/modules/virtualbox/virtualbox_controller.py b/gns3server/modules/virtualbox/virtualbox_controller.py new file mode 100644 index 00000000..72cd8a96 --- /dev/null +++ b/gns3server/modules/virtualbox/virtualbox_controller.py @@ -0,0 +1,529 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +Controls VirtualBox using the VBox API. +""" + +import sys +import os +import tempfile +import re +import time +import socket +import subprocess + +if sys.platform.startswith('win'): + import msvcrt + import win32file + +from .virtualbox_error import VirtualBoxError +from .pipe_proxy import PipeProxy + +import logging +log = logging.getLogger(__name__) + + +class VirtualBoxController(object): + + def __init__(self, vmname, vboxmanager, host): + + self._host = host + self._machine = None + self._session = None + self._vboxmanager = vboxmanager + self._maximum_adapters = 0 + self._serial_pipe_thread = None + self._serial_pipe = None + + self._vmname = vmname + self._console = 0 + self._adapters = [] + self._headless = False + self._adapter_type = "Automatic" + + try: + self._machine = self._vboxmanager.vbox.findMachine(self._vmname) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + # The maximum support network cards depends on the Chipset (PIIX3 or ICH9) + self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType) + + @property + def vmname(self): + + return self._vmname + + @vmname.setter + def vmname(self, new_vmname): + + self._vmname = new_vmname + try: + self._machine = self._vboxmanager.vbox.findMachine(new_vmname) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + # The maximum support network cards depends on the Chipset (PIIX3 or ICH9) + self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType) + + @property + def console(self): + + return self._console + + @console.setter + def console(self, console): + + self._console = console + + @property + def headless(self): + + return self._headless + + @headless.setter + def headless(self, headless): + + self._headless = headless + + @property + def adapters(self): + + return self._adapters + + @adapters.setter + def adapters(self, adapters): + + self._adapters = adapters + + @property + def adapter_type(self): + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + + self._adapter_type = adapter_type + + def start(self): + + if self._machine.state == self._vboxmanager.constants.MachineState_Paused: + self.resume() + return + + self._get_session() + self._set_network_options() + self._set_console_options() + + progress = self._launch_vm_process() + log.info("VM is starting with {}% completed".format(progress.percent)) + if progress.percent != 100: + # This will happen if you attempt to start VirtualBox with unloaded "vboxdrv" module. + # or have too little RAM or damaged vHDD, or connected to non-existent network. + # We must unlock machine, otherwise it locks the VirtualBox Manager GUI. (on Linux hosts) + self._unlock_machine() + raise VirtualBoxError("Unable to start the VM (failed at {}%)".format(progress.percent)) + + try: + self._machine.setGuestPropertyValue("NameInGNS3", self._name) + except Exception: + pass + + # starts the Telnet to pipe thread + pipe_name = self._get_pipe_name() + if sys.platform.startswith('win'): + try: + self._serial_pipe = open(pipe_name, "a+b") + except OSError as e: + raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) + self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) + #self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() + else: + try: + self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._serial_pipe.connect(pipe_name) + except OSError as e: + raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) + self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) + #self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() + + def stop(self): + + if self._serial_pipe_thread: + self._serial_pipe_thread.stop() + self._serial_pipe_thread.join(1) + if self._serial_pipe_thread.isAlive(): + log.warn("Serial pire thread is still alive!") + self._serial_pipe_thread = None + + if self._serial_pipe: + if sys.platform.startswith('win'): + win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) + else: + self._serial_pipe.close() + self._serial_pipe = None + + if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ + self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: + try: + if sys.platform.startswith('win') and "VBOX_INSTALL_PATH" in os.environ: + # work around VirtualBox bug #9239 + vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") + command = '"{}" controlvm "{}" poweroff'.format(vboxmanage_path, self._vmname) + subprocess.call(command, timeout=3) + else: + progress = self._session.console.powerDown() + # wait for VM to actually go down + progress.waitForCompletion(3000) + log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent)) + + self._lock_machine() + for adapter_id in range(0, len(self._adapters)): + self._disable_adapter(adapter_id, disable=True) + self._session.machine.saveSettings() + self._unlock_machine() + except Exception as e: + # Do not crash "vboxwrapper", if stopping VM fails. + # But return True anyway, so VM state in GNS3 can become "stopped" + # This can happen, if user manually kills VBox VM. + log.warn("could not stop VM for {}: {}".format(self._vmname, e)) + return + + def suspend(self): + + try: + self._session.console.pause() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def reload(self): + + try: + self._session.console.reset() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def resume(self): + + try: + self._session.console.resume() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def _get_session(self): + + log.debug("getting session for {}".format(self._vmname)) + try: + self._session = self._vboxmanager.mgr.getSessionObject(self._vboxmanager.vbox) + except Exception as e: + # fails on heavily loaded hosts... + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + def _set_network_options(self): + + log.debug("setting network options for {}".format(self._vmname)) + + self._lock_machine() + + first_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM + try: + first_adapter = self._session.machine.getNetworkAdapter(0) + first_adapter_type = first_adapter.adapterType + except Exception as e: + pass + #raise VirtualBoxError("VirtualBox error: {}".format(e)) + + for adapter_id in range(0, len(self._adapters)): + try: + # VirtualBox starts counting from 0 + adapter = self._session.machine.getNetworkAdapter(adapter_id) + vbox_adapter_type = adapter.adapterType + + if self._adapter_type == "PCnet-PCI II (Am79C970A)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A + if self._adapter_type == "PCNet-FAST III (Am79C973)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973 + if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM + if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC + if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM + if self._adapter_type == "Paravirtualized Network (virtio-net)": + vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio + if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC" + vbox_adapter_type = first_adapter_type + + adapter.adapterType = vbox_adapter_type + + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + nio = self._adapters[adapter_id].get_nio(0) + if nio: + log.debug("setting UDP params on adapter {}".format(adapter_id)) + try: + adapter.enabled = True + adapter.cableConnected = True + adapter.traceEnabled = False + # Temporary hack around VBox-UDP patch limitation: inability to use DNS + if nio.rhost == 'localhost': + rhost = '127.0.0.1' + else: + rhost = nio.rhost + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic + adapter.genericDriver = "UDPTunnel" + adapter.setProperty("sport", str(nio.lport)) + adapter.setProperty("dest", rhost) + adapter.setProperty("dport", str(nio.rport)) + except Exception as e: + # usually due to COM Error: "The object is not ready" + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + if nio.capturing: + self._enable_capture(adapter, nio.pcap_output_file) + + else: + # shutting down unused adapters... + try: + adapter.enabled = True + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + adapter.cableConnected = False + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + #for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): + # log.debug("disabling remaining adapter {}".format(adapter_id)) + # self._disable_adapter(adapter_id) + + try: + self._session.machine.saveSettings() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + self._unlock_machine() + + def _disable_adapter(self, adapter_id, disable=True): + + log.debug("disabling network adapter for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 6 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not disable network adapter after 4 retries: {}".format(last_exception)) + try: + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter.traceEnabled = False + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + if disable: + adapter.enabled = False + break + except Exception as e: + # usually due to COM Error: "The object is not ready" + log.warn("cannot disable network adapter for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(1) + continue + + def _enable_capture(self, adapter, output_file): + + log.debug("enabling capture for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not enable packet capture after 4 retries: {}".format(last_exception)) + try: + adapter.traceEnabled = True + adapter.traceFile = output_file + break + except Exception as e: + log.warn("cannot enable packet capture for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(0.75) + continue + + def create_udp(self, adapter_id, sport, daddr, dport): + + if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ + self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: + # the machine is being executed + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not create an UDP tunnel after 4 retries :{}".format(last_exception)) + try: + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter.cableConnected = True + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + self._session.machine.saveSettings() + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic + adapter.genericDriver = "UDPTunnel" + adapter.setProperty("sport", str(sport)) + adapter.setProperty("dest", daddr) + adapter.setProperty("dport", str(dport)) + self._session.machine.saveSettings() + break + except Exception as e: + # usually due to COM Error: "The object is not ready" + log.warn("cannot create UDP tunnel for {}: {}".format(self._vmname, e)) + last_exception = e + time.sleep(0.75) + continue + + def delete_udp(self, adapter_id): + + if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ + self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: + # the machine is being executed + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not delete an UDP tunnel after 4 retries :{}".format(last_exception)) + try: + adapter = self._session.machine.getNetworkAdapter(adapter_id) + adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null + adapter.cableConnected = False + self._session.machine.saveSettings() + break + except Exception as e: + # usually due to COM Error: "The object is not ready" + log.debug("cannot delete UDP tunnel for {}: {}".format(self._vmname, e)) + last_exception = e + time.sleep(0.75) + continue + + def _get_pipe_name(self): + + p = re.compile('\s+', re.UNICODE) + pipe_name = p.sub("_", self._vmname) + if sys.platform.startswith('win'): + pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name) + else: + pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name)) + return pipe_name + + def _set_console_options(self): + """ + # Example to manually set serial parameters using Python + + from vboxapi import VirtualBoxManager + mgr = VirtualBoxManager(None, None) + mach = mgr.vbox.findMachine("My VM") + session = mgr.mgr.getSessionObject(mgr.vbox) + mach.lockMachine(session, 1) + mach2=session.machine + serial_port = mach2.getSerialPort(0) + serial_port.enabled = True + serial_port.path = "/tmp/test_pipe" + serial_port.hostMode = 1 + serial_port.server = True + session.unlockMachine() + """ + + log.info("setting console options for {}".format(self._vmname)) + + self._lock_machine() + pipe_name = self._get_pipe_name() + + try: + serial_port = self._session.machine.getSerialPort(0) + serial_port.enabled = True + serial_port.path = pipe_name + serial_port.hostMode = 1 + serial_port.server = True + self._session.machine.saveSettings() + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + self._unlock_machine() + + def _launch_vm_process(self): + + log.debug("launching VM {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not launch the VM after 4 retries: {}".format(last_exception)) + try: + if self._headless: + mode = "headless" + else: + mode = "gui" + log.info("starting {} in {} mode".format(self._vmname, mode)) + progress = self._machine.launchVMProcess(self._session, mode, "") + break + except Exception as e: + # This will usually happen if you try to start the same VM twice, + # but may happen on loaded hosts too... + log.warn("cannot launch VM {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(0.6) + continue + + try: + progress.waitForCompletion(-1) + except Exception as e: + raise VirtualBoxError("VirtualBox error: {}".format(e)) + + return progress + + def _lock_machine(self): + + log.debug("locking machine for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not lock the machine after 4 retries: {}".format(last_exception)) + try: + self._machine.lockMachine(self._session, 1) + break + except Exception as e: + log.warn("cannot lock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + last_exception = e + time.sleep(1) + continue + + def _unlock_machine(self): + + log.debug("unlocking machine for {}".format(self._vmname)) + # this command is retried several times, because it fails more often... + retries = 4 + last_exception = None + for retry in range(retries): + if retry == (retries - 1): + raise VirtualBoxError("Could not unlock the machine after 4 retries: {}".format(last_exception)) + try: + self._session.unlockMachine() + break + except Exception as e: + log.warn("cannot unlock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) + time.sleep(1) + last_exception = e + continue \ No newline at end of file diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 10319044..d05ea631 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -19,24 +19,14 @@ VirtualBox VM instance. """ -import sys import os import shutil -import tempfile -import re -import time -import socket -import subprocess -from .pipe_proxy import PipeProxy from .virtualbox_error import VirtualBoxError +from .virtualbox_controller import VirtualBoxController from .adapters.ethernet_adapter import EthernetAdapter from ..attic import find_unused_port -if sys.platform.startswith('win'): - import msvcrt - import win32file - import logging log = logging.getLogger(__name__) @@ -97,16 +87,6 @@ class VirtualBoxVM(object): self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range - # Telnet to pipe mini-server - self._serial_pipe_thread = None - self._serial_pipe = None - - # VirtualBox API variables - self._machine = None - self._session = None - self._vboxmanager = vboxmanager - self._maximum_adapters = 0 - # VirtualBox settings self._console = console self._ethernet_adapters = [] @@ -140,19 +120,11 @@ class VirtualBoxVM(object): self._vboxwrapper.send('vbox create vbox "{}"'.format(self._name)) self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname)) self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console)) - self._vboxwrapper.send('vbox setattr "{}" console_support True'.format(self._name)) - self._vboxwrapper.send('vbox setattr "{}" console_telnet_server True'.format(self._name)) else: - try: - self._machine = self._vboxmanager.vbox.findMachine(self._vmname) - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - # The maximum support network cards depends on the Chipset (PIIX3 or ICH9) - self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType) - - self.adapters = 2 + self._vboxcontroller = VirtualBoxController(self._vmname, vboxmanager, self._host) + self._vboxcontroller.console = self._console + self.adapters = 2 # creates 2 adapters by default log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) @@ -274,6 +246,8 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console)) + else: + self._vboxcontroller.console = console log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name, id=self._id, @@ -344,10 +318,14 @@ class VirtualBoxVM(object): if headless: if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" headless_mode True'.format(self._name)) + else: + self._vboxcontroller.headless = True log.info("VirtualBox VM {name} [id={id}] has enabled the headless mode".format(name=self._name, id=self._id)) else: if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" headless_mode False'.format(self._name)) + else: + self._vboxcontroller.headless = False log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id)) self._headless = headless @@ -372,13 +350,7 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname)) else: - try: - self._machine = self._vboxmanager.vbox.findMachine(vmname) - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - # The maximum support network cards depends on the Chipset (PIIX3 or ICH9) - self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType) + self._vboxcontroller.vmname = vmname log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname)) self._vmname = vmname @@ -407,6 +379,8 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, len(self._ethernet_adapters))) + else: + self._vboxcontroller.adapters = self._ethernet_adapters log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, id=self._id, @@ -434,6 +408,8 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox setattr "{}" netcard "{}"'.format(self._name, adapter_type)) + else: + self._vboxcontroller.adapter_type = adapter_type log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, id=self._id, @@ -445,54 +421,9 @@ class VirtualBoxVM(object): """ if self._vboxwrapper: - status = int(self._vboxwrapper.send('vbox status "{}"'.format(self._name))[0]) - if status == 6: # paused - self.resume() - return self._vboxwrapper.send('vbox start "{}"'.format(self._name)) else: - - if self._machine.state == self._vboxmanager.constants.MachineState_Paused: - self.resume() - return - - self._get_session() - self._set_network_options() - self._set_console_options() - - progress = self._launch_vm_process() - log.info("VM is starting with {}% completed".format(progress.percent)) - if progress.percent != 100: - # This will happen if you attempt to start VirtualBox with unloaded "vboxdrv" module. - # or have too little RAM or damaged vHDD, or connected to non-existent network. - # We must unlock machine, otherwise it locks the VirtualBox Manager GUI. (on Linux hosts) - self._unlock_machine() - raise VirtualBoxError("Unable to start the VM (failed at {}%)".format(progress.percent)) - - try: - self._machine.setGuestPropertyValue("NameInGNS3", self._name) - except Exception: - pass - - # starts the Telnet to pipe thread - pipe_name = self._get_pipe_name() - if sys.platform.startswith('win'): - try: - self._serial_pipe = open(pipe_name, "a+b") - except OSError as e: - raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) - self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) - #self._serial_pipe_thread.setDaemon(True) - self._serial_pipe_thread.start() - else: - try: - self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._serial_pipe.connect(pipe_name) - except OSError as e: - raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) - #self._serial_pipe_thread.setDaemon(True) - self._serial_pipe_thread.start() + self._vboxcontroller.start() def stop(self): """ @@ -500,48 +431,13 @@ class VirtualBoxVM(object): """ if self._vboxwrapper: - self._vboxwrapper.send('vbox stop "{}"'.format(self._name)) + try: + self._vboxwrapper.send('vbox stop "{}"'.format(self._name)) + except VirtualBoxError: + # probably lost the connection + return else: - - if self._serial_pipe_thread: - self._serial_pipe_thread.stop() - self._serial_pipe_thread.join(1) - if self._serial_pipe_thread.isAlive(): - log.warn("Serial pire thread is still alive!") - self._serial_pipe_thread = None - - if self._serial_pipe: - if sys.platform.startswith('win'): - win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) - else: - self._serial_pipe.close() - self._serial_pipe = None - - if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ - self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: - try: - if sys.platform.startswith('win') and "VBOX_INSTALL_PATH" in os.environ: - # work around VirtualBox bug #9239 - vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe") - command = '"{}" controlvm "{}" poweroff'.format(vboxmanage_path, self._vmname) - subprocess.call(command, timeout=3) - else: - progress = self._session.console.powerDown() - # wait for VM to actually go down - progress.waitForCompletion(3000) - log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent)) - - self._lock_machine() - for adapter_id in range(0, len(self._ethernet_adapters)): - self._disable_adapter(adapter_id, disable=True) - self._session.machine.saveSettings() - self._unlock_machine() - except Exception as e: - # Do not crash "vboxwrapper", if stopping VM fails. - # But return True anyway, so VM state in GNS3 can become "stopped" - # This can happen, if user manually kills VBox VM. - log.warn("could not stop VM for {}: {}".format(self._vmname, e)) - return + self._vboxcontroller.stop() def suspend(self): """ @@ -551,10 +447,7 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox suspend "{}"'.format(self._name)) else: - try: - self._session.console.pause() - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) + self._vboxcontroller.suspend() def reload(self): """ @@ -564,11 +457,7 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox reset "{}"'.format(self._name)) else: - try: - progress = self._session.console.reset() - progress.waitForCompletion(-1) - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) + self._vboxcontroller.reload() def resume(self): """ @@ -578,10 +467,7 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox resume "{}"'.format(self._name)) else: - try: - self._session.console.resume() - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) + self._vboxcontroller.resume() def port_add_nio_binding(self, adapter_id, nio): """ @@ -599,12 +485,12 @@ class VirtualBoxVM(object): if self._vboxwrapper: self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name, - adapter_id, - nio.lport, - nio.rhost, - nio.rport)) + adapter_id, + nio.lport, + nio.rhost, + nio.rport)) else: - self._create_udp(adapter_id, nio.lport, nio.rhost, nio.rport) + self._vboxcontroller.create_udp(adapter_id, nio.lport, nio.rhost, nio.rport) adapter.add_nio(0, nio) log.info("VirtualBox VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, @@ -631,7 +517,7 @@ class VirtualBoxVM(object): self._vboxwrapper.send('vbox delete_udp "{}" {}'.format(self._name, adapter_id)) else: - self._delete_udp(adapter_id) + self._vboxcontroller.delete_udp(adapter_id) nio = adapter.get_nio(0) adapter.remove_nio(0) @@ -700,287 +586,3 @@ class VirtualBoxVM(object): log.info("VirtualBox VM {name} [id={id}]: stopping packet capture on adapter {adapter_id}".format(name=self._name, id=self._id, adapter_id=adapter_id)) - - def _get_session(self): - - log.debug("getting session for {}".format(self._vmname)) - try: - self._session = self._vboxmanager.mgr.getSessionObject(self._vboxmanager.vbox) - except Exception as e: - # fails on heavily loaded hosts... - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - def _set_network_options(self): - - log.debug("setting network options for {}".format(self._vmname)) - - self._lock_machine() - - first_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM - try: - first_adapter = self._session.machine.getNetworkAdapter(0) - first_adapter_type = first_adapter.adapterType - except Exception as e: - pass - #raise VirtualBoxError("VirtualBox error: {}".format(e)) - - for adapter_id in range(0, len(self._ethernet_adapters)): - try: - # VirtualBox starts counting from 0 - adapter = self._session.machine.getNetworkAdapter(adapter_id) - adapter_type = adapter.adapterType - - if self._adapter_type == "PCnet-PCI II (Am79C970A)": - adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A - if self._adapter_type == "PCNet-FAST III (Am79C973)": - adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973 - if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": - adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM - if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": - adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC - if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": - adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM - if self._adapter_type == "Paravirtualized Network (virtio-net)": - adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio - if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC" - adapter_type = first_adapter_type - - adapter.adapterType = adapter_type - - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - nio = self._ethernet_adapters[adapter_id].get_nio(0) - if nio: - log.debug("setting UDP params on adapter {}".format(adapter_id)) - try: - adapter.enabled = True - adapter.cableConnected = True - adapter.traceEnabled = False - # Temporary hack around VBox-UDP patch limitation: inability to use DNS - if nio.rhost == 'localhost': - rhost = '127.0.0.1' - else: - rhost = nio.rhost - adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic - adapter.genericDriver = "UDPTunnel" - adapter.setProperty("sport", str(nio.lport)) - adapter.setProperty("dest", rhost) - adapter.setProperty("dport", str(nio.rport)) - except Exception as e: - # usually due to COM Error: "The object is not ready" - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - if nio.capturing: - self._enable_capture(adapter, nio.pcap_output_file) - - else: - # shutting down unused adapters... - try: - adapter.enabled = True - adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null - adapter.cableConnected = False - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - #for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): - # log.debug("disabling remaining adapter {}".format(adapter_id)) - # self._disable_adapter(adapter_id) - - try: - self._session.machine.saveSettings() - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - self._unlock_machine() - - def _disable_adapter(self, adapter_id, disable=True): - - log.debug("disabling network adapter for {}".format(self._vmname)) - # this command is retried several times, because it fails more often... - retries = 6 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not disable network adapter after 4 retries: {}".format(last_exception)) - try: - adapter = self._session.machine.getNetworkAdapter(adapter_id) - adapter.traceEnabled = False - adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null - if disable: - adapter.enabled = False - break - except Exception as e: - # usually due to COM Error: "The object is not ready" - log.warn("cannot disable network adapter for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) - last_exception = e - time.sleep(1) - continue - - def _enable_capture(self, adapter, output_file): - - log.debug("enabling capture for {}".format(self._vmname)) - # this command is retried several times, because it fails more often... - retries = 4 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not enable packet capture after 4 retries: {}".format(last_exception)) - try: - adapter.traceEnabled = True - adapter.traceFile = output_file - break - except Exception as e: - log.warn("cannot enable packet capture for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) - last_exception = e - time.sleep(0.75) - continue - - def _create_udp(self, adapter_id, sport, daddr, dport): - - if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ - self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: - # the machine is being executed - retries = 4 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not create an UDP tunnel after 4 retries :{}".format(last_exception)) - try: - adapter = self._session.machine.getNetworkAdapter(adapter_id) - adapter.cableConnected = True - adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null - self._session.machine.saveSettings() - adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic - adapter.genericDriver = "UDPTunnel" - adapter.setProperty("sport", str(sport)) - adapter.setProperty("dest", daddr) - adapter.setProperty("dport", str(dport)) - self._session.machine.saveSettings() - break - except Exception as e: - # usually due to COM Error: "The object is not ready" - log.warn("cannot create UDP tunnel for {}: {}".format(self._vmname, e)) - last_exception = e - time.sleep(0.75) - continue - - def _delete_udp(self, adapter_id): - - if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \ - self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline: - # the machine is being executed - retries = 4 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not delete an UDP tunnel after 4 retries :{}".format(last_exception)) - try: - adapter = self._session.machine.getNetworkAdapter(adapter_id) - adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null - adapter.cableConnected = False - self._session.machine.saveSettings() - break - except Exception as e: - # usually due to COM Error: "The object is not ready" - log.debug("cannot delete UDP tunnel for {}: {}".format(self._vmname, e)) - last_exception = e - time.sleep(0.75) - continue - - def _get_pipe_name(self): - - p = re.compile('\s+', re.UNICODE) - pipe_name = p.sub("_", self._vmname) - if sys.platform.startswith('win'): - pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name) - else: - pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name)) - return pipe_name - - def _set_console_options(self): - - log.info("setting console options for {}".format(self.vmname)) - - self._lock_machine() - pipe_name = self._get_pipe_name() - - try: - serial_port = self._session.machine.getSerialPort(0) - serial_port.enabled = True - serial_port.path = pipe_name - serial_port.hostMode = 1 - serial_port.server = True - self._session.machine.saveSettings() - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - self._unlock_machine() - - def _launch_vm_process(self): - - log.debug("launching VM {}".format(self._vmname)) - # this command is retried several times, because it fails more often... - retries = 4 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not launch the VM after 4 retries: {}".format(last_exception)) - try: - if self._headless: - mode = "headless" - else: - mode = "gui" - log.info("starting {} in {} mode".format(self._vmname, mode)) - progress = self._machine.launchVMProcess(self._session, mode, "") - break - except Exception as e: - # This will usually happen if you try to start the same VM twice, - # but may happen on loaded hosts too... - log.warn("cannot launch VM {}, retrying {}: {}".format(self._vmname, retry + 1, e)) - last_exception = e - time.sleep(0.6) - continue - - try: - progress.waitForCompletion(-1) - except Exception as e: - raise VirtualBoxError("VirtualBox error: {}".format(e)) - - return progress - - def _lock_machine(self): - - log.debug("locking machine for {}".format(self._vmname)) - # this command is retried several times, because it fails more often... - retries = 4 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not lock the machine after 4 retries: {}".format(last_exception)) - try: - self._machine.lockMachine(self._session, 1) - break - except Exception as e: - log.warn("cannot lock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) - last_exception = e - time.sleep(1) - continue - - def _unlock_machine(self): - - log.debug("unlocking machine for {}".format(self._vmname)) - # this command is retried several times, because it fails more often... - retries = 4 - last_exception = None - for retry in range(retries): - if retry == (retries - 1): - raise VirtualBoxError("Could not unlock the machine after 4 retries: {}".format(last_exception)) - try: - self._session.unlockMachine() - break - except Exception as e: - log.warn("cannot unlock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) - time.sleep(1) - last_exception = e - continue diff --git a/gns3server/version.py b/gns3server/version.py index bb2875b1..573f2248 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta2.dev1" +__version__ = "1.0beta2.dev2" __version_info__ = (1, 0, 0, -99) From 6e39630b9bb53b22f3eb7ce1685aa87b4c30c707 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 22 Aug 2014 17:39:57 -0600 Subject: [PATCH 16/29] Required VirtualBox wrapper is >= 9.1 --- gns3server/modules/virtualbox/vboxwrapper_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/modules/virtualbox/vboxwrapper_client.py b/gns3server/modules/virtualbox/vboxwrapper_client.py index bcaaed11..911f1d50 100644 --- a/gns3server/modules/virtualbox/vboxwrapper_client.py +++ b/gns3server/modules/virtualbox/vboxwrapper_client.py @@ -158,7 +158,7 @@ class VboxWrapperClient(object): self._started = True version = self.send('vboxwrapper version')[0] - if parse_version(version) < parse_version("0.9.2"): + if parse_version(version) < parse_version("0.9.1"): self.stop() raise VirtualBoxError("VirtualBox wrapper version must be >= 0.9.1") From 934404cc907557a23d9805c92b287e47f02cd529 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 25 Aug 2014 15:40:04 -0600 Subject: [PATCH 17/29] Change default port ranges. --- gns3server/modules/dynamips/__init__.py | 2 +- gns3server/modules/iou/__init__.py | 6 +++--- gns3server/modules/virtualbox/__init__.py | 2 +- gns3server/modules/vpcs/__init__.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index d26619a1..afd33ff3 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -137,7 +137,7 @@ class Dynamips(IModule): self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir - self._host = kwargs["host"] + self._host = dynamips_config.get("host", kwargs["host"]) if not sys.platform.startswith("win32"): #FIXME: pickle issues Windows diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index a3f0c47d..f72cf5e9 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -87,11 +87,11 @@ class IOU(IModule): IModule.__init__(self, name, *args, **kwargs) self._iou_instances = {} self._console_start_port_range = iou_config.get("console_start_port_range", 4001) - self._console_end_port_range = iou_config.get("console_end_port_range", 4512) + self._console_end_port_range = iou_config.get("console_end_port_range", 4500) self._allocated_udp_ports = [] self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001) - self._udp_end_port_range = iou_config.get("udp_end_port_range", 40001) - self._host = kwargs["host"] + self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000) + self._host = iou_config.get("host", kwargs["host"]) self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 0ff1bb28..999fdaed 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -93,7 +93,7 @@ class VirtualBox(IModule): self._allocated_udp_ports = [] self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001) self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500) - self._host = kwargs["host"] + self._host = vbox_config.get("host", kwargs["host"]) self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 61de9308..0420175f 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -81,12 +81,12 @@ class VPCS(IModule): # a new process start when calling IModule IModule.__init__(self, name, *args, **kwargs) self._vpcs_instances = {} - self._console_start_port_range = vpcs_config.get("console_start_port_range", 4512) + self._console_start_port_range = vpcs_config.get("console_start_port_range", 4501) self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000) self._allocated_udp_ports = [] - self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 40001) - self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 40512) - self._host = kwargs["host"] + self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501) + self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000) + self._host = vpcs_config.get("host", kwargs["host"]) self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir From 80ab81190cb656f8164adf58120f69debe9e5bd7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 26 Aug 2014 15:27:43 -0600 Subject: [PATCH 18/29] Add "enable console" option to VirtualBox VMs (True by default). Add "start at" option to VirtualBox VMs (adapter start index, 0 by default). --- gns3server/modules/virtualbox/__init__.py | 5 +- gns3server/modules/virtualbox/schemas.py | 12 ++- .../virtualbox/virtualbox_controller.py | 78 +++++++++++++------ .../modules/virtualbox/virtualbox_vm.py | 74 +++++++++++++++++- 4 files changed, 138 insertions(+), 31 deletions(-) diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 999fdaed..cfd8a6aa 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -417,7 +417,10 @@ class VirtualBox(IModule): try: vbox_instance.start() except VirtualBoxError as e: - self.send_custom_error(str(e)) + if self._vboxwrapper: + self.send_custom_error("{}: {}".format(e, self._vboxwrapper.read_stderr())) + else: + self.send_custom_error(str(e)) return self.send_response(True) diff --git a/gns3server/modules/virtualbox/schemas.py b/gns3server/modules/virtualbox/schemas.py index b86839a0..bc72cb9a 100644 --- a/gns3server/modules/virtualbox/schemas.py +++ b/gns3server/modules/virtualbox/schemas.py @@ -82,9 +82,15 @@ VBOX_UPDATE_SCHEMA = { "adapters": { "description": "number of adapters", "type": "integer", - "minimum": 0, + "minimum": 1, "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox }, + "adapter_start_index": { + "description": "adapter index from which to start using adapters", + "type": "integer", + "minimum": 0, + "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox + }, "adapter_type": { "description": "VirtualBox adapter type", "type": "string", @@ -96,6 +102,10 @@ VBOX_UPDATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "enable_console": { + "description": "enable the console", + "type": "boolean" + }, "headless": { "description": "headless mode", "type": "boolean" diff --git a/gns3server/modules/virtualbox/virtualbox_controller.py b/gns3server/modules/virtualbox/virtualbox_controller.py index 72cd8a96..9ed37953 100644 --- a/gns3server/modules/virtualbox/virtualbox_controller.py +++ b/gns3server/modules/virtualbox/virtualbox_controller.py @@ -54,6 +54,7 @@ class VirtualBoxController(object): self._console = 0 self._adapters = [] self._headless = False + self._enable_console = True self._adapter_type = "Automatic" try: @@ -101,6 +102,16 @@ class VirtualBoxController(object): self._headless = headless + @property + def enable_console(self): + + return self._enable_console + + @enable_console.setter + def enable_console(self, enable_console): + + self._enable_console = enable_console + @property def adapters(self): @@ -123,13 +134,17 @@ class VirtualBoxController(object): def start(self): + if len(self._adapters) > self._maximum_adapters: + raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters)) + if self._machine.state == self._vboxmanager.constants.MachineState_Paused: self.resume() return self._get_session() self._set_network_options() - self._set_console_options() + if self._enable_console: + self._set_console_options() progress = self._launch_vm_process() log.info("VM is starting with {}% completed".format(progress.percent)) @@ -145,25 +160,26 @@ class VirtualBoxController(object): except Exception: pass - # starts the Telnet to pipe thread - pipe_name = self._get_pipe_name() - if sys.platform.startswith('win'): - try: - self._serial_pipe = open(pipe_name, "a+b") - except OSError as e: - raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) - self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) - #self._serial_pipe_thread.setDaemon(True) - self._serial_pipe_thread.start() - else: - try: - self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._serial_pipe.connect(pipe_name) - except OSError as e: - raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) - #self._serial_pipe_thread.setDaemon(True) - self._serial_pipe_thread.start() + if self._enable_console: + # starts the Telnet to pipe thread + pipe_name = self._get_pipe_name() + if sys.platform.startswith('win'): + try: + self._serial_pipe = open(pipe_name, "a+b") + except OSError as e: + raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) + self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) + #self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() + else: + try: + self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._serial_pipe.connect(pipe_name) + except OSError as e: + raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) + self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) + #self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() def stop(self): @@ -196,8 +212,14 @@ class VirtualBoxController(object): log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent)) self._lock_machine() + for adapter_id in range(0, len(self._adapters)): + if self._adapters[adapter_id] is None: + continue self._disable_adapter(adapter_id, disable=True) + if self._enable_console: + serial_port = self._session.machine.getSerialPort(0) + serial_port.enabled = False self._session.machine.saveSettings() self._unlock_machine() except Exception as e: @@ -252,11 +274,17 @@ class VirtualBoxController(object): #raise VirtualBoxError("VirtualBox error: {}".format(e)) for adapter_id in range(0, len(self._adapters)): + try: # VirtualBox starts counting from 0 adapter = self._session.machine.getNetworkAdapter(adapter_id) - vbox_adapter_type = adapter.adapterType + if self._adapters[adapter_id] is None: + # force enable to avoid any discrepancy in the interface numbering inside the VM + # e.g. Ethernet2 in GNS3 becoming eth0 inside the VM when using a start index of 2. + adapter.enabled = True + continue + vbox_adapter_type = adapter.adapterType if self._adapter_type == "PCnet-PCI II (Am79C970A)": vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A if self._adapter_type == "PCNet-FAST III (Am79C973)": @@ -310,9 +338,9 @@ class VirtualBoxController(object): except Exception as e: raise VirtualBoxError("VirtualBox error: {}".format(e)) - #for adapter_id in range(len(self._ethernet_adapters), self._maximum_adapters): - # log.debug("disabling remaining adapter {}".format(adapter_id)) - # self._disable_adapter(adapter_id) + for adapter_id in range(len(self._adapters), self._maximum_adapters): + log.debug("disabling remaining adapter {}".format(adapter_id)) + self._disable_adapter(adapter_id) try: self._session.machine.saveSettings() @@ -526,4 +554,4 @@ class VirtualBoxController(object): log.warn("cannot unlock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e)) time.sleep(1) last_exception = e - continue \ No newline at end of file + continue diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index d05ea631..dee20427 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -91,7 +91,9 @@ class VirtualBoxVM(object): self._console = console self._ethernet_adapters = [] self._headless = False + self._enable_console = True self._vmname = vmname + self._adapter_start_index = 0 self._adapter_type = "Automatic" working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id)) @@ -137,9 +139,11 @@ class VirtualBoxVM(object): vbox_defaults = {"name": self._name, "vmname": self._vmname, - "adapters": len(self._ethernet_adapters), + "adapters": self.adapters, + "adapter_start_index": self._adapter_start_index, "adapter_type": "Automatic", "console": self._console, + "enable_console": self._enable_console, "headless": self._headless} return vbox_defaults @@ -329,6 +333,38 @@ class VirtualBoxVM(object): log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id)) self._headless = headless + @property + def enable_console(self): + """ + Returns either the console is enabled or not + + :returns: boolean + """ + + return self._enable_console + + @enable_console.setter + def enable_console(self, enable_console): + """ + Sets either the console is enabled or not + + :param enable_console: boolean + """ + + if enable_console: + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" enable_console True'.format(self._name)) + else: + self._vboxcontroller.enable_console = True + log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id)) + else: + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" enable_console False'.format(self._name)) + else: + self._vboxcontroller.enable_console = False + log.info("VirtualBox VM {name} [id={id}] has disabled the console".format(name=self._name, id=self._id)) + self._enable_console = enable_console + @property def vmname(self): """ @@ -374,17 +410,47 @@ class VirtualBoxVM(object): """ self._ethernet_adapters.clear() - for _ in range(0, adapters): + for adapter_id in range(0, self._adapter_start_index + adapters): + if adapter_id < self._adapter_start_index: + self._ethernet_adapters.append(None) + continue self._ethernet_adapters.append(EthernetAdapter()) if self._vboxwrapper: - self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, len(self._ethernet_adapters))) + self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, adapters)) else: self._vboxcontroller.adapters = self._ethernet_adapters log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, id=self._id, - adapters=len(self._ethernet_adapters))) + adapters=adapters)) + + @property + def adapter_start_index(self): + """ + Returns the adapter start index for this VirtualBox VM instance. + + :returns: index + """ + + return self._adapter_start_index + + @adapter_start_index.setter + def adapter_start_index(self, adapter_start_index): + """ + Sets the adapter start index for this VirtualBox VM instance. + + :param adapter_start_index: index + """ + + if self._vboxwrapper: + self._vboxwrapper.send('vbox setattr "{}" nic_start_index {}'.format(self._name, adapter_start_index)) + + self._adapter_start_index = adapter_start_index + self.adapters = self.adapters # this forces to recreate the adapter list with the correct index + log.info("VirtualBox VM {name} [id={id}]: adapter start index changed to {index}".format(name=self._name, + id=self._id, + index=adapter_start_index)) @property def adapter_type(self): From 76b357c1ce99e09d89840817ce51a750519a3406 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 26 Aug 2014 17:07:48 -0600 Subject: [PATCH 19/29] Do not activate sparse memory by default for c1700 and c2600 platforms. https://github.com/GNS3/dynamips/issues/54 --- gns3server/modules/dynamips/backends/vm.py | 3 ++- gns3server/modules/dynamips/nodes/c1700.py | 4 +++- gns3server/modules/dynamips/nodes/c2600.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py index 1da7e50c..6a471620 100644 --- a/gns3server/modules/dynamips/backends/vm.py +++ b/gns3server/modules/dynamips/backends/vm.py @@ -158,7 +158,8 @@ class VM(object): router = PLATFORMS[platform](hypervisor, name, router_id) router.ram = ram router.image = image - router.sparsemem = self._hypervisor_manager.sparse_memory_support + if platform not in ("c1700", "c2600"): + router.sparsemem = self._hypervisor_manager.sparse_memory_support router.mmap = self._hypervisor_manager.mmap_support if "console" in request: router.console = request["console"] diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 0d59f616..241601a2 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -51,6 +51,7 @@ class C1700(Router): self._chassis = chassis self._iomem = 15 # percentage self._clock_divisor = 8 + self._sparsemem = False if chassis != "1720": self.chassis = chassis @@ -72,7 +73,8 @@ class C1700(Router): "disk1": self._disk1, "chassis": self._chassis, "iomem": self._iomem, - "clock_divisor": self._clock_divisor} + "clock_divisor": self._clock_divisor, + "sparsemem": self._sparsemem} # update the router defaults with the platform specific defaults router_defaults.update(platform_defaults) diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index 155fbf2f..56866235 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -66,6 +66,7 @@ class C2600(Router): self._chassis = chassis self._iomem = 15 # percentage self._clock_divisor = 8 + self._sparsemem = False if chassis != "2610": self.chassis = chassis @@ -87,7 +88,8 @@ class C2600(Router): "disk1": self._disk1, "iomem": self._iomem, "chassis": self._chassis, - "clock_divisor": self._clock_divisor} + "clock_divisor": self._clock_divisor, + "sparsemem": self._sparsemem} # update the router defaults with the platform specific defaults router_defaults.update(platform_defaults) From 9d2e18328bdc967647585fa71ba2bed3624c3453 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 2 Sep 2014 13:06:26 -0600 Subject: [PATCH 20/29] Bump version to 1.0-beta2. --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 573f2248..ddc7c1c2 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta2.dev2" +__version__ = "1.0beta2" __version_info__ = (1, 0, 0, -99) From b483f87c2f8ea6fd1adaa8c065b5af8936373621 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 2 Sep 2014 15:49:39 -0600 Subject: [PATCH 21/29] Bump version to 1.0-beta3.dev1. --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index ddc7c1c2..1dcc2571 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta2" +__version__ = "1.0beta3.dev1" __version_info__ = (1, 0, 0, -99) From d1715baae1ecf5b7366cdc6e6cdd15177c4f0ef0 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 18 Sep 2014 15:47:43 -0600 Subject: [PATCH 22/29] Base QEMU support. --- gns3server/modules/__init__.py | 3 +- gns3server/modules/iou/nios/nio.py | 2 +- .../modules/iou/nios/nio_generic_ethernet.py | 2 +- gns3server/modules/iou/nios/nio_tap.py | 2 +- gns3server/modules/iou/nios/nio_udp.py | 2 +- gns3server/modules/qemu/__init__.py | 684 ++++++++++++++++++ gns3server/modules/qemu/adapters/__init__.py | 0 gns3server/modules/qemu/adapters/adapter.py | 104 +++ .../modules/qemu/adapters/ethernet_adapter.py | 31 + gns3server/modules/qemu/nios/__init__.py | 0 gns3server/modules/qemu/nios/nio.py | 65 ++ gns3server/modules/qemu/nios/nio_udp.py | 75 ++ gns3server/modules/qemu/qemu_error.py | 39 + gns3server/modules/qemu/qemu_vm.py | 616 ++++++++++++++++ gns3server/modules/qemu/schemas.py | 373 ++++++++++ gns3server/modules/virtualbox/__init__.py | 13 +- gns3server/modules/virtualbox/nios/nio.py | 2 +- gns3server/modules/virtualbox/nios/nio_udp.py | 2 +- gns3server/modules/vpcs/nios/nio_tap.py | 2 +- gns3server/modules/vpcs/nios/nio_udp.py | 2 +- gns3server/modules/vpcs/vpcs_device.py | 3 +- 21 files changed, 2005 insertions(+), 17 deletions(-) create mode 100644 gns3server/modules/qemu/__init__.py create mode 100644 gns3server/modules/qemu/adapters/__init__.py create mode 100644 gns3server/modules/qemu/adapters/adapter.py create mode 100644 gns3server/modules/qemu/adapters/ethernet_adapter.py create mode 100644 gns3server/modules/qemu/nios/__init__.py create mode 100644 gns3server/modules/qemu/nios/nio.py create mode 100644 gns3server/modules/qemu/nios/nio_udp.py create mode 100644 gns3server/modules/qemu/qemu_error.py create mode 100644 gns3server/modules/qemu/qemu_vm.py create mode 100644 gns3server/modules/qemu/schemas.py diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 5bd4c110..dd628589 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -20,8 +20,9 @@ from .base import IModule from .dynamips import Dynamips from .vpcs import VPCS from .virtualbox import VirtualBox +from .qemu import Qemu -MODULES = [Dynamips, VPCS, VirtualBox] +MODULES = [Dynamips, VPCS, VirtualBox, Qemu] if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/iou/nios/nio.py b/gns3server/modules/iou/nios/nio.py index 197d4817..059d56a3 100644 --- a/gns3server/modules/iou/nios/nio.py +++ b/gns3server/modules/iou/nios/nio.py @@ -22,7 +22,7 @@ Base interface for NIOs. class NIO(object): """ - IOU NIO. + Network Input/Output. """ def __init__(self): diff --git a/gns3server/modules/iou/nios/nio_generic_ethernet.py b/gns3server/modules/iou/nios/nio_generic_ethernet.py index 45c89b4e..068e9fc3 100644 --- a/gns3server/modules/iou/nios/nio_generic_ethernet.py +++ b/gns3server/modules/iou/nios/nio_generic_ethernet.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_GenericEthernet(NIO): """ - NIO generic Ethernet NIO. + Generic Ethernet NIO. :param ethernet_device: Ethernet device name (e.g. eth0) """ diff --git a/gns3server/modules/iou/nios/nio_tap.py b/gns3server/modules/iou/nios/nio_tap.py index 3164e933..95ec631d 100644 --- a/gns3server/modules/iou/nios/nio_tap.py +++ b/gns3server/modules/iou/nios/nio_tap.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_TAP(NIO): """ - IOU TAP NIO. + TAP NIO. :param tap_device: TAP device name (e.g. tap0) """ diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/modules/iou/nios/nio_udp.py index 41ffbc4f..2c850351 100644 --- a/gns3server/modules/iou/nios/nio_udp.py +++ b/gns3server/modules/iou/nios/nio_udp.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_UDP(NIO): """ - IOU UDP NIO. + UDP NIO. :param lport: local port number :param rhost: remote address/host diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py new file mode 100644 index 00000000..1935b10c --- /dev/null +++ b/gns3server/modules/qemu/__init__.py @@ -0,0 +1,684 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +QEMU server module. +""" + +import os +import socket +import shutil +import subprocess +import re + +from gns3server.modules import IModule +from gns3server.config import Config +from .qemu_vm import QemuVM +from .qemu_error import QemuError +from .nios.nio_udp import NIO_UDP +from ..attic import find_unused_port + +from .schemas import QEMU_CREATE_SCHEMA +from .schemas import QEMU_DELETE_SCHEMA +from .schemas import QEMU_UPDATE_SCHEMA +from .schemas import QEMU_START_SCHEMA +from .schemas import QEMU_STOP_SCHEMA +from .schemas import QEMU_SUSPEND_SCHEMA +from .schemas import QEMU_RELOAD_SCHEMA +from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA +from .schemas import QEMU_ADD_NIO_SCHEMA +from .schemas import QEMU_DELETE_NIO_SCHEMA + +QEMU_BINARIES = ["qemu.exe", + "qemu-system-arm", + "qemu-system-mips", + "qemu-system-ppc", + "qemu-system-sparc", + "qemu-system-x86", + "qemu-system-i386", + "qemu-system-x86_64"] + +import logging +log = logging.getLogger(__name__) + + +class Qemu(IModule): + """ + QEMU module. + + :param name: module name + :param args: arguments for the module + :param kwargs: named arguments for the module + """ + + def __init__(self, name, *args, **kwargs): + + # get the qemu-img location + config = Config.instance() + qemu_config = config.get_section_config(name.upper()) + self._qemu_img_path = qemu_config.get("qemu_img_path") + if not self._qemu_img_path or not os.path.isfile(self._qemu_img_path): + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for qemu-img in the current working directory and $PATH + for path in paths: + try: + if "qemu-img" in os.listdir(path) and os.access(os.path.join(path, "qemu-img"), os.X_OK): + self._qemu_img_path = os.path.join(path, "qemu-img") + break + except OSError: + continue + + if not self._qemu_img_path: + log.warning("qemu-img couldn't be found!") + elif not os.access(self._qemu_img_path, os.X_OK): + log.warning("qemu-img is not executable") + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._qemu_instances = {} + + config = Config.instance() + qemu_config = config.get_section_config(name.upper()) + self._console_start_port_range = qemu_config.get("console_start_port_range", 5001) + self._console_end_port_range = qemu_config.get("console_end_port_range", 5500) + self._allocated_udp_ports = [] + self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001) + self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500) + self._host = qemu_config.get("host", kwargs["host"]) + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + + def stop(self, signum=None): + """ + Properly stops the module. + + :param signum: signal number (if called by the signal handler) + """ + + # delete all QEMU instances + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.delete() + + IModule.stop(self, signum) # this will stop the I/O loop + + def get_qemu_instance(self, qemu_id): + """ + Returns a QEMU VM instance. + + :param qemu_id: QEMU VM identifier + + :returns: QemuVM instance + """ + + if qemu_id not in self._qemu_instances: + log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1) + self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id)) + return None + return self._qemu_instances[qemu_id] + + @IModule.route("qemu.reset") + def reset(self, request): + """ + Resets the module. + + :param request: JSON request + """ + + # delete all QEMU instances + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.delete() + + # resets the instance IDs + QemuVM.reset() + + self._qemu_instances.clear() + self._allocated_udp_ports.clear() + + log.info("QEMU module has been reset") + + @IModule.route("qemu.settings") + def settings(self, request): + """ + Set or update settings. + + Optional request parameters: + - working_dir (path to a working directory) + - project_name + - console_start_port_range + - console_end_port_range + - udp_start_port_range + - udp_end_port_range + + :param request: JSON request + """ + + if request is None: + self.send_param_error() + return + + if "qemu_img_path" in request and request["qemu_img_path"]: + self._qemu_img_path = request["qemu_img_path"] + log.info("QEMU image utility path set to {}".format(self._qemu_img_path)) + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.qemu_img_path = self._qemu_img_path + + if "working_dir" in request: + new_working_dir = request["working_dir"] + log.info("this server is local with working directory path to {}".format(new_working_dir)) + else: + new_working_dir = os.path.join(self._projects_dir, request["project_name"]) + log.info("this server is remote with working directory path to {}".format(new_working_dir)) + if self._projects_dir != self._working_dir != new_working_dir: + if not os.path.isdir(new_working_dir): + try: + shutil.move(self._working_dir, new_working_dir) + except OSError as e: + log.error("could not move working directory from {} to {}: {}".format(self._working_dir, + new_working_dir, + e)) + return + + # update the working directory if it has changed + if self._working_dir != new_working_dir: + self._working_dir = new_working_dir + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_instance.id)) + + if "console_start_port_range" in request and "console_end_port_range" in request: + self._console_start_port_range = request["console_start_port_range"] + self._console_end_port_range = request["console_end_port_range"] + + if "udp_start_port_range" in request and "udp_end_port_range" in request: + self._udp_start_port_range = request["udp_start_port_range"] + self._udp_end_port_range = request["udp_end_port_range"] + + log.debug("received request {}".format(request)) + + @IModule.route("qemu.create") + def qemu_create(self, request): + """ + Creates a new QEMU VM instance. + + Mandatory request parameters: + - name (QEMU VM name) + - qemu_path (path to the Qemu binary) + + Optional request parameters: + - console (QEMU VM console port) + + Response parameters: + - id (QEMU VM instance identifier) + - name (QEMU VM name) + - default settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_CREATE_SCHEMA): + return + + name = request["name"] + qemu_path = request["qemu_path"] + console = request.get("console") + qemu_id = request.get("qemu_id") + + try: + qemu_instance = QemuVM(name, + qemu_path, + self._qemu_img_path, + self._working_dir, + self._host, + qemu_id, + console, + self._console_start_port_range, + self._console_end_port_range) + + except QemuError as e: + self.send_custom_error(str(e)) + return + + response = {"name": qemu_instance.name, + "id": qemu_instance.id} + + defaults = qemu_instance.defaults() + response.update(defaults) + self._qemu_instances[qemu_instance.id] = qemu_instance + self.send_response(response) + + @IModule.route("qemu.delete") + def qemu_delete(self, request): + """ + Deletes a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameter: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_DELETE_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.clean_delete() + del self._qemu_instances[request["id"]] + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + @IModule.route("qemu.update") + def qemu_update(self, request): + """ + Updates a QEMU VM instance + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Optional request parameters: + - any setting to update + + Response parameters: + - updated settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_UPDATE_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + # update the QEMU VM settings + response = {} + for name, value in request.items(): + if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value: + try: + setattr(qemu_instance, name, value) + response[name] = value + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response(response) + + @IModule.route("qemu.start") + def qemu_start(self, request): + """ + Starts a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_START_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.start() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.stop") + def qemu_stop(self, request): + """ + Stops a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_STOP_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.stop() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.reload") + def qemu_reload(self, request): + """ + Reloads a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_RELOAD_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.reload() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.stop") + def qemu_stop(self, request): + """ + Stops a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_STOP_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.stop() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.suspend") + def qemu_suspend(self, request): + """ + Suspends a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_SUSPEND_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.suspend() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.allocate_udp_port") + def allocate_udp_port(self, request): + """ + Allocates a UDP port in order to create an UDP NIO. + + Mandatory request parameters: + - id (QEMU VM identifier) + - port_id (unique port identifier) + + Response parameters: + - port_id (unique port identifier) + - lport (allocated local port) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_ALLOCATE_UDP_PORT_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + port = find_unused_port(self._udp_start_port_range, + self._udp_end_port_range, + host=self._host, + socket_type="UDP", + ignore_ports=self._allocated_udp_ports) + except Exception as e: + self.send_custom_error(str(e)) + return + + self._allocated_udp_ports.append(port) + log.info("{} [id={}] has allocated UDP port {} with host {}".format(qemu_instance.name, + qemu_instance.id, + port, + self._host)) + + response = {"lport": port, + "port_id": request["port_id"]} + self.send_response(response) + + @IModule.route("qemu.add_nio") + def add_nio(self, request): + """ + Adds an NIO (Network Input/Output) for a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + - port (port number) + - port_id (unique port identifier) + - nio (one of the following) + - type "nio_udp" + - lport (local port) + - rhost (remote host) + - rport (remote port) + + Response parameters: + - port_id (unique port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + port = request["port"] + try: + nio = None + if request["nio"]["type"] == "nio_udp": + lport = request["nio"]["lport"] + rhost = request["nio"]["rhost"] + rport = request["nio"]["rport"] + try: + #TODO: handle IPv6 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.connect((rhost, rport)) + except OSError as e: + raise QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) + nio = NIO_UDP(lport, rhost, rport) + if not nio: + raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) + except QemuError as e: + self.send_custom_error(str(e)) + return + + try: + qemu_instance.port_add_nio_binding(port, nio) + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response({"port_id": request["port_id"]}) + + @IModule.route("qemu.delete_nio") + def delete_nio(self, request): + """ + Deletes an NIO (Network Input/Output). + + Mandatory request parameters: + - id (QEMU VM instance identifier) + - port (port identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + port = request["port"] + try: + nio = qemu_instance.port_remove_nio_binding(port) + if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports: + self._allocated_udp_ports.remove(nio.lport) + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + def _get_qemu_version(self, qemu_path): + """ + Gets the Qemu version. + + :param qemu_path: path to Qemu + """ + + try: + output = subprocess.check_output([qemu_path, "--version"]) + match = re.search("QEMU emulator version ([0-9a-z\-\.]+)", output.decode("utf-8")) + if match: + version = match.group(1) + return version + else: + raise QemuError("Could not determine the Qemu version for {}".format(qemu_path)) + except (OSError, subprocess.CalledProcessError) as e: + raise QemuError("Error while looking for the Qemu version: {}".format(e)) + + @IModule.route("qemu.qemu_list") + def qemu_list(self, request): + """ + Gets QEMU binaries list. + + Response parameters: + - Server address/host + - List of Qemu binaries + """ + + qemus = [] + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for Qemu binaries in the current working directory and $PATH + for path in paths: + for qemu_binary in QEMU_BINARIES: + try: + if qemu_binary in os.listdir(path) and os.access(os.path.join(path, qemu_binary), os.X_OK): + qemu_path = os.path.join(path, qemu_binary) + version = self._get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + except OSError: + continue + + response = {"server": self._host, + "qemus": qemus} + self.send_response(response) + + @IModule.route("qemu.echo") + def echo(self, request): + """ + Echo end point for testing purposes. + + :param request: JSON request + """ + + if request is None: + self.send_param_error() + else: + log.debug("received request {}".format(request)) + self.send_response(request) diff --git a/gns3server/modules/qemu/adapters/__init__.py b/gns3server/modules/qemu/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/qemu/adapters/adapter.py b/gns3server/modules/qemu/adapters/adapter.py new file mode 100644 index 00000000..cf439427 --- /dev/null +++ b/gns3server/modules/qemu/adapters/adapter.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + + +class Adapter(object): + """ + Base class for adapters. + + :param interfaces: number of interfaces supported by this adapter. + """ + + def __init__(self, interfaces=1): + + self._interfaces = interfaces + + self._ports = {} + for port_id in range(0, interfaces): + self._ports[port_id] = None + + def removable(self): + """ + Returns True if the adapter can be removed from a slot + and False if not. + + :returns: boolean + """ + + return True + + def port_exists(self, port_id): + """ + Checks if a port exists on this adapter. + + :returns: True is the port exists, + False otherwise. + """ + + if port_id in self._ports: + return True + return False + + def add_nio(self, port_id, nio): + """ + Adds a NIO to a port on this adapter. + + :param port_id: port ID (integer) + :param nio: NIO instance + """ + + self._ports[port_id] = nio + + def remove_nio(self, port_id): + """ + Removes a NIO from a port on this adapter. + + :param port_id: port ID (integer) + """ + + self._ports[port_id] = None + + def get_nio(self, port_id): + """ + Returns the NIO assigned to a port. + + :params port_id: port ID (integer) + + :returns: NIO instance + """ + + return self._ports[port_id] + + @property + def ports(self): + """ + Returns port to NIO mapping + + :returns: dictionary port -> NIO + """ + + return self._ports + + @property + def interfaces(self): + """ + Returns the number of interfaces supported by this adapter. + + :returns: number of interfaces + """ + + return self._interfaces diff --git a/gns3server/modules/qemu/adapters/ethernet_adapter.py b/gns3server/modules/qemu/adapters/ethernet_adapter.py new file mode 100644 index 00000000..27426ec2 --- /dev/null +++ b/gns3server/modules/qemu/adapters/ethernet_adapter.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +from .adapter import Adapter + + +class EthernetAdapter(Adapter): + """ + QEMU Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=1) + + def __str__(self): + + return "QEMU Ethernet adapter" diff --git a/gns3server/modules/qemu/nios/__init__.py b/gns3server/modules/qemu/nios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/qemu/nios/nio.py b/gns3server/modules/qemu/nios/nio.py new file mode 100644 index 00000000..eee5f1d5 --- /dev/null +++ b/gns3server/modules/qemu/nios/nio.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 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 . + +""" +Base interface for NIOs. +""" + + +class NIO(object): + """ + Network Input/Output. + """ + + def __init__(self): + + self._capturing = False + self._pcap_output_file = "" + + def startPacketCapture(self, pcap_output_file): + """ + + :param pcap_output_file: PCAP destination file for the capture + """ + + self._capturing = True + self._pcap_output_file = pcap_output_file + + def stopPacketCapture(self): + + self._capturing = False + self._pcap_output_file = "" + + @property + def capturing(self): + """ + Returns either a capture is configured on this NIO. + + :returns: boolean + """ + + return self._capturing + + @property + def pcap_output_file(self): + """ + Returns the path to the PCAP output file. + + :returns: path to the PCAP output file + """ + + return self._pcap_output_file diff --git a/gns3server/modules/qemu/nios/nio_udp.py b/gns3server/modules/qemu/nios/nio_udp.py new file mode 100644 index 00000000..2c850351 --- /dev/null +++ b/gns3server/modules/qemu/nios/nio_udp.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 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 . + +""" +Interface for UDP NIOs. +""" + +from .nio import NIO + + +class NIO_UDP(NIO): + """ + UDP NIO. + + :param lport: local port number + :param rhost: remote address/host + :param rport: remote port number + """ + + _instance_count = 0 + + def __init__(self, lport, rhost, rport): + + NIO.__init__(self) + self._lport = lport + self._rhost = rhost + self._rport = rport + + @property + def lport(self): + """ + Returns the local port + + :returns: local port number + """ + + return self._lport + + @property + def rhost(self): + """ + Returns the remote host + + :returns: remote address/host + """ + + return self._rhost + + @property + def rport(self): + """ + Returns the remote port + + :returns: remote port number + """ + + return self._rport + + def __str__(self): + + return "NIO UDP" diff --git a/gns3server/modules/qemu/qemu_error.py b/gns3server/modules/qemu/qemu_error.py new file mode 100644 index 00000000..55135a34 --- /dev/null +++ b/gns3server/modules/qemu/qemu_error.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +Custom exceptions for QEMU module. +""" + + +class QemuError(Exception): + + def __init__(self, message, original_exception=None): + + Exception.__init__(self, message) + if isinstance(message, Exception): + message = str(message) + self._message = message + self._original_exception = original_exception + + def __repr__(self): + + return self._message + + def __str__(self): + + return self._message diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py new file mode 100644 index 00000000..2a32b035 --- /dev/null +++ b/gns3server/modules/qemu/qemu_vm.py @@ -0,0 +1,616 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +QEMU VM instance. +""" + +import os +import shutil +import random +import subprocess + +from .qemu_error import QemuError +from .adapters.ethernet_adapter import EthernetAdapter +from .nios.nio_udp import NIO_UDP +from ..attic import find_unused_port + +import logging +log = logging.getLogger(__name__) + + +class QemuVM(object): + """ + QEMU VM implementation. + + :param name: name of this QEMU VM + :param qemu_path: path to the QEMU binary + :param qemu_img_path: path to the QEMU IMG binary + :param working_dir: path to a working directory + :param host: host/address to bind for console and UDP connections + :param qemu_id: QEMU VM instance ID + :param console: TCP console port + :param console_start_port_range: TCP console port range start + :param console_end_port_range: TCP console port range end + """ + + _instances = [] + _allocated_console_ports = [] + + def __init__(self, + name, + qemu_path, + qemu_img_path, + working_dir, + host="127.0.0.1", + qemu_id=None, + console=None, + console_start_port_range=5001, + console_end_port_range=5500): + + if not qemu_id: + self._id = 0 + for identifier in range(1, 1024): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break + + if self._id == 0: + raise QemuError("Maximum number of QEMU VM instances reached") + else: + if qemu_id in self._instances: + raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id)) + self._id = qemu_id + self._instances.append(self._id) + + self._name = name + self._working_dir = None + self._host = host + self._command = [] + self._started = False + self._process = None + self._stdout_file = "" + self._qemu_img_path = qemu_img_path + self._console_start_port_range = console_start_port_range + self._console_end_port_range = console_end_port_range + + # QEMU settings + self._qemu_path = qemu_path + self._disk_image = "" + self._options = "" + self._ram = 256 + self._console = console + self._ethernet_adapters = [] + self._adapter_type = "e1000" + + working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id)) + + if qemu_id and not os.path.isdir(working_dir_path): + raise QemuError("Working directory {} doesn't exist".format(working_dir_path)) + + # create the device own working directory + self.working_dir = working_dir_path + + if not self._console: + # allocate a console port + try: + self._console = find_unused_port(self._console_start_port_range, + self._console_end_port_range, + self._host, + ignore_ports=self._allocated_console_ports) + except Exception as e: + raise QemuError(e) + + if self._console in self._allocated_console_ports: + raise QemuError("Console port {} is already used by another QEMU VM".format(console)) + self._allocated_console_ports.append(self._console) + + self.adapters = 1 # creates 1 adapter by default + log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for this QEMU VM. + + :returns: default values (dictionary) + """ + + qemu_defaults = {"name": self._name, + "qemu_path": self._qemu_path, + "ram": self._ram, + "disk_image": self._disk_image, + "options": self._options, + "adapters": self.adapters, + "adapter_type": self._adapter_type, + "console": self._console} + + return qemu_defaults + + @property + def id(self): + """ + Returns the unique ID for this QEMU VM. + + :returns: id (integer) + """ + + return self._id + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + cls._allocated_console_ports.clear() + + @property + def name(self): + """ + Returns the name of this QEMU VM. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this QEMU VM. + + :param new_name: name + """ + + log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + self._name = new_name + + @property + def working_dir(self): + """ + Returns current working directory + + :returns: path to the working directory + """ + + return self._working_dir + + @working_dir.setter + def working_dir(self, working_dir): + """ + Sets the working directory this QEMU VM. + + :param working_dir: path to the working directory + """ + + try: + os.makedirs(working_dir) + except FileExistsError: + pass + except OSError as e: + raise QemuError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name, + id=self._id, + wd=self._working_dir)) + + @property + def console(self): + """ + Returns the TCP console port. + + :returns: console port (integer) + """ + + return self._console + + @console.setter + def console(self, console): + """ + Sets the TCP console port. + + :param console: console port (integer) + """ + + if console in self._allocated_console_ports: + raise QemuError("Console port {} is already used by another QEMU VM".format(console)) + + self._allocated_console_ports.remove(self._console) + self._console = console + self._allocated_console_ports.append(self._console) + + log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + def delete(self): + """ + Deletes this QEMU VM. + """ + + self.stop() + if self._id in self._instances: + self._instances.remove(self._id) + + if self.console and self.console in self._allocated_console_ports: + self._allocated_console_ports.remove(self.console) + + log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + def clean_delete(self): + """ + Deletes this QEMU VM & all files. + """ + + self.stop() + if self._id in self._instances: + self._instances.remove(self._id) + + if self.console: + self._allocated_console_ports.remove(self.console) + + try: + shutil.rmtree(self._working_dir) + except OSError as e: + log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name, + id=self._id, + error=e)) + return + + log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, + id=self._id)) + + @property + def qemu_path(self): + """ + Returns the QEMU binary path for this QEMU VM. + + :returns: QEMU path + """ + + return self._qemu_path + + @qemu_path.setter + def qemu_path(self, qemu_path): + """ + Sets the QEMU binary path this QEMU VM. + + :param qemu_path: QEMU path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name, + id=self._id, + qemu_path=qemu_path)) + self._qemu_path = qemu_path + + @property + def qemu_img_path(self): + """ + Returns the QEMU IMG binary path for this QEMU VM. + + :returns: QEMU IMG path + """ + + return self._qemu_img_path + + @qemu_img_path.setter + def qemu_img_path(self, qemu_img_path): + """ + Sets the QEMU IMG binary path this QEMU VM. + + :param qemu_img_path: QEMU IMG path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU IMG path to {qemu_img_path}".format(name=self._name, + id=self._id, + qemu_img_path=qemu_img_path)) + self._qemu_img_path = qemu_img_path + + @property + def disk_image(self): + """ + Returns the disk image path for this QEMU VM. + + :returns: QEMU disk image path + """ + + return self._disk_image + + @disk_image.setter + def disk_image(self, disk_image): + """ + Sets the disk image for this QEMU VM. + + :param disk_image: QEMU disk image path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=disk_image)) + self._disk_image = disk_image + + @property + def adapters(self): + """ + Returns the number of Ethernet adapters for this QEMU VM instance. + + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @adapters.setter + def adapters(self, adapters): + """ + Sets the number of Ethernet adapters for this QEMU VM instance. + + :param adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for adapter_id in range(0, adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=adapters)) + + @property + def adapter_type(self): + """ + Returns the adapter type for this QEMU VM instance. + + :returns: adapter type (string) + """ + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + """ + Sets the adapter type for this QEMU VM instance. + + :param adapter_type: adapter type (string) + """ + + self._adapter_type = adapter_type + + log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, + id=self._id, + adapter_type=adapter_type)) + + def start(self): + """ + Starts this QEMU VM. + """ + + if not self.is_running(): + + if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): + raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) + + #TODO: check binary image is valid? + self._command = self._build_command() + + try: + log.info("starting QEMU: {}".format(self._command)) + self._stdout_file = os.path.join(self._working_dir, "qemu.log") + log.info("logging to {}".format(self._stdout_file)) + with open(self._stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except OSError as e: + stdout = self.read_stdout() + log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + + def stop(self): + """ + Stops this QEMU VM. + """ + + # stop the QEMU process + if self.is_running(): + log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid)) + try: + self._process.terminate() + self._process.wait(1) + except subprocess.TimeoutExpired: + self._process.kill() + if self._process.poll() is None: + log.warn("QEMU VM instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + self._started = False + + def suspend(self): + """ + Suspends this QEMU VM. + """ + + pass + + def reload(self): + """ + Reloads this QEMU VM. + """ + + pass + + def resume(self): + """ + Resumes this QEMU VM. + """ + + pass + + def port_add_nio_binding(self, adapter_id, nio): + """ + Adds a port NIO binding. + + :param adapter_id: adapter ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + adapter.add_nio(0, nio) + log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + + def port_remove_nio_binding(self, adapter_id): + """ + Removes a port NIO binding. + + :param adapter_id: adapter ID + + :returns: NIO instance + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + nio = adapter.get_nio(0) + adapter.remove_nio(0) + log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + return nio + + @property + def started(self): + """ + Returns either this QEMU VM has been started or not. + + :returns: boolean + """ + + return self._started + + def read_stdout(self): + """ + Reads the standard output of the QEMU process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._stdout_file: + try: + with open(self._stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the QEMU process is running + + :returns: True or False + """ + + if self._process and self._process.poll() is None: + return True + return False + + def command(self): + """ + Returns the QEMU command line. + + :returns: QEMU command line (string) + """ + + return " ".join(self._build_command()) + + def _serial_options(self): + + if self._console: + return ["-serial", "telnet:{}:{},server,nowait".format(self._host, self._console)] + else: + return [] + + def _disk_options(self): + + hda_disk = os.path.join(self._working_dir, "hda.disk") + if not os.path.exists(hda_disk): + try: + retcode = subprocess.call([self._qemu_img_path, "create", "-o", + "backing_file={}".format(self._disk_image), + "-f", "qcow2", hda_disk]) + log.info("{} returned with {}".format(self._qemu_img_path, retcode)) + except OSError as e: + raise QemuError("Could not create disk image {}".format(e)) + + return ["-hda", hda_disk] + + def _network_options(self): + + network_options = [] + adapter_id = 0 + for adapter in self._ethernet_adapters: + nio = adapter.get_nio(0) + if nio: + #TODO: let users specific the base mac address + mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) + network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) + if isinstance(nio, NIO_UDP): + network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)]) + else: + network_options.extend(["-device", "{}".format(self._adapter_type)]) + adapter_id += 1 + + return network_options + + def _build_command(self): + """ + Command to start the QEMU process. + (to be passed to subprocess.Popen()) + """ + + command = [self._qemu_path] + command.extend(["-name", self._name]) + command.extend(["-m", str(self._ram)]) + command.extend(self._disk_options()) + command.extend(self._serial_options()) + command.extend(self._network_options()) + return command diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/modules/qemu/schemas.py new file mode 100644 index 00000000..bfcbfdff --- /dev/null +++ b/gns3server/modules/qemu/schemas.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + + +QEMU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new QEMU VM instance", + "type": "object", + "properties": { + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "Path to QEMU", + "type": "string", + "minLength": 1, + }, + "qemu_id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "qemu_path"], +} + +QEMU_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "path to QEMU", + "type": "string", + "minLength": 1, + }, + "disk_image": { + "description": "QEMU disk image path", + "type": "string", + "minLength": 1, + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 1, + "maximum": 8, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "options": { + "description": "additional QEMU options", + "type": "string", + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_SUSPEND_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to suspend a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the QEMU VM instance", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id", "port_id"] +} + +QEMU_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a QEMU VM instance", + "type": "object", + + "definitions": { + "UDP": { + "description": "UDP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_udp"] + }, + "lport": { + "description": "Local port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "rhost": { + "description": "Remote host", + "type": "string", + "minLength": 1 + }, + "rport": { + "description": "Remote port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + }, + "required": ["type", "lport", "rhost", "rport"], + "additionalProperties": False + }, + "Ethernet": { + "description": "Generic Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_generic_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "LinuxEthernet": { + "description": "Linux Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_linux_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "TAP": { + "description": "TAP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_tap"] + }, + "tap_device": { + "description": "TAP device name e.g. tap0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "tap_device"], + "additionalProperties": False + }, + "UNIX": { + "description": "UNIX Network Input/Output", + "properties": { + "type": { + "enum": ["nio_unix"] + }, + "local_file": { + "description": "path to the UNIX socket file (local)", + "type": "string", + "minLength": 1 + }, + "remote_file": { + "description": "path to the UNIX socket file (remote)", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "local_file", "remote_file"], + "additionalProperties": False + }, + "VDE": { + "description": "VDE Network Input/Output", + "properties": { + "type": { + "enum": ["nio_vde"] + }, + "control_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + "local_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "control_file", "local_file"], + "additionalProperties": False + }, + "NULL": { + "description": "NULL Network Input/Output", + "properties": { + "type": { + "enum": ["nio_null"] + }, + }, + "required": ["type"], + "additionalProperties": False + }, + }, + + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the QEMU VM instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 8 + }, + "nio": { + "type": "object", + "description": "Network Input/Output", + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/TAP"}, + {"$ref": "#/definitions/UNIX"}, + {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/NULL"}, + ] + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "nio"] +} + + +QEMU_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 8 + }, + }, + "additionalProperties": False, + "required": ["id", "port"] +} diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index cfd8a6aa..d7af5c48 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -68,7 +68,7 @@ class VirtualBox(IModule): self._vboxwrapper_path = vbox_config.get("vboxwrapper_path") if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path): paths = [os.getcwd()] + os.environ["PATH"].split(":") - # look for iouyap in the current working directory and $PATH + # look for vboxwrapper in the current working directory and $PATH for path in paths: try: if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK): @@ -172,9 +172,9 @@ class VirtualBox(IModule): """ Returns a VirtualBox VM instance. - :param vbox_id: VirtualBox device identifier + :param vbox_id: VirtualBox VM identifier - :returns: VBoxDevice instance + :returns: VirtualBoxVM instance """ if vbox_id not in self._vbox_instances: @@ -271,6 +271,7 @@ class VirtualBox(IModule): Mandatory request parameters: - name (VirtualBox VM name) + - vmname (VirtualBox VM name in VirtualBox) Optional request parameters: - console (VirtualBox VM console port) @@ -653,7 +654,7 @@ class VirtualBox(IModule): Deletes an NIO (Network Input/Output). Mandatory request parameters: - - id (VPCS instance identifier) + - id (VirtualBox instance identifier) - port (port identifier) Response parameters: @@ -688,7 +689,7 @@ class VirtualBox(IModule): Starts a packet capture. Mandatory request parameters: - - id (vm identifier) + - id (VirtualBox VM identifier) - port (port number) - port_id (port identifier) - capture_file_name @@ -729,7 +730,7 @@ class VirtualBox(IModule): Stops a packet capture. Mandatory request parameters: - - id (vm identifier) + - id (VirtualBox VM identifier) - port (port number) - port_id (port identifier) diff --git a/gns3server/modules/virtualbox/nios/nio.py b/gns3server/modules/virtualbox/nios/nio.py index c85569bd..eee5f1d5 100644 --- a/gns3server/modules/virtualbox/nios/nio.py +++ b/gns3server/modules/virtualbox/nios/nio.py @@ -22,7 +22,7 @@ Base interface for NIOs. class NIO(object): """ - IOU NIO. + Network Input/Output. """ def __init__(self): diff --git a/gns3server/modules/virtualbox/nios/nio_udp.py b/gns3server/modules/virtualbox/nios/nio_udp.py index 41ffbc4f..2c850351 100644 --- a/gns3server/modules/virtualbox/nios/nio_udp.py +++ b/gns3server/modules/virtualbox/nios/nio_udp.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_UDP(NIO): """ - IOU UDP NIO. + UDP NIO. :param lport: local port number :param rhost: remote address/host diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py index ee550e7b..4c3ed6b2 100644 --- a/gns3server/modules/vpcs/nios/nio_tap.py +++ b/gns3server/modules/vpcs/nios/nio_tap.py @@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only). class NIO_TAP(object): """ - IOU TAP NIO. + TAP NIO. :param tap_device: TAP device name (e.g. tap0) """ diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py index 3142d70e..0527f675 100644 --- a/gns3server/modules/vpcs/nios/nio_udp.py +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -22,7 +22,7 @@ Interface for UDP NIOs. class NIO_UDP(object): """ - IOU UDP NIO. + UDP NIO. :param lport: local port number :param rhost: remote address/host diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 33d5d8e0..d5ad8c09 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -338,11 +338,10 @@ class VPCSDevice(object): """ try: - output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir) + output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) if match: version = match.group(1) - print(version) if parse_version(version) < parse_version("0.5b1"): raise VPCSError("VPCS executable version must be >= 0.5b1") else: From aca9e0de56c018d7fc83fbee27fc3633cc76373e Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 22 Sep 2014 21:24:55 -0600 Subject: [PATCH 23/29] Qemu integration stage 2, support for ASA and IDS. --- gns3server/modules/qemu/__init__.py | 28 --- gns3server/modules/qemu/qemu_vm.py | 275 ++++++++++++++++++++++------ gns3server/modules/qemu/schemas.py | 24 ++- gns3server/version.py | 2 +- 4 files changed, 245 insertions(+), 84 deletions(-) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 1935b10c..7d0b5efd 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -67,26 +67,6 @@ class Qemu(IModule): def __init__(self, name, *args, **kwargs): - # get the qemu-img location - config = Config.instance() - qemu_config = config.get_section_config(name.upper()) - self._qemu_img_path = qemu_config.get("qemu_img_path") - if not self._qemu_img_path or not os.path.isfile(self._qemu_img_path): - paths = [os.getcwd()] + os.environ["PATH"].split(":") - # look for qemu-img in the current working directory and $PATH - for path in paths: - try: - if "qemu-img" in os.listdir(path) and os.access(os.path.join(path, "qemu-img"), os.X_OK): - self._qemu_img_path = os.path.join(path, "qemu-img") - break - except OSError: - continue - - if not self._qemu_img_path: - log.warning("qemu-img couldn't be found!") - elif not os.access(self._qemu_img_path, os.X_OK): - log.warning("qemu-img is not executable") - # a new process start when calling IModule IModule.__init__(self, name, *args, **kwargs) self._qemu_instances = {} @@ -173,13 +153,6 @@ class Qemu(IModule): self.send_param_error() return - if "qemu_img_path" in request and request["qemu_img_path"]: - self._qemu_img_path = request["qemu_img_path"] - log.info("QEMU image utility path set to {}".format(self._qemu_img_path)) - for qemu_id in self._qemu_instances: - qemu_instance = self._qemu_instances[qemu_id] - qemu_instance.qemu_img_path = self._qemu_img_path - if "working_dir" in request: new_working_dir = request["working_dir"] log.info("this server is local with working directory path to {}".format(new_working_dir)) @@ -245,7 +218,6 @@ class Qemu(IModule): try: qemu_instance = QemuVM(name, qemu_path, - self._qemu_img_path, self._working_dir, self._host, qemu_id, diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 2a32b035..ce341780 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -23,6 +23,7 @@ import os import shutil import random import subprocess +import shlex from .qemu_error import QemuError from .adapters.ethernet_adapter import EthernetAdapter @@ -39,7 +40,6 @@ class QemuVM(object): :param name: name of this QEMU VM :param qemu_path: path to the QEMU binary - :param qemu_img_path: path to the QEMU IMG binary :param working_dir: path to a working directory :param host: host/address to bind for console and UDP connections :param qemu_id: QEMU VM instance ID @@ -54,7 +54,6 @@ class QemuVM(object): def __init__(self, name, qemu_path, - qemu_img_path, working_dir, host="127.0.0.1", qemu_id=None, @@ -85,18 +84,21 @@ class QemuVM(object): self._started = False self._process = None self._stdout_file = "" - self._qemu_img_path = qemu_img_path self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range # QEMU settings self._qemu_path = qemu_path - self._disk_image = "" + self._hda_disk_image = "" + self._hdb_disk_image = "" self._options = "" self._ram = 256 self._console = console self._ethernet_adapters = [] self._adapter_type = "e1000" + self._initrd = "" + self._kernel_image = "" + self._kernel_command_line = "" working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id)) @@ -134,11 +136,15 @@ class QemuVM(object): qemu_defaults = {"name": self._name, "qemu_path": self._qemu_path, "ram": self._ram, - "disk_image": self._disk_image, + "hda_disk_image": self._hda_disk_image, + "hdb_disk_image": self._hdb_disk_image, "options": self._options, "adapters": self.adapters, "adapter_type": self._adapter_type, - "console": self._console} + "console": self._console, + "initrd": self._initrd, + "kernel_image": self._kernel_image, + "kernel_command_line": self._kernel_command_line} return qemu_defaults @@ -306,50 +312,51 @@ class QemuVM(object): self._qemu_path = qemu_path @property - def qemu_img_path(self): + def hda_disk_image(self): """ - Returns the QEMU IMG binary path for this QEMU VM. + Returns the hda disk image path for this QEMU VM. - :returns: QEMU IMG path + :returns: QEMU hda disk image path """ - return self._qemu_img_path + return self._hda_disk_image - @qemu_img_path.setter - def qemu_img_path(self, qemu_img_path): + @hda_disk_image.setter + def hda_disk_image(self, hda_disk_image): """ - Sets the QEMU IMG binary path this QEMU VM. + Sets the hda disk image for this QEMU VM. - :param qemu_img_path: QEMU IMG path + :param hda_disk_image: QEMU hda disk image path """ - log.info("QEMU VM {name} [id={id}] has set the QEMU IMG path to {qemu_img_path}".format(name=self._name, - id=self._id, - qemu_img_path=qemu_img_path)) - self._qemu_img_path = qemu_img_path + log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=hda_disk_image)) + self._hda_disk_image = hda_disk_image @property - def disk_image(self): + def hdb_disk_image(self): """ - Returns the disk image path for this QEMU VM. + Returns the hdb disk image path for this QEMU VM. - :returns: QEMU disk image path + :returns: QEMU hdb disk image path """ - return self._disk_image + return self._hdb_disk_image - @disk_image.setter - def disk_image(self, disk_image): + @hdb_disk_image.setter + def hdb_disk_image(self, hdb_disk_image): """ - Sets the disk image for this QEMU VM. + Sets the hdb disk image for this QEMU VM. - :param disk_image: QEMU disk image path + :param hdb_disk_image: QEMU hdb disk image path """ - log.info("QEMU VM {name} [id={id}] has set the QEMU disk image path to {disk_image}".format(name=self._name, - id=self._id, - disk_image=disk_image)) - self._disk_image = disk_image + log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=hdb_disk_image)) + self._hdb_disk_image = hdb_disk_image + @property def adapters(self): @@ -401,6 +408,121 @@ class QemuVM(object): id=self._id, adapter_type=adapter_type)) + @property + def ram(self): + """ + Returns the RAM amount for this QEMU VM. + + :returns: RAM amount in MB + """ + + return self._ram + + @ram.setter + def ram(self, ram): + """ + Sets the amount of RAM for this QEMU VM. + + :param ram: RAM amount in MB + """ + + log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name, + id=self._id, + ram=ram)) + self._ram = ram + + @property + def options(self): + """ + Returns the options for this QEMU VM. + + :returns: QEMU options + """ + + return self._options + + @options.setter + def options(self, options): + """ + Sets the options for this QEMU VM. + + :param options: QEMU options + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name, + id=self._id, + options=options)) + self._options = options + + @property + def initrd(self): + """ + Returns the initrd path for this QEMU VM. + + :returns: QEMU initrd path + """ + + return self._initrd + + @initrd.setter + def initrd(self, initrd): + """ + Sets the initrd path for this QEMU VM. + + :param initrd: QEMU initrd path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name, + id=self._id, + initrd=initrd)) + self._initrd = initrd + + @property + def kernel_image(self): + """ + Returns the kernel image path for this QEMU VM. + + :returns: QEMU kernel image path + """ + + return self._kernel_image + + @kernel_image.setter + def kernel_image(self, kernel_image): + """ + Sets the kernel image path for this QEMU VM. + + :param kernel_image: QEMU kernel image path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name, + id=self._id, + kernel_image=kernel_image)) + self._kernel_image = kernel_image + + @property + def kernel_command_line(self): + """ + Returns the kernel command line for this QEMU VM. + + :returns: QEMU kernel command line + """ + + return self._kernel_command_line + + @kernel_command_line.setter + def kernel_command_line(self, kernel_command_line): + """ + Sets the kernel command line for this QEMU VM. + + :param kernel_command_line: QEMU kernel command line + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name, + id=self._id, + kernel_command_line=kernel_command_line)) + self._kernel_command_line = kernel_command_line + def start(self): """ Starts this QEMU VM. @@ -411,9 +533,7 @@ class QemuVM(object): if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) - #TODO: check binary image is valid? self._command = self._build_command() - try: log.info("starting QEMU: {}".format(self._command)) self._stdout_file = os.path.join(self._working_dir, "qemu.log") @@ -567,36 +687,81 @@ class QemuVM(object): def _disk_options(self): - hda_disk = os.path.join(self._working_dir, "hda.disk") - if not os.path.exists(hda_disk): - try: - retcode = subprocess.call([self._qemu_img_path, "create", "-o", - "backing_file={}".format(self._disk_image), - "-f", "qcow2", hda_disk]) - log.info("{} returned with {}".format(self._qemu_img_path, retcode)) - except OSError as e: - raise QemuError("Could not create disk image {}".format(e)) + options = [] + qemu_img_path = "" + qemu_path_dir = os.path.dirname(self._qemu_path) + try: + for f in os.listdir(qemu_path_dir): + if f.startswith("qemu-img"): + qemu_img_path = os.path.join(qemu_path_dir, f) + except OSError as e: + raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e)) - return ["-hda", hda_disk] + if not qemu_img_path: + raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir)) + + try: + if self._hda_disk_image: + hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2") + if not os.path.exists(hda_disk): + retcode = subprocess.call([qemu_img_path, "create", "-o", + "backing_file={}".format(self._hda_disk_image), + "-f", "qcow2", hda_disk]) + log.info("{} returned with {}".format(qemu_img_path, retcode)) + else: + # create a "FLASH" with 256MB if no disk image has been specified + hda_disk = os.path.join(self._working_dir, "flash.qcow2") + if not os.path.exists(hda_disk): + retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "256M"]) + log.info("{} returned with {}".format(qemu_img_path, retcode)) + + except OSError as e: + raise QemuError("Could not create disk image {}".format(e)) + + options.extend(["-hda", hda_disk]) + if self._hdb_disk_image: + hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2") + if not os.path.exists(hdb_disk): + try: + retcode = subprocess.call([qemu_img_path, "create", "-o", + "backing_file={}".format(self._hdb_disk_image), + "-f", "qcow2", hdb_disk]) + log.info("{} returned with {}".format(qemu_img_path, retcode)) + except OSError as e: + raise QemuError("Could not create disk image {}".format(e)) + options.extend(["-hdb", hdb_disk]) + + return options + + def _linux_boot_options(self): + + options = [] + if self._initrd: + options.extend(["-initrd", self._initrd]) + if self._kernel_image: + options.extend(["-kernel", self._kernel_image]) + if self._kernel_command_line: + options.extend(["-append", self._kernel_command_line]) + + return options def _network_options(self): network_options = [] adapter_id = 0 for adapter in self._ethernet_adapters: + #TODO: let users specify a base mac address + mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) + network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) nio = adapter.get_nio(0) - if nio: - #TODO: let users specific the base mac address - mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) - network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) - if isinstance(nio, NIO_UDP): - network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)]) + if nio and isinstance(nio, NIO_UDP): + network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)]) else: - network_options.extend(["-device", "{}".format(self._adapter_type)]) + network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)]) adapter_id += 1 return network_options @@ -611,6 +776,10 @@ class QemuVM(object): command.extend(["-name", self._name]) command.extend(["-m", str(self._ram)]) command.extend(self._disk_options()) + command.extend(self._linux_boot_options()) command.extend(self._serial_options()) + additional_options = self._options.strip() + if additional_options: + command.extend(shlex.split(additional_options)) command.extend(self._network_options()) return command diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/modules/qemu/schemas.py index bfcbfdff..5b00e98a 100644 --- a/gns3server/modules/qemu/schemas.py +++ b/gns3server/modules/qemu/schemas.py @@ -79,8 +79,13 @@ QEMU_UPDATE_SCHEMA = { "type": "string", "minLength": 1, }, - "disk_image": { - "description": "QEMU disk image path", + "hda_disk_image": { + "description": "QEMU hda disk image path", + "type": "string", + "minLength": 1, + }, + "hdb_disk_image": { + "description": "QEMU hdb disk image path", "type": "string", "minLength": 1, }, @@ -105,6 +110,21 @@ QEMU_UPDATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "initrd": { + "description": "QEMU initrd path", + "type": "string", + "minLength": 1, + }, + "kernel_image": { + "description": "QEMU kernel image path", + "type": "string", + "minLength": 1, + }, + "kernel_command_line": { + "description": "QEMU kernel command line", + "type": "string", + "minLength": 1, + }, "options": { "description": "additional QEMU options", "type": "string", diff --git a/gns3server/version.py b/gns3server/version.py index 1dcc2571..8e0b974c 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta3.dev1" +__version__ = "1.0beta3.dev2" __version_info__ = (1, 0, 0, -99) From e7141685ccf0add0031465e7b06b695dab484cdf Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 23 Sep 2014 21:38:51 -0600 Subject: [PATCH 24/29] Tweaks to support Qemu on Windows. --- gns3server/modules/qemu/__init__.py | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 7d0b5efd..ba40f028 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -19,6 +19,7 @@ QEMU server module. """ +import sys import os import socket import shutil @@ -43,15 +44,6 @@ from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA from .schemas import QEMU_ADD_NIO_SCHEMA from .schemas import QEMU_DELETE_NIO_SCHEMA -QEMU_BINARIES = ["qemu.exe", - "qemu-system-arm", - "qemu-system-mips", - "qemu-system-ppc", - "qemu-system-sparc", - "qemu-system-x86", - "qemu-system-i386", - "qemu-system-x86_64"] - import logging log = logging.getLogger(__name__) @@ -603,6 +595,8 @@ class Qemu(IModule): :param qemu_path: path to Qemu """ + if sys.platform.startswith("win"): + return "" try: output = subprocess.check_output([qemu_path, "--version"]) match = re.search("QEMU emulator version ([0-9a-z\-\.]+)", output.decode("utf-8")) @@ -627,15 +621,22 @@ class Qemu(IModule): qemus = [] paths = [os.getcwd()] + os.environ["PATH"].split(":") # look for Qemu binaries in the current working directory and $PATH + if sys.platform.startswith("win"): + # add specific Windows paths + paths.append(os.path.join(os.getcwd(), "qemu")) + if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]): + paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu")) + if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]): + paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu")) for path in paths: - for qemu_binary in QEMU_BINARIES: - try: - if qemu_binary in os.listdir(path) and os.access(os.path.join(path, qemu_binary), os.X_OK): - qemu_path = os.path.join(path, qemu_binary) + try: + for f in os.listdir(path): + if f.startswith("qemu-system") and os.access(os.path.join(path, f), os.X_OK): + qemu_path = os.path.join(path, f) version = self._get_qemu_version(qemu_path) qemus.append({"path": qemu_path, "version": version}) - except OSError: - continue + except OSError: + continue response = {"server": self._host, "qemus": qemus} From a49f107af2abca9317471de9b090fd2061c10ea9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 24 Sep 2014 11:01:33 -0600 Subject: [PATCH 25/29] Bump to version 1.0-beta3. --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 8e0b974c..d82cd9d9 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta3.dev2" +__version__ = "1.0beta3" __version_info__ = (1, 0, 0, -99) From 6dce005594ee7f9eb9ac72783f61f974fd3d9d56 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 24 Sep 2014 11:14:28 -0600 Subject: [PATCH 26/29] Bump to version 1.0-beta1.dev1. --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index d82cd9d9..c7f8b7ef 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.0beta3" +__version__ = "1.0beta4.dev1" __version_info__ = (1, 0, 0, -99) From 04f670cb5069338f9ea9ffe4b67d0c798043245c Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Sat, 27 Sep 2014 19:56:45 +0200 Subject: [PATCH 27/29] Instruction for development on MacOS X --- README.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d733fa63..8713028e 100644 --- a/README.rst +++ b/README.rst @@ -34,4 +34,19 @@ Please use our all-in-one installer. Mac OS X -------- -Please use our DMG package. +Please use our DMG package for a simple installation. + + +If you want to test the current git version or contribute to the project. + +You can follow this instructions with virtualenwrapper: http://virtualenvwrapper.readthedocs.org/ +and homebrew: http://brew.sh/. + +.. code:: bash + + brew install python3 + mkvirtualenv gns3-server --python=/usr/local/bin/python3.4 + python3 setup.py install + gns3server + + From 20dc779fd8a9668e4e546b2c198dccc1f0023fc7 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Sat, 27 Sep 2014 19:27:26 +0100 Subject: [PATCH 28/29] Fix test for dynamips c7200 NPE (Default is now NPE-400) --- tests/dynamips/test_c7200.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/dynamips/test_c7200.py b/tests/dynamips/test_c7200.py index ed5df69e..7b74cc7f 100644 --- a/tests/dynamips/test_c7200.py +++ b/tests/dynamips/test_c7200.py @@ -29,9 +29,9 @@ def test_router_exists(router_c7200): def test_npe(router_c7200): - assert router_c7200.npe == "npe-200" # default value - router_c7200.npe = "npe-400" - assert router_c7200.npe == "npe-400" + assert router_c7200.npe == "npe-400" # default value + router_c7200.npe = "npe-200" + assert router_c7200.npe == "npe-200" def test_midplane(router_c7200): From a8193fa0635efeefd92216ac185c013a69a9fb89 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 28 Sep 2014 18:23:27 -0600 Subject: [PATCH 29/29] Split the PATH environment variable using os.pathsep --- gns3server/modules/dynamips/__init__.py | 2 +- gns3server/modules/iou/__init__.py | 2 +- gns3server/modules/qemu/__init__.py | 2 +- gns3server/modules/virtualbox/__init__.py | 2 +- gns3server/modules/vpcs/__init__.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index afd33ff3..a35a71c5 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -111,7 +111,7 @@ class Dynamips(IModule): dynamips_config = config.get_section_config(name.upper()) self._dynamips = dynamips_config.get("dynamips_path") if not self._dynamips or not os.path.isfile(self._dynamips): - paths = [os.getcwd()] + os.environ["PATH"].split(":") + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) # look for Dynamips in the current working directory and $PATH for path in paths: try: diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index f72cf5e9..648223bf 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -68,7 +68,7 @@ class IOU(IModule): iou_config = config.get_section_config(name.upper()) self._iouyap = iou_config.get("iouyap_path") if not self._iouyap or not os.path.isfile(self._iouyap): - paths = [os.getcwd()] + os.environ["PATH"].split(":") + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) # look for iouyap in the current working directory and $PATH for path in paths: try: diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index ba40f028..754ffbbf 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -619,7 +619,7 @@ class Qemu(IModule): """ qemus = [] - paths = [os.getcwd()] + os.environ["PATH"].split(":") + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) # look for Qemu binaries in the current working directory and $PATH if sys.platform.startswith("win"): # add specific Windows paths diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index d7af5c48..0d2a97fc 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -67,7 +67,7 @@ class VirtualBox(IModule): vbox_config = config.get_section_config(name.upper()) self._vboxwrapper_path = vbox_config.get("vboxwrapper_path") if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path): - paths = [os.getcwd()] + os.environ["PATH"].split(":") + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) # look for vboxwrapper in the current working directory and $PATH for path in paths: try: diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 0420175f..b88324a4 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -63,7 +63,7 @@ class VPCS(IModule): vpcs_config = config.get_section_config(name.upper()) self._vpcs = vpcs_config.get("vpcs_path") if not self._vpcs or not os.path.isfile(self._vpcs): - paths = [os.getcwd()] + os.environ["PATH"].split(":") + paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) # look for VPCS in the current working directory and $PATH for path in paths: try: