diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 36d031a6..c57d910e 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -34,7 +34,7 @@ class BaseManager: @classmethod def instance(cls): """ - Singleton to return only one instance of Manager. + Singleton to return only one instance of BaseManager. :returns: instance of Manager """ diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 5fe85b64..17ef0ff5 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -18,7 +18,6 @@ import asyncio from .vm_error import VMError -from .attic import find_unused_port from ..config import Config import logging @@ -26,6 +25,7 @@ log = logging.getLogger(__name__) class BaseVM: + def __init__(self, name, identifier, port_manager): self._loop = asyncio.get_event_loop() @@ -33,13 +33,12 @@ class BaseVM: self._name = name self._id = identifier self._created = asyncio.Future() - self._worker = asyncio.async(self._run()) self._port_manager = port_manager self._config = Config.instance() - log.info("{type} device {name} [id={id}] has been created".format( - type=self.__class__.__name__, - name=self._name, - id=self._id)) + self._worker = asyncio.async(self._run()) + log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, + name=self._name, + id=self._id)) @property def id(self): @@ -62,13 +61,19 @@ class BaseVM: return self._name @asyncio.coroutine - def _execute(self, subcommand, args): - """Called when we receive an event""" + def _execute(self, command): + """ + Called when we receive an event. + """ + raise NotImplementedError @asyncio.coroutine def _create(self): - """Called when the run loop start""" + """ + Called when the run loop start + """ + raise NotImplementedError @asyncio.coroutine @@ -82,12 +87,12 @@ class BaseVM: return while True: - future, subcommand, args = yield from self._queue.get() + future, command = yield from self._queue.get() try: try: - yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout) + yield from asyncio.wait_for(self._execute(command), timeout=timeout) except asyncio.TimeoutError: - raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout)) + raise VMError("{} has timed out after {} seconds!".format(command, timeout)) future.set_result(True) except Exception as e: future.set_exception(e) @@ -100,6 +105,7 @@ class BaseVM: """ Starts the VM process. """ + raise NotImplementedError def put(self, *args): diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index 59aed442..8093538e 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -15,61 +15,186 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import socket 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 + :param host: IP address to bind for console connections """ - 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() + def __init__(self, host="127.0.0.1", console_bind_to_any=False): + + self._console_host = host + self._udp_host = host + self._console_port_range = (2000, 4000) + self._udp_port_range = (10000, 20000) + + self._used_tcp_ports = set() + self._used_udp_ports = set() if console_bind_to_any: - if ipaddress.ip_address(console_host).version == 6: + if ipaddress.ip_address(host).version == 6: self._console_host = "::" else: self._console_host = "0.0.0.0" else: - self._console_host = console_host + self._console_host = 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) + @property + def console_host(self): + + return self._console_host + + @console_host.setter + def host(self, new_host): + + self._console_host = new_host + + @property + def console_port_range(self): + + return self._console_port_range + + @console_host.setter + def console_port_range(self, new_range): + + assert isinstance(new_range, tuple) + self._console_port_range = new_range + + @property + def udp_host(self): + + return self._udp_host + + @udp_host.setter + def host(self, new_host): + + self._udp_host = new_host + + @property + def udp_port_range(self): + + return self._udp_port_range + + @udp_host.setter + def udp_port_range(self, new_range): + + assert isinstance(new_range, tuple) + self._udp_port_range = new_range + + @staticmethod + def find_unused_port(start_port, end_port, host="127.0.0.1", socket_type="TCP", ignore_ports=[]): + """ + Finds an unused port in a range. + + :param start_port: first port in the range + :param end_port: last port in the range + :param host: host/address for bind() + :param socket_type: TCP (default) or UDP + :param ignore_ports: list of port to ignore within the range + """ + + if end_port < start_port: + raise Exception("Invalid port range {}-{}".format(start_port, end_port)) + + if socket_type == "UDP": + socket_type = socket.SOCK_DGRAM + else: + socket_type = socket.SOCK_STREAM + + last_exception = None + for port in range(start_port, end_port + 1): + if port in ignore_ports: + continue + try: + if ":" in host: + # IPv6 address support + with socket.socket(socket.AF_INET6, socket_type) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) # the port is available if bind is a success + else: + with socket.socket(socket.AF_INET, socket_type) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) # the port is available if bind is a success + return port + except OSError as e: + last_exception = e + if port + 1 == end_port: + break + else: + continue + + raise Exception("Could not find a free port between {} and {} on host {}, last exception: {}".format(start_port, + end_port, + host, + last_exception)) + + def get_free_console_port(self): + """ + Get an available TCP console port and reserve it + """ + + port = self.find_unused_port(self._console_port_range[0], + self._console_port_range[1], + host=self._console_host, + socket_type="TCP", + ignore_ports=self._used_tcp_ports) + + self._used_tcp_ports.add(port) return port - def reserve_port(self, port): + def reserve_console_port(self, port): """ - Reserve a specific port number + Reserve a specific TCP console 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(self, port): - """ - Release a specific port number - - :param port: Port number + :param port: TCP port number """ - self._used_ports.remove(port) + if port in self._used_tcp_ports: + raise Exception("TCP port already {} in use on host".format(port, self._host)) + self._used_tcp_ports.add(port) + def release_console_port(self, port): + """ + Release a specific TCP console port number + + :param port: TCP port number + """ + + self._used_tcp_ports.remove(port) + + def get_free_udp_port(self): + """ + Get an available UDP port and reserve it + """ + + port = self.find_unused_port(self._udp_port_range[0], + self._udp_port_range[1], + host=self._udp_host, + socket_type="UDP", + ignore_ports=self._used_udp_ports) + + self._used_udp_ports.add(port) + return port + + def reserve_udp_port(self, port): + """ + Reserve a specific UDP port number + + :param port: UDP port number + """ + + if port in self._used_udp_ports: + raise Exception("UDP port already {} in use on host".format(port, self._host)) + self._used_udp_ports.add(port) + + def release_udp_port(self, port): + """ + Release a specific UDP port number + + :param port: UDP port number + """ + + self._used_udp_ports.remove(port) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index d1531eb2..7d5ed339 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -42,6 +42,7 @@ from ..base_vm import BaseVM import logging log = logging.getLogger(__name__) + class VPCSDevice(BaseVM): """ VPCS device implementation. @@ -52,8 +53,8 @@ class VPCSDevice(BaseVM): :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, vpcs_id, port_manager, - working_dir = None, console = None): + def __init__(self, name, vpcs_id, port_manager, working_dir=None, console=None): + super().__init__(name, vpcs_id, port_manager) #self._path = path @@ -95,10 +96,10 @@ class VPCSDevice(BaseVM): """ Check if VPCS is available with the correct version """ + if self._path == "vpcs": self._path = shutil.which("vpcs") - if not self._path: raise VPCSError("No path to a VPCS executable has been set") diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index a2819471..c4b7c71c 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -30,6 +30,13 @@ VPCS_CREATE_SCHEMA = { "description": "VPCS device instance ID", "type": "integer" }, + "uuid": { + "description": "VPCS device UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, "console": { "description": "console TCP port", "minimum": 1,