diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index c84e2b02..5213c27e 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -38,7 +38,7 @@ class VPCSHandler(object): vm = yield from vpcs.create_vm(request.json['name']) response.json({'name': vm.name, "vpcs_id": vm.id, - "console": 4242}) + "console": vm.console}) @classmethod @Route.post( diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index dbb29dce..abfd5df1 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -73,7 +73,7 @@ class BaseManager: else: if identifier in self._vms: raise DeviceError("VM identifier {} is already used by another VM instance".format(identifier)) - vm = self._VM_CLASS(vmname, identifier) + vm = self._VM_CLASS(vmname, identifier, self.port_manager) yield from vm.wait_for_creation() self._vms[vm.id] = vm return vm diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 73921266..f9f41827 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -26,7 +26,7 @@ log = logging.getLogger(__name__) class BaseVM: _allocated_console_ports = [] - def __init__(self, name, identifier): + def __init__(self, name, identifier, port_manager): self._loop = asyncio.get_event_loop() self._allocate_console() self._queue = asyncio.Queue() @@ -34,6 +34,7 @@ class BaseVM: self._id = identifier self._created = asyncio.Future() self._worker = asyncio.async(self._run()) + self._port_manager = port_manager log.info("{type} device {name} [id={id}] has been created".format( type=self.__class__.__name__, name=self._name, diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py new file mode 100644 index 00000000..2b833566 --- /dev/null +++ b/gns3server/modules/port_manager.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ipaddress +from .attic import find_unused_port + +class PortManager: + """ + :param console: TCP console port + :param console_host: IP address to bind for console connections + :param console_start_port_range: TCP console port range start + :param console_end_port_range: TCP console port range end + """ + def __init__(self, + console_host, + console_bind_to_any, + console_start_port_range=10000, + console_end_port_range=15000): + + self._console_start_port_range = console_start_port_range + self._console_end_port_range = console_end_port_range + self._used_ports = set() + + if console_bind_to_any: + if ipaddress.ip_address(console_host).version == 6: + self._console_host = "::" + else: + self._console_host = "0.0.0.0" + else: + self._console_host = console_host + + def get_free_port(self): + """Get an available console port and reserve it""" + port = find_unused_port(self._console_start_port_range, + self._console_end_port_range, + host=self._console_host, + socket_type='TCP', + ignore_ports=self._used_ports) + self._used_ports.add(port) + return port + + def reserve_port(port): + """ + Reserve a specific port number + + :param port: Port number + """ + if port in self._used_ports: + raise Exception("Port already {} in use".format(port)) + self._used_ports.add(port) + + def release_port(port): + """ + Release a specific port number + + :param port: Port number + """ + self._used_ports.remove(port) + diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 008acac8..61c896a0 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -48,17 +48,11 @@ class VPCSDevice(BaseVM): :param path: path to VPCS executable :param working_dir: path to a working directory :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end """ - def __init__(self, name, vpcs_id, + def __init__(self, name, vpcs_id, port_manager, path = None, working_dir = None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4512, - console_end_port_range=5000): + console=None): #self._path = path #self._working_dir = working_dir @@ -67,13 +61,10 @@ class VPCSDevice(BaseVM): self._working_dir = "/tmp" self._console = console - self._console_host = console_host self._command = [] self._process = None self._vpcs_stdout_file = "" self._started = False - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range # VPCS settings self._script_file = "" @@ -87,8 +78,16 @@ class VPCSDevice(BaseVM): # # create the device own working directory # self.working_dir = working_dir_path # + try: + if not self._console: + self._console = port_manager.get_free_port() + else: + self._console = port_manager.reserve_port(self._console) + except Exception as e: + raise VPCSError(e) + self._check_requirements() - super().__init__(name, vpcs_id) + super().__init__(name, vpcs_id, port_manager) def _check_requirements(self): """ @@ -180,7 +179,6 @@ class VPCSDevice(BaseVM): flags = 0 if sys.platform.startswith("win32"): flags = subprocess.CREATE_NEW_PROCESS_GROUP - yield from asyncio.create_subprocess_exec() with open(self._vpcs_stdout_file, "w") as fd: self._process = yield from asyncio.create_subprocess_exec(*self._command, stdout=fd, diff --git a/gns3server/server.py b/gns3server/server.py index 1a22bbeb..0db04449 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -24,7 +24,6 @@ import sys import signal import asyncio import aiohttp -import ipaddress import functools import types import time @@ -32,6 +31,7 @@ import time from .web.route import Route from .config import Config from .modules import MODULES +from .modules.port_manager import PortManager #TODO: get rid of * have something generic to automatically import handlers so the routes can be found from gns3server.handlers import * @@ -48,14 +48,7 @@ class Server: self._port = port self._loop = None self._start_time = time.time() - - if console_bind_to_any: - if ipaddress.ip_address(self._host).version == 6: - self._console_host = "::" - else: - self._console_host = "0.0.0.0" - else: - self._console_host = self._host + self._port_manager = PortManager(host, console_bind_to_any) #TODO: server config file support, to be reviewed # # get the projects and temp directories from the configuration file (passed to the modules) @@ -147,7 +140,8 @@ class Server: app.router.add_route(method, route, handler) for module in MODULES: log.debug("loading module {}".format(module.__name__)) - module.instance() + m = module.instance() + m.port_manager = self._port_manager log.info("starting server on {}:{}".format(self._host, self._port)) self._loop.run_until_complete(self._run_application(app)) diff --git a/tests/api/base.py b/tests/api/base.py index 5650b217..a95d36c2 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -29,6 +29,7 @@ from gns3server.web.route import Route #TODO: get rid of * from gns3server.handlers import * from gns3server.modules import MODULES +from gns3server.modules.port_manager import PortManager class Query: @@ -137,9 +138,12 @@ def loop(request): request.addfinalizer(tear_down) return loop +@pytest.fixture(scope="module") +def port_manager(): + return PortManager("127.0.0.1", False) @pytest.fixture(scope="module") -def server(request, loop): +def server(request, loop, port_manager): port = _get_unused_port() host = "localhost" app = web.Application() @@ -147,6 +151,7 @@ def server(request, loop): app.router.add_route(method, route, handler) for module in MODULES: instance = module.instance() + instance.port_manager = port_manager srv = loop.create_server(app.make_handler(), host, port) srv = loop.run_until_complete(srv) diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 35a5a47e..7eeb8e82 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -20,41 +20,41 @@ import asyncio from tests.utils import asyncio_patch #Move loop to util -from tests.api.base import loop +from tests.api.base import loop, port_manager from asyncio.subprocess import Process from unittest.mock import patch, MagicMock from gns3server.modules.vpcs.vpcs_device import VPCSDevice from gns3server.modules.vpcs.vpcs_error import VPCSError @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) -def test_vm(tmpdir): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") +def test_vm(tmpdir, port_manager): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") assert vm.name == "test" assert vm.id == 42 @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) -def test_vm_invalid_vpcs_version(tmpdir): +def test_vm_invalid_vpcs_version(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") assert vm.name == "test" assert vm.id == 42 -def test_vm_invalid_vpcs_path(tmpdir): +def test_vm_invalid_vpcs_path(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test_fake") assert vm.name == "test" assert vm.id == 42 -def test_start(tmpdir, loop): +def test_start(tmpdir, loop, port_manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True -def test_stop(tmpdir, loop): +def test_stop(tmpdir, loop, port_manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True loop.run_until_complete(asyncio.async(vm.stop()))