From a61686a436dc9daa92e0e0e40f1ea96b26181e82 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 08:50:34 -0600 Subject: [PATCH 01/15] Test --- .gitignore | 3 + gns3server/modules/__init__.py | 3 +- gns3server/modules/iou/schemas.py | 7 - gns3server/modules/vpcs/__init__.py | 690 +++++++++++++++++++++++ gns3server/modules/vpcs/nios/__init__.py | 0 gns3server/modules/vpcs/nios/nio_tap.py | 46 ++ gns3server/modules/vpcs/nios/nio_udp.py | 72 +++ gns3server/modules/vpcs/schemas.py | 306 ++++++++++ gns3server/modules/vpcs/vpcs_device.py | 471 ++++++++++++++++ gns3server/modules/vpcs/vpcs_error.py | 39 ++ gns3server/modules/vpcs/vpcscon.py | 642 +++++++++++++++++++++ tests/vpcs/test_vpcs_device.py | 29 + 12 files changed, 2300 insertions(+), 8 deletions(-) create mode 100644 gns3server/modules/vpcs/__init__.py create mode 100644 gns3server/modules/vpcs/nios/__init__.py create mode 100644 gns3server/modules/vpcs/nios/nio_tap.py create mode 100644 gns3server/modules/vpcs/nios/nio_udp.py create mode 100644 gns3server/modules/vpcs/schemas.py create mode 100644 gns3server/modules/vpcs/vpcs_device.py create mode 100644 gns3server/modules/vpcs/vpcs_error.py create mode 100644 gns3server/modules/vpcs/vpcscon.py create mode 100644 tests/vpcs/test_vpcs_device.py diff --git a/.gitignore b/.gitignore index 49bac9bd..1355fd28 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ nosetests.xml .project .pydevproject .settings + +#Gedit temp files +*~ diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 59304d19..c57ddf2b 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,8 +18,9 @@ import sys from .base import IModule from .dynamips import Dynamips +from .vpcs import VPCS -MODULES = [Dynamips] +MODULES = [Dynamips, VPCS] if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/iou/schemas.py b/gns3server/modules/iou/schemas.py index 62ac0ec4..62b35747 100644 --- a/gns3server/modules/iou/schemas.py +++ b/gns3server/modules/iou/schemas.py @@ -313,13 +313,6 @@ IOU_ADD_NIO_SCHEMA = { "minimum": 0, "maximum": 3 }, - - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, "nio": { "type": "object", "description": "Network Input/Output", diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py new file mode 100644 index 00000000..251cb280 --- /dev/null +++ b/gns3server/modules/vpcs/__init__.py @@ -0,0 +1,690 @@ +# -*- 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 . + +""" +VPCS server module. +""" + +import os +import sys +import base64 +import tempfile +import fcntl +import struct +import socket +import shutil + +from gns3server.modules import IModule +from gns3server.config import Config +import gns3server.jsonrpc as jsonrpc +from .vpcs_device import VPCSDevice +from .vpcs_error import VPCSError +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP +from ..attic import find_unused_port + +from .schemas import VPCS_CREATE_SCHEMA +from .schemas import VPCS_DELETE_SCHEMA +from .schemas import VPCS_UPDATE_SCHEMA +from .schemas import VPCS_START_SCHEMA +from .schemas import VPCS_STOP_SCHEMA +from .schemas import VPCS_RELOAD_SCHEMA +from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA +from .schemas import VPCS_ADD_NIO_SCHEMA +from .schemas import VPCS_DELETE_NIO_SCHEMA + +import logging +log = logging.getLogger(__name__) + + +class VPCS(IModule): + """ + VPCS 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 VPCS location + config = Config.instance() + VPCS_config = config.get_section_config(name.upper()) + self._VPCS = VPCS_config.get("VPCS") + if not self._VPCS or not os.path.isfile(self._VPCS): + VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") + if os.path.isfile(VPCS_in_cwd): + self._VPCS = VPCS_in_cwd + else: + # look for VPCS if none is defined or accessible + for path in os.environ["PATH"].split(":"): + try: + if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): + self._VPCS = os.path.join(path, "VPCS") + break + except OSError: + continue + + if not self._VPCS: + log.warning("VPCS binary couldn't be found!") + elif not os.access(self._VPCS, os.X_OK): + log.warning("VPCS is not executable") + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._VPCS_instances = {} + self._console_start_port_range = 4001 + self._console_end_port_range = 4512 + self._allocated_console_ports = [] + self._current_console_port = self._console_start_port_range + self._udp_start_port_range = 30001 + self._udp_end_port_range = 40001 + self._current_udp_port = self._udp_start_port_range + self._host = kwargs["host"] + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + self._VPCSrc = "" + + # check every 5 seconds + self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) + self._VPCS_callback.start() + + def stop(self, signum=None): + """ + Properly stops the module. + + :param signum: signal number (if called by the signal handler) + """ + + self._VPCS_callback.stop() + # delete all VPCS instances + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.delete() + + IModule.stop(self, signum) # this will stop the I/O loop + + def _check_VPCS_is_alive(self): + """ + Periodic callback to check if VPCS and VPCS are alive + for each VPCS instance. + + Sends a notification to the client if not. + """ + + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + if VPCS_instance.started and (not VPCS_instance.is_running() or not VPCS_instance.is_VPCS_running()): + notification = {"module": self.name, + "id": VPCS_id, + "name": VPCS_instance.name} + if not VPCS_instance.is_running(): + stdout = VPCS_instance.read_VPCS_stdout() + notification["message"] = "VPCS has stopped running" + notification["details"] = stdout + self.send_notification("{}.VPCS_stopped".format(self.name), notification) + elif not VPCS_instance.is_VPCS_running(): + stdout = VPCS_instance.read_VPCS_stdout() + notification["message"] = "VPCS has stopped running" + notification["details"] = stdout + self.send_notification("{}.VPCS_stopped".format(self.name), notification) + VPCS_instance.stop() + + def get_VPCS_instance(self, VPCS_id): + """ + Returns an VPCS device instance. + + :param VPCS_id: VPCS device identifier + + :returns: VPCSDevice instance + """ + + if VPCS_id not in self._VPCS_instances: + log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1) + self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id)) + return None + return self._VPCS_instances[VPCS_id] + + @IModule.route("VPCS.reset") + def reset(self, request): + """ + Resets the module. + + :param request: JSON request + """ + + # delete all VPCS instances + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.delete() + + # resets the instance IDs + VPCSDevice.reset() + + self._VPCS_instances.clear() + self._remote_server = False + self._current_console_port = self._console_start_port_range + self._current_udp_port = self._udp_start_port_range + + if self._VPCSrc and os.path.isfile(self._VPCSrc): + try: + log.info("deleting VPCSrc file {}".format(self._VPCSrc)) + os.remove(self._VPCSrc) + except OSError as e: + log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e)) + + log.info("VPCS module has been reset") + + @IModule.route("VPCS.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 == None: + self.send_param_error() + return + + if "VPCS" in request and request["VPCS"]: + self._VPCS = request["VPCS"] + log.info("VPCS path set to {}".format(self._VPCS)) + + 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"] + ".gns3") + 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 VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.working_dir = self._working_dir + + 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)) + + def test_result(self, message, result="error"): + """ + """ + + return {"result": result, "message": message} + + @IModule.route("VPCS.test_settings") + def test_settings(self, request): + """ + """ + + response = [] + + self.send_response(response) + + @IModule.route("VPCS.create") + def VPCS_create(self, request): + """ + Creates a new VPCS instance. + + Mandatory request parameters: + - path (path to the VPCS executable) + + Optional request parameters: + - name (VPCS name) + + Response parameters: + - id (VPCS instance identifier) + - name (VPCS name) + - default settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_CREATE_SCHEMA): + return + + name = None + if "name" in request: + name = request["name"] + VPCS_path = request["path"] + + try: + try: + os.makedirs(self._working_dir) + except FileExistsError: + pass + except OSError as e: + raise VPCSError("Could not create working directory {}".format(e)) + + VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name) + # find a console port + if self._current_console_port > self._console_end_port_range: + self._current_console_port = self._console_start_port_range + try: + VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) + except Exception as e: + raise VPCSError(e) + self._current_console_port += 1 + except VPCSError as e: + self.send_custom_error(str(e)) + return + + response = {"name": VPCS_instance.name, + "id": VPCS_instance.id} + + defaults = VPCS_instance.defaults() + response.update(defaults) + self._VPCS_instances[VPCS_instance.id] = VPCS_instance + self.send_response(response) + + @IModule.route("VPCS.delete") + def VPCS_delete(self, request): + """ + Deletes an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameter: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_DELETE_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + VPCS_instance.delete() + del self._VPCS_instances[request["id"]] + except VPCSError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + @IModule.route("VPCS.update") + def VPCS_update(self, request): + """ + Updates an VPCS instance + + Mandatory request parameters: + - id (VPCS instance identifier) + + Optional request parameters: + - any setting to update + - startup_config_base64 (startup-config base64 encoded) + + Response parameters: + - updated settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_UPDATE_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + response = {} + try: + # a new startup-config has been pushed + if "startup_config_base64" in request: + config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + config = "!\n" + config.replace("\r", "") + config = config.replace('%h', VPCS_instance.name) + config_path = os.path.join(VPCS_instance.working_dir, "startup-config") + try: + with open(config_path, "w") as f: + log.info("saving startup-config to {}".format(config_path)) + f.write(config) + except OSError as e: + raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) + # update the request with the new local startup-config path + request["startup_config"] = os.path.basename(config_path) + + except VPCSError as e: + self.send_custom_error(str(e)) + return + + # update the VPCS settings + for name, value in request.items(): + if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value: + try: + setattr(VPCS_instance, name, value) + response[name] = value + except VPCSError as e: + self.send_custom_error(str(e)) + return + + self.send_response(response) + + @IModule.route("VPCS.start") + def vm_start(self, request): + """ + Starts an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_START_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + log.debug("starting VPCS with command: {}".format(VPCS_instance.command())) + VPCS_instance.VPCS = self._VPCS + VPCS_instance.VPCSrc = self._VPCSrc + VPCS_instance.start() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.stop") + def vm_stop(self, request): + """ + Stops an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_STOP_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + VPCS_instance.stop() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.reload") + def vm_reload(self, request): + """ + Reloads an VPCS instance. + + Mandatory request parameters: + - id (VPCS identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_RELOAD_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + if VPCS_instance.is_running(): + VPCS_instance.stop() + VPCS_instance.start() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.allocate_udp_port") + def allocate_udp_port(self, request): + """ + Allocates a UDP port in order to create an UDP NIO. + + Mandatory request parameters: + - id (VPCS 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, VPCS_ALLOCATE_UDP_PORT_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + + # find a UDP port + if self._current_udp_port >= self._udp_end_port_range: + self._current_udp_port = self._udp_start_port_range + try: + port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") + except Exception as e: + raise VPCSError(e) + self._current_udp_port += 1 + + log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name, + VPCS_instance.id, + port, + self._host)) + response = {"lport": port} + + except VPCSError as e: + self.send_custom_error(str(e)) + return + + response["port_id"] = request["port_id"] + self.send_response(response) + + def _check_for_privileged_access(self, device): + """ + Check if VPCS can access Ethernet and TAP devices. + + :param device: device name + """ + + # we are root, so VPCS should have privileged access too + if os.geteuid() == 0: + return + + # test if VPCS has the CAP_NET_RAW capability + if "security.capability" in os.listxattr(self._VPCS): + try: + caps = os.getxattr(self._VPCS, "security.capability") + # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set + if struct.unpack(". + +""" +Interface for TAP NIOs (UNIX based OSes only). +""" + + +class NIO_TAP(object): + """ + VPCS TAP NIO. + + :param tap_device: TAP device name (e.g. tap0) + """ + + def __init__(self, tap_device): + + self._tap_device = tap_device + + @property + def tap_device(self): + """ + Returns the TAP device used by this NIO. + + :returns: the TAP device name + """ + + return self._tap_device + + def __str__(self): + + return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py new file mode 100644 index 00000000..8dca91b3 --- /dev/null +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -0,0 +1,72 @@ +# -*- 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. +""" + + +class NIO_UDP(object): + """ + VPCS 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): + + 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/vpcs/schemas.py b/gns3server/modules/vpcs/schemas.py new file mode 100644 index 00000000..d1061384 --- /dev/null +++ b/gns3server/modules/vpcs/schemas.py @@ -0,0 +1,306 @@ +# -*- 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 . + + +VPCS_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the VPCS executable", + "type": "string", + "minLength": 1, + } + }, + "required": ["path"] +} + +VPCS_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the VPCS executable", + "type": "string", + "minLength": 1, + }, + "script_file": { + "description": "path to the VPCS startup configuration file", + "type": "string", + "minLength": 1, + }, + "script_file_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + }, + "required": ["id"] +} + +VPCS_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + }, + "required": ["id", "port_id"] +} + +VPCS_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for an VPCS 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": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + "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"}, + ] + }, + }, + "required": ["id", "port_id", "nio"] +} + +VPCS_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py new file mode 100644 index 00000000..3bd3f95a --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -0,0 +1,471 @@ +# -*- 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 . + +""" +VPCS device management (creates command line, processes, files etc.) in +order to run an VPCS instance. +""" + +import os +import re +import signal +import subprocess +import argparse +import threading +import configparser +from .vpcscon import start_vpcscon +from .vpcs_error import VPCSError +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP + +import logging +log = logging.getLogger(__name__) + + +class VPCSDevice(object): + """ + VPCS device implementation. + + :param path: path to VPCS executable + :param working_dir: path to a working directory + :param host: host/address to bind for console and UDP connections + :param name: name of this VPCS device + """ + + _instances = [] + + def __init__(self, path, working_dir, host="127.0.0.1", name=None): + + # find an instance identifier (0 <= id < 255) + # This 255 limit is due to a restriction on the number of possible + # mac addresses given in VPCS using the -m option + self._id = 0 + for identifier in range(0, 255): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break + + if self._id == 0: + raise VPCSError("Maximum number of VPCS instances reached") + + if name: + self._name = name + else: + self._name = "VPCS{}".format(self._id) + self._path = path + self._console = None + self._working_dir = None + self._command = [] + self._process = None + self._vpcs_stdout_file = "" + self._vpcscon_thead = None + self._vpcscon_thread_stop_event = None + self._host = host + self._started = False + + # VPCS settings + self._script_file = "" + + # update the working directory + self.working_dir = working_dir + + log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for VPCS. + + :returns: default values (dictionary) + """ + + vpcs_defaults = {"name": self._name, + "path": self._path, + "script_file": self._script_file, + "console": self._console} + + return vpcs_defaults + + @property + def id(self): + """ + Returns the unique ID for this VPCS device. + + :returns: id (integer) + """ + + return(self._id) + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + + @property + def name(self): + """ + Returns the name of this VPCS device. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VPCS device. + + :param new_name: name + """ + + self._name = new_name + log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + @property + def path(self): + """ + Returns the path to the VPCS executable. + + :returns: path to VPCS + """ + + return(self._path) + + @path.setter + def path(self, path): + """ + Sets the path to the VPCS executable. + + :param path: path to VPCS + """ + + self._path = path + log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, + id=self._id, + path=path)) + + @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 for VPCS. + + :param working_dir: path to the working directory + """ + + # create our own working directory + working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id)) + try: + os.makedirs(working_dir) + except FileExistsError: + pass + except OSError as e: + raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("VPCS {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) + """ + + self._console = console + log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + def command(self): + """ + Returns the VPCS command line. + + :returns: VPCS command line (string) + """ + + return " ".join(self._build_command()) + + def delete(self): + """ + Deletes this VPCS device. + """ + + self.stop() + self._instances.remove(self._id) + log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + @property + def started(self): + """ + Returns either this VPCS device has been started or not. + + :returns: boolean + """ + + return self._started + + def _start_vpcscon(self): + """ + Starts vpcscon thread (for console connections). + """ + + if not self._vpcscon_thead: + telnet_server = "{}:{}".format(self._host, self._console) + log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) + args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) + self._vpcscon_thread_stop_event = threading.Event() + self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) + self._vpcscon_thead.start() ", ".join(missing_libs))) + + def start(self): + """ + Starts the VPCS process. + """ + + if not self.is_running(): + + if not os.path.isfile(self._path): + raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) + + if not os.access(self._path, os.X_OK): + raise VPCSError("VPCS image '{}' is not executable".format(self._path)) + + self._command = self._build_command() + try: + log.info("starting VPCS: {}".format(self._command)) + self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") + log.info("logging to {}".format(self._vpcs_stdout_file)) + with open(self._vpcs_stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except FileNotFoundError as e: + raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e)) + except OSError as e: + vpcs_stdout = self.read_vpcs_stdout() + log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + + # start console support + self._start_vpcscon() + + def stop(self): + """ + Stops the VPCS process. + """ + + # stop the VPCS process + if self.is_running(): + log.info("stopping VPCS 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() == None: + log.warn("VPCS instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + self._started = False + + # stop console support + if self._vpcscon_thead: + self._vpcscon_thread_stop_event.set() + if self._vpcscon_thead.is_alive(): + self._vpcscon_thead.join(timeout=0.10) + self._vpcscon_thead = None + + + def read_vpcs_stdout(self): + """ + Reads the standard output of the VPCS process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._vpcs_stdout_file: + try: + with open(self._vpcs_stdout_file) as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the VPCS process is running + + :returns: True or False + """ + + if self._process and self._process.poll() == None: + return True + return False + + + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + adapter.add_nio(port_id, nio) + log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + + def _build_command(self): + """ + Command to start the VPCS process. + (to be passed to subprocess.Popen()) + + VPCS command line: + usage: vpcs [options] [scriptfile] + Option: + -h print this help then exit + -v print version information then exit + + -p port run as a daemon listening on the tcp 'port' + -m num start byte of ether address, default from 0 + -r file load and execute script file + compatible with older versions, DEPRECATED. + + -e tap mode, using /dev/tapx (linux only) + -u udp mode, default + + udp mode options: + -s port local udp base port, default from 20000 + -c port remote udp base port (dynamips udp port), default from 30000 + -t ip remote host IP, default 127.0.0.1 + + hypervisor mode option: + -H port run as the hypervisor listening on the tcp 'port' + + If no 'scriptfile' specified, vpcs will read and execute the file named + 'startup.vpc' if it exsits in the current directory. + + """ + + command = [self._path] + command.extend(["-p", str(self._console)]) + command.extend(["-s", str(self._lport)]) + command.extend(["-c", str(self._rport)]) + command.extend(["-t", str(self._rhost)]) + command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset + if self._script_file: + command.extend([self._script_file]) + return command + + @property + def script_file(self): + """ + Returns the startup-config for this VPCS instance. + + :returns: path to startup-config file + """ + + return self._script_file + + @script_file.setter + def script_file(self, script_file): + """ + Sets the startup-config for this VPCS instance. + + :param script_file: path to startup-config file + """ + + self._script_file = script_file + log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, + id=self._id, + config=self._script_file)) + + diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py new file mode 100644 index 00000000..167129ba --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -0,0 +1,39 @@ +# -*- 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 . + +""" +Custom exceptions for VPCS module. +""" + + +class VPCSError(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/vpcs/vpcscon.py b/gns3server/modules/vpcs/vpcscon.py new file mode 100644 index 00000000..b481175d --- /dev/null +++ b/gns3server/modules/vpcs/vpcscon.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013, 2014 James E. Carpenter +# +# 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 socket +import sys +import os +import select +import fcntl +import struct +import termios +import tty +import time +import argparse +import traceback + + +import logging +log = logging.getLogger(__name__) + + +# Escape characters +ESC_CHAR = '^^' # can be overriden from command line +ESC_QUIT = 'q' + +# VPCS seems to only send *1* byte at a time. If +# they ever fix that we'll be ready for it. +BUFFER_SIZE = 1024 + +# How long to wait before retrying a connection (seconds) +RETRY_DELAY = 3 + +# How often to test an idle connection (seconds) +POLL_TIMEOUT = 3 + + +EXIT_SUCCESS = 0 +EXIT_FAILURE = 1 +EXIT_ABORT = 2 + +# Mostly from: +# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py +#--[ Telnet Commands ]--------------------------------------------------------- +SE = 240 # End of subnegotiation parameters +NOP = 241 # No operation +DATMK = 242 # Data stream portion of a sync. +BREAK = 243 # NVT Character BRK +IP = 244 # Interrupt Process +AO = 245 # Abort Output +AYT = 246 # Are you there +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # The Go Ahead Signal +SB = 250 # Sub-option to follow +WILL = 251 # Will; request or confirm option begin +WONT = 252 # Wont; deny option request +DO = 253 # Do = Request or confirm remote option +DONT = 254 # Don't = Demand or confirm option halt +IAC = 255 # Interpret as Command +SEND = 1 # Sub-process negotiation SEND command +IS = 0 # Sub-process negotiation IS command +#--[ Telnet Options ]---------------------------------------------------------- +BINARY = 0 # Transmit Binary +ECHO = 1 # Echo characters back to sender +RECON = 2 # Reconnection +SGA = 3 # Suppress Go-Ahead +TMARK = 6 # Timing Mark +TTYPE = 24 # Terminal Type +NAWS = 31 # Negotiate About Window Size +LINEMO = 34 # Line Mode + + +class FileLock: + + # struct flock { /* from fcntl(2) */ + # ... + # short l_type; /* Type of lock: F_RDLCK, + # F_WRLCK, F_UNLCK */ + # short l_whence; /* How to interpret l_start: + # SEEK_SET, SEEK_CUR, SEEK_END */ + # off_t l_start; /* Starting offset for lock */ + # off_t l_len; /* Number of bytes to lock */ + # pid_t l_pid; /* PID of process blocking our lock + # (F_GETLK only) */ + # ... + # }; + _flock = struct.Struct('hhqql') + + def __init__(self, fname=None): + self.fd = None + self.fname = fname + + def get_lock(self): + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, + 0, 0, os.getpid()) + flk = self._flock.unpack( + fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) + + # If it's not locked (or is locked by us) then return None, + # otherwise return the PID of the owner. + if flk[0] == fcntl.F_UNLCK: + return None + return flk[4] + + def lock(self): + try: + self.fd = open('{}.lck'.format(self.fname), 'a') + except Exception as e: + raise LockError("Couldn't get lock on {}: {}" + .format(self.fname, e)) + + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) + try: + fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) + except BlockingIOError: + raise LockError("Already connected. PID {} has lock on {}" + .format(self.get_lock(), self.fname)) + + # If we got here then we must have the lock. Store the PID. + self.fd.truncate(0) + self.fd.write('{}\n'.format(os.getpid())) + self.fd.flush() + + def unlock(self): + if self.fd: + # Deleting first prevents a race condition + os.unlink(self.fd.name) + self.fd.close() + + def __enter__(self): + self.lock() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.unlock() + return False + + +class Console: + def fileno(self): + raise NotImplementedError("Only routers have fileno()") + + +class Router: + pass + + +class TTY(Console): + + def read(self, fileno, bufsize): + return self.fd.read(bufsize) + + def write(self, buf): + return self.fd.write(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def __enter__(self): + try: + self.fd = open('/dev/tty', 'r+b', buffering=0) + except OSError as e: + raise TTYError("Couldn't open controlling TTY: {}".format(e)) + + # Save original flags + self.termios = termios.tcgetattr(self.fd) + self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) + + # Update flags + tty.setraw(self.fd, termios.TCSANOW) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + # Restore flags to original settings + termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) + + self.fd.close() + + return False + + +class TelnetServer(Console): + + def __init__(self, addr, port, stop_event): + self.addr = addr + self.port = port + self.fd_dict = {} + self.stop_event = stop_event + + def read(self, fileno, bufsize): + # Someone wants to connect? + if fileno == self.sock_fd.fileno(): + self._accept() + return None + + self._cur_fileno = fileno + + # Read a maximum of _bufsize_ bytes without blocking. When it + # would want to block it means there's no more data. An empty + # buffer normally means that we've been disconnected. + try: + buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) + except BlockingIOError: + return None + if not buf: + self._disconnect(fileno) + + # Process and remove any telnet commands from the buffer + if IAC in buf: + buf = self._IAC_parser(buf) + + return buf + + def write(self, buf): + for fd in self.fd_dict.values(): + fd.send(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.sock_fd, select.EPOLLIN) + + def _read_block(self, bufsize): + buf = self._read_cur(bufsize, socket.MSG_WAITALL) + # If we don't get everything we were looking for then the + # client probably disconnected. + if len(buf) < bufsize: + self._disconnect(self._cur_fileno) + return buf + + def _read_cur(self, bufsize, flags): + return self.fd_dict[self._cur_fileno].recv(bufsize, flags) + + def _write_cur(self, buf): + return self.fd_dict[self._cur_fileno].send(buf) + + def _IAC_parser(self, buf): + skip_to = 0 + while not self.stop_event.is_set(): + # Locate an IAC to process + iac_loc = buf.find(IAC, skip_to) + if iac_loc < 0: + break + + # Get the TELNET command + iac_cmd = bytearray([IAC]) + try: + iac_cmd.append(buf[iac_loc + 1]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 1]) + + # Is this just a 2-byte TELNET command? + if iac_cmd[1] not in [WILL, WONT, DO, DONT]: + if iac_cmd[1] == AYT: + log.debug("Telnet server received Are-You-There (AYT)") + self._write_cur( + b'\r\nYour Are-You-There received. I am here.\r\n' + ) + elif iac_cmd[1] == IAC: + # It's data, not an IAC + iac_cmd.pop() + # This prevents the 0xff from being + # interputed as yet another IAC + skip_to = iac_loc + 1 + log.debug("Received IAC IAC") + elif iac_cmd[1] == NOP: + pass + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x}".format(*iac_cmd)) + + # This must be a 3-byte TELNET command + else: + try: + iac_cmd.append(buf[iac_loc + 2]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 2]) + # We do ECHO, SGA, and BINARY. Period. + if (iac_cmd[1] == DO + and iac_cmd[2] not in [ECHO, SGA, BINARY]): + + self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + + # Remove the entire TELNET command from the buffer + buf = buf.replace(iac_cmd, b'', 1) + + # Return the new copy of the buffer, minus telnet commands + return buf + + def _accept(self): + fd, addr = self.sock_fd.accept() + self.fd_dict[fd.fileno()] = fd + self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) + + log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) + + # This is a one-way negotiation. This is very basic so there + # shouldn't be any problems with any decent client. + fd.send(bytes([IAC, WILL, ECHO, + IAC, WILL, SGA, + IAC, WILL, BINARY, + IAC, DO, BINARY])) + + if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: + fd.send(b'\r\nToo many connections\r\n') + self._disconnect(fd.fileno()) + log.warn("Client disconnected because of too many connections. " + "(limit currently {})".format(args.telnet_limit)) + + def _disconnect(self, fileno): + fd = self.fd_dict.pop(fileno) + log.info("Telnet client disconnected") + fd.shutdown(socket.SHUT_RDWR) + fd.close() + + def __enter__(self): + # Open a socket and start listening + sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock_fd.bind((self.addr, self.port)) + except OSError: + raise TelnetServerError("Cannot bind to {}:{}" + .format(self.addr, self.port)) + + sock_fd.listen(socket.SOMAXCONN) + self.sock_fd = sock_fd + log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for fileno in list(self.fd_dict.keys()): + self._disconnect(fileno) + self.sock_fd.close() + return False + + +class VPCS(Router): + + def __init__(self, ttyC, ttyS, stop_event): + self.ttyC = ttyC + self.ttyS = ttyS + self.stop_event = stop_event + + def read(self, bufsize): + try: + buf = self.fd.recv(bufsize) + except BlockingIOError: + return None + return buf + + def write(self, buf): + self.fd.send(buf) + + def _open(self): + self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.fd.setblocking(False) + + def _bind(self): + try: + os.unlink(self.ttyC) + except FileNotFoundError: + pass + except Exception as e: + raise NetioError("Couldn't unlink socket {}: {}" + .format(self.ttyC, e)) + + try: + self.fd.bind(self.ttyC) + except Exception as e: + raise NetioError("Couldn't create socket {}: {}" + .format(self.ttyC, e)) + + def _connect(self): + # Keep trying until we connect or die trying + while not self.stop_event.is_set(): + try: + self.fd.connect(self.ttyS) + except FileNotFoundError: + log.debug("Waiting to connect to {}".format(self.ttyS)) + time.sleep(RETRY_DELAY) + except Exception as e: + raise NetioError("Couldn't connect to socket {}: {}" + .format(self.ttyS, e)) + else: + break + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def fileno(self): + return self.fd.fileno() + + def __enter__(self): + self._open() + self._bind() + self._connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + os.unlink(self.ttyC) + self.fd.close() + return False + + +class VPCSConError(Exception): + pass + + +class LockError(VPCSConError): + pass + + +class NetioError(VPCSConError): + pass + + +class TTYError(VPCSConError): + pass + + +class TelnetServerError(VPCSConError): + pass + + +class ConfigError(VPCSConError): + pass + + +def mkdir_netio(netio_dir): + try: + os.mkdir(netio_dir) + except FileExistsError: + pass + except Exception as e: + raise NetioError("Couldn't create directory {}: {}" + .format(netio_dir, e)) + + +def send_recv_loop(console, router, esc_char, stop_event): + + epoll = select.epoll() + router.register(epoll) + console.register(epoll) + + router_fileno = router.fileno() + esc_quit = bytes(ESC_QUIT.upper(), 'ascii') + esc_state = False + + while not stop_event.is_set(): + event_list = epoll.poll(timeout=POLL_TIMEOUT) + + # When/if the poll times out we send an empty datagram. If VPCS + # has gone away then this will toss a ConnectionRefusedError + # exception. + if not event_list: + router.write(b'') + continue + + for fileno, event in event_list: + buf = bytearray() + + # VPCS --> tty(s) + if fileno == router_fileno: + while not stop_event.is_set(): + data = router.read(BUFFER_SIZE) + if not data: + break + buf.extend(data) + console.write(buf) + + # tty --> VPCS + else: + while not stop_event.is_set(): + data = console.read(fileno, BUFFER_SIZE) + if not data: + break + buf.extend(data) + + # If we just received the escape character then + # enter the escape state. + # + # If we are in the escape state then check for a + # quit command. Or if it's the escape character then + # send the escape character. Else, send the escape + # character we ate earlier and whatever character we + # just got. Exit escape state. + # + # If we're not in the escape state and this isn't an + # escape character then just send it to VPCS. + if esc_state: + if buf.upper() == esc_quit: + sys.exit(EXIT_SUCCESS) + elif buf == esc_char: + router.write(esc_char) + else: + router.write(esc_char) + router.write(buf) + esc_state = False + elif buf == esc_char: + esc_state = True + else: + router.write(buf) + + +def get_args(): + parser = argparse.ArgumentParser( + description='Connect to an VPCS console port.') + parser.add_argument('-d', '--debug', action='store_true', + help='display some debugging information') + parser.add_argument('-e', '--escape', + help='set escape character (default: %(default)s)', + default=ESC_CHAR, metavar='CHAR') + parser.add_argument('-t', '--telnet-server', + help='start telnet server listening on ADDR:PORT', + metavar='ADDR:PORT', default=False) + parser.add_argument('-l', '--telnet-limit', + help='maximum number of simultaneous ' + 'telnet connections (default: %(default)s)', + metavar='LIMIT', type=int, default=1) + parser.add_argument('appl_id', help='VPCS instance identifier') + return parser.parse_args() + + +def get_escape_character(escape): + + # Figure out the escape character to use. + # Can be any ASCII character or a spelled out control + # character, like "^e". The string "none" disables it. + if escape.lower() == 'none': + esc_char = b'' + elif len(escape) == 2 and escape[0] == '^': + c = ord(escape[1].upper()) - 0x40 + if not 0 <= c <= 0x1f: # control code range + raise ConfigError("Invalid control code") + esc_char = bytes([c]) + elif len(escape) == 1: + try: + esc_char = bytes(escape, 'ascii') + except ValueError as e: + raise ConfigError("Invalid escape character") from e + else: + raise ConfigError("Invalid length for escape character") + + return esc_char + + +def start_VPCScon(cmdline_args, stop_event): + + global args + args = cmdline_args + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + # default logging level + logging.basicConfig(level=logging.INFO) + + # Create paths for the Unix domain sockets + netio = '/tmp/netio{}'.format(os.getuid()) + ttyC = '{}/ttyC{}'.format(netio, args.appl_id) + ttyS = '{}/ttyS{}'.format(netio, args.appl_id) + + try: + mkdir_netio(netio) + with FileLock(ttyC): + esc_char = get_escape_character(args.escape) + + if args.telnet_server: + addr, _, port = args.telnet_server.partition(':') + nport = 0 + try: + nport = int(port) + except ValueError: + pass + if (addr == '' or nport == 0): + raise ConfigError('format for --telnet-server must be ' + 'ADDR:PORT (like 127.0.0.1:20000)') + + while not stop_event.is_set(): + try: + if args.telnet_server: + with TelnetServer(addr, nport, stop_event) as console: + with VPCS(ttyC, ttyS, stop_event) as router: + send_recv_loop(console, router, b'', stop_event) + else: + with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console: + send_recv_loop(console, router, esc_char, stop_event) + except ConnectionRefusedError: + pass + except KeyboardInterrupt: + sys.exit(EXIT_ABORT) + finally: + # Put us at the beginning of a line + if not args.telnet_server: + print() + + except VPCSConError as e: + if args.debug: + traceback.print_exc(file=sys.stderr) + else: + print(e, file=sys.stderr) + sys.exit(EXIT_FAILURE) + + log.info("exiting...") + + +def main(): + + import threading + stop_event = threading.Event() + args = get_args() + start_VPCScon(args, stop_event) + +if __name__ == '__main__': + main() diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py new file mode 100644 index 00000000..9d72f2e6 --- /dev/null +++ b/tests/vpcs/test_vpcs_device.py @@ -0,0 +1,29 @@ +from gns3server.modules.vpcs import VPCSDevice +import os +import pytest + + +@pytest.fixture(scope="session") +def vpcs(request): + + cwd = os.path.dirname(os.path.abspath(__file__)) + vpcs_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") + vpcs_device = VPCSDevice(vpcs_path, "/tmp") + vpcs_device.start() + request.addfinalizer(vpcs_device.delete) + return vpcs_device + + +def test_vpcs_is_started(vpcs): + + print(vpcs.command()) + assert vpcs.id == 1 # we should have only one VPCS running! + assert vpcs.is_running() + + +def test_vpcs_restart(vpcs): + + vpcs.stop() + assert not vpcs.is_running() + vpcs.start() + assert vpcs.is_running() From c6b4ac04e1d052b1537650d2f807f47e1b8569a8 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 09:05:05 -0600 Subject: [PATCH 02/15] Revert "Test" This reverts commit a61686a436dc9daa92e0e0e40f1ea96b26181e82. --- .gitignore | 3 - gns3server/modules/__init__.py | 3 +- gns3server/modules/iou/schemas.py | 7 + gns3server/modules/vpcs/__init__.py | 690 ----------------------- gns3server/modules/vpcs/nios/__init__.py | 0 gns3server/modules/vpcs/nios/nio_tap.py | 46 -- gns3server/modules/vpcs/nios/nio_udp.py | 72 --- gns3server/modules/vpcs/schemas.py | 306 ---------- gns3server/modules/vpcs/vpcs_device.py | 471 ---------------- gns3server/modules/vpcs/vpcs_error.py | 39 -- gns3server/modules/vpcs/vpcscon.py | 642 --------------------- tests/vpcs/test_vpcs_device.py | 29 - 12 files changed, 8 insertions(+), 2300 deletions(-) delete mode 100644 gns3server/modules/vpcs/__init__.py delete mode 100644 gns3server/modules/vpcs/nios/__init__.py delete mode 100644 gns3server/modules/vpcs/nios/nio_tap.py delete mode 100644 gns3server/modules/vpcs/nios/nio_udp.py delete mode 100644 gns3server/modules/vpcs/schemas.py delete mode 100644 gns3server/modules/vpcs/vpcs_device.py delete mode 100644 gns3server/modules/vpcs/vpcs_error.py delete mode 100644 gns3server/modules/vpcs/vpcscon.py delete mode 100644 tests/vpcs/test_vpcs_device.py diff --git a/.gitignore b/.gitignore index 1355fd28..49bac9bd 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,3 @@ nosetests.xml .project .pydevproject .settings - -#Gedit temp files -*~ diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index c57ddf2b..59304d19 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,9 +18,8 @@ import sys from .base import IModule from .dynamips import Dynamips -from .vpcs import VPCS -MODULES = [Dynamips, VPCS] +MODULES = [Dynamips] if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/iou/schemas.py b/gns3server/modules/iou/schemas.py index 62b35747..62ac0ec4 100644 --- a/gns3server/modules/iou/schemas.py +++ b/gns3server/modules/iou/schemas.py @@ -313,6 +313,13 @@ IOU_ADD_NIO_SCHEMA = { "minimum": 0, "maximum": 3 }, + + "slot": { + "description": "Slot number", + "type": "integer", + "minimum": 0, + "maximum": 15 + }, "nio": { "type": "object", "description": "Network Input/Output", diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py deleted file mode 100644 index 251cb280..00000000 --- a/gns3server/modules/vpcs/__init__.py +++ /dev/null @@ -1,690 +0,0 @@ -# -*- 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 . - -""" -VPCS server module. -""" - -import os -import sys -import base64 -import tempfile -import fcntl -import struct -import socket -import shutil - -from gns3server.modules import IModule -from gns3server.config import Config -import gns3server.jsonrpc as jsonrpc -from .vpcs_device import VPCSDevice -from .vpcs_error import VPCSError -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from ..attic import find_unused_port - -from .schemas import VPCS_CREATE_SCHEMA -from .schemas import VPCS_DELETE_SCHEMA -from .schemas import VPCS_UPDATE_SCHEMA -from .schemas import VPCS_START_SCHEMA -from .schemas import VPCS_STOP_SCHEMA -from .schemas import VPCS_RELOAD_SCHEMA -from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA -from .schemas import VPCS_ADD_NIO_SCHEMA -from .schemas import VPCS_DELETE_NIO_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class VPCS(IModule): - """ - VPCS 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 VPCS location - config = Config.instance() - VPCS_config = config.get_section_config(name.upper()) - self._VPCS = VPCS_config.get("VPCS") - if not self._VPCS or not os.path.isfile(self._VPCS): - VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") - if os.path.isfile(VPCS_in_cwd): - self._VPCS = VPCS_in_cwd - else: - # look for VPCS if none is defined or accessible - for path in os.environ["PATH"].split(":"): - try: - if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): - self._VPCS = os.path.join(path, "VPCS") - break - except OSError: - continue - - if not self._VPCS: - log.warning("VPCS binary couldn't be found!") - elif not os.access(self._VPCS, os.X_OK): - log.warning("VPCS is not executable") - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._VPCS_instances = {} - self._console_start_port_range = 4001 - self._console_end_port_range = 4512 - self._allocated_console_ports = [] - self._current_console_port = self._console_start_port_range - self._udp_start_port_range = 30001 - self._udp_end_port_range = 40001 - self._current_udp_port = self._udp_start_port_range - self._host = kwargs["host"] - self._projects_dir = kwargs["projects_dir"] - self._tempdir = kwargs["temp_dir"] - self._working_dir = self._projects_dir - self._VPCSrc = "" - - # check every 5 seconds - self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) - self._VPCS_callback.start() - - def stop(self, signum=None): - """ - Properly stops the module. - - :param signum: signal number (if called by the signal handler) - """ - - self._VPCS_callback.stop() - # delete all VPCS instances - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.delete() - - IModule.stop(self, signum) # this will stop the I/O loop - - def _check_VPCS_is_alive(self): - """ - Periodic callback to check if VPCS and VPCS are alive - for each VPCS instance. - - Sends a notification to the client if not. - """ - - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - if VPCS_instance.started and (not VPCS_instance.is_running() or not VPCS_instance.is_VPCS_running()): - notification = {"module": self.name, - "id": VPCS_id, - "name": VPCS_instance.name} - if not VPCS_instance.is_running(): - stdout = VPCS_instance.read_VPCS_stdout() - notification["message"] = "VPCS has stopped running" - notification["details"] = stdout - self.send_notification("{}.VPCS_stopped".format(self.name), notification) - elif not VPCS_instance.is_VPCS_running(): - stdout = VPCS_instance.read_VPCS_stdout() - notification["message"] = "VPCS has stopped running" - notification["details"] = stdout - self.send_notification("{}.VPCS_stopped".format(self.name), notification) - VPCS_instance.stop() - - def get_VPCS_instance(self, VPCS_id): - """ - Returns an VPCS device instance. - - :param VPCS_id: VPCS device identifier - - :returns: VPCSDevice instance - """ - - if VPCS_id not in self._VPCS_instances: - log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1) - self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id)) - return None - return self._VPCS_instances[VPCS_id] - - @IModule.route("VPCS.reset") - def reset(self, request): - """ - Resets the module. - - :param request: JSON request - """ - - # delete all VPCS instances - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.delete() - - # resets the instance IDs - VPCSDevice.reset() - - self._VPCS_instances.clear() - self._remote_server = False - self._current_console_port = self._console_start_port_range - self._current_udp_port = self._udp_start_port_range - - if self._VPCSrc and os.path.isfile(self._VPCSrc): - try: - log.info("deleting VPCSrc file {}".format(self._VPCSrc)) - os.remove(self._VPCSrc) - except OSError as e: - log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e)) - - log.info("VPCS module has been reset") - - @IModule.route("VPCS.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 == None: - self.send_param_error() - return - - if "VPCS" in request and request["VPCS"]: - self._VPCS = request["VPCS"] - log.info("VPCS path set to {}".format(self._VPCS)) - - 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"] + ".gns3") - 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 VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.working_dir = self._working_dir - - 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)) - - def test_result(self, message, result="error"): - """ - """ - - return {"result": result, "message": message} - - @IModule.route("VPCS.test_settings") - def test_settings(self, request): - """ - """ - - response = [] - - self.send_response(response) - - @IModule.route("VPCS.create") - def VPCS_create(self, request): - """ - Creates a new VPCS instance. - - Mandatory request parameters: - - path (path to the VPCS executable) - - Optional request parameters: - - name (VPCS name) - - Response parameters: - - id (VPCS instance identifier) - - name (VPCS name) - - default settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_CREATE_SCHEMA): - return - - name = None - if "name" in request: - name = request["name"] - VPCS_path = request["path"] - - try: - try: - os.makedirs(self._working_dir) - except FileExistsError: - pass - except OSError as e: - raise VPCSError("Could not create working directory {}".format(e)) - - VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name) - # find a console port - if self._current_console_port > self._console_end_port_range: - self._current_console_port = self._console_start_port_range - try: - VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) - except Exception as e: - raise VPCSError(e) - self._current_console_port += 1 - except VPCSError as e: - self.send_custom_error(str(e)) - return - - response = {"name": VPCS_instance.name, - "id": VPCS_instance.id} - - defaults = VPCS_instance.defaults() - response.update(defaults) - self._VPCS_instances[VPCS_instance.id] = VPCS_instance - self.send_response(response) - - @IModule.route("VPCS.delete") - def VPCS_delete(self, request): - """ - Deletes an VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - - Response parameter: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_DELETE_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - VPCS_instance.delete() - del self._VPCS_instances[request["id"]] - except VPCSError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("VPCS.update") - def VPCS_update(self, request): - """ - Updates an VPCS instance - - Mandatory request parameters: - - id (VPCS instance identifier) - - Optional request parameters: - - any setting to update - - startup_config_base64 (startup-config base64 encoded) - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_UPDATE_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - response = {} - try: - # a new startup-config has been pushed - if "startup_config_base64" in request: - config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', VPCS_instance.name) - config_path = os.path.join(VPCS_instance.working_dir, "startup-config") - try: - with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(config_path)) - f.write(config) - except OSError as e: - raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) - # update the request with the new local startup-config path - request["startup_config"] = os.path.basename(config_path) - - except VPCSError as e: - self.send_custom_error(str(e)) - return - - # update the VPCS settings - for name, value in request.items(): - if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value: - try: - setattr(VPCS_instance, name, value) - response[name] = value - except VPCSError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("VPCS.start") - def vm_start(self, request): - """ - Starts an VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_START_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - log.debug("starting VPCS with command: {}".format(VPCS_instance.command())) - VPCS_instance.VPCS = self._VPCS - VPCS_instance.VPCSrc = self._VPCSrc - VPCS_instance.start() - except VPCSError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("VPCS.stop") - def vm_stop(self, request): - """ - Stops an VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_STOP_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - VPCS_instance.stop() - except VPCSError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("VPCS.reload") - def vm_reload(self, request): - """ - Reloads an VPCS instance. - - Mandatory request parameters: - - id (VPCS identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_RELOAD_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - if VPCS_instance.is_running(): - VPCS_instance.stop() - VPCS_instance.start() - except VPCSError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("VPCS.allocate_udp_port") - def allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (VPCS 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, VPCS_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - - # find a UDP port - if self._current_udp_port >= self._udp_end_port_range: - self._current_udp_port = self._udp_start_port_range - try: - port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") - except Exception as e: - raise VPCSError(e) - self._current_udp_port += 1 - - log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name, - VPCS_instance.id, - port, - self._host)) - response = {"lport": port} - - except VPCSError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - def _check_for_privileged_access(self, device): - """ - Check if VPCS can access Ethernet and TAP devices. - - :param device: device name - """ - - # we are root, so VPCS should have privileged access too - if os.geteuid() == 0: - return - - # test if VPCS has the CAP_NET_RAW capability - if "security.capability" in os.listxattr(self._VPCS): - try: - caps = os.getxattr(self._VPCS, "security.capability") - # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set - if struct.unpack(". - -""" -Interface for TAP NIOs (UNIX based OSes only). -""" - - -class NIO_TAP(object): - """ - VPCS TAP NIO. - - :param tap_device: TAP device name (e.g. tap0) - """ - - def __init__(self, tap_device): - - self._tap_device = tap_device - - @property - def tap_device(self): - """ - Returns the TAP device used by this NIO. - - :returns: the TAP device name - """ - - return self._tap_device - - def __str__(self): - - return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py deleted file mode 100644 index 8dca91b3..00000000 --- a/gns3server/modules/vpcs/nios/nio_udp.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- 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. -""" - - -class NIO_UDP(object): - """ - VPCS 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): - - 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/vpcs/schemas.py b/gns3server/modules/vpcs/schemas.py deleted file mode 100644 index d1061384..00000000 --- a/gns3server/modules/vpcs/schemas.py +++ /dev/null @@ -1,306 +0,0 @@ -# -*- 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 . - - -VPCS_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new VPCS instance", - "type": "object", - "properties": { - "name": { - "description": "VPCS device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the VPCS executable", - "type": "string", - "minLength": 1, - } - }, - "required": ["path"] -} - -VPCS_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "name": { - "description": "VPCS device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the VPCS executable", - "type": "string", - "minLength": 1, - }, - "script_file": { - "description": "path to the VPCS startup configuration file", - "type": "string", - "minLength": 1, - }, - "script_file_base64": { - "description": "startup configuration base64 encoded", - "type": "string" - }, - }, - "required": ["id"] -} - -VPCS_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VPCS instance", - "type": "integer" - }, - }, - "required": ["id", "port_id"] -} - -VPCS_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an VPCS 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": "VPCS device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VPCS instance", - "type": "integer" - }, - "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"}, - ] - }, - }, - "required": ["id", "port_id", "nio"] -} - -VPCS_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py deleted file mode 100644 index 3bd3f95a..00000000 --- a/gns3server/modules/vpcs/vpcs_device.py +++ /dev/null @@ -1,471 +0,0 @@ -# -*- 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 . - -""" -VPCS device management (creates command line, processes, files etc.) in -order to run an VPCS instance. -""" - -import os -import re -import signal -import subprocess -import argparse -import threading -import configparser -from .vpcscon import start_vpcscon -from .vpcs_error import VPCSError -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP - -import logging -log = logging.getLogger(__name__) - - -class VPCSDevice(object): - """ - VPCS device implementation. - - :param path: path to VPCS executable - :param working_dir: path to a working directory - :param host: host/address to bind for console and UDP connections - :param name: name of this VPCS device - """ - - _instances = [] - - def __init__(self, path, working_dir, host="127.0.0.1", name=None): - - # find an instance identifier (0 <= id < 255) - # This 255 limit is due to a restriction on the number of possible - # mac addresses given in VPCS using the -m option - self._id = 0 - for identifier in range(0, 255): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise VPCSError("Maximum number of VPCS instances reached") - - if name: - self._name = name - else: - self._name = "VPCS{}".format(self._id) - self._path = path - self._console = None - self._working_dir = None - self._command = [] - self._process = None - self._vpcs_stdout_file = "" - self._vpcscon_thead = None - self._vpcscon_thread_stop_event = None - self._host = host - self._started = False - - # VPCS settings - self._script_file = "" - - # update the working directory - self.working_dir = working_dir - - log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - def defaults(self): - """ - Returns all the default attribute values for VPCS. - - :returns: default values (dictionary) - """ - - vpcs_defaults = {"name": self._name, - "path": self._path, - "script_file": self._script_file, - "console": self._console} - - return vpcs_defaults - - @property - def id(self): - """ - Returns the unique ID for this VPCS device. - - :returns: id (integer) - """ - - return(self._id) - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - - @property - def name(self): - """ - Returns the name of this VPCS device. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this VPCS device. - - :param new_name: name - """ - - self._name = new_name - log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - @property - def path(self): - """ - Returns the path to the VPCS executable. - - :returns: path to VPCS - """ - - return(self._path) - - @path.setter - def path(self, path): - """ - Sets the path to the VPCS executable. - - :param path: path to VPCS - """ - - self._path = path - log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, - id=self._id, - path=path)) - - @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 for VPCS. - - :param working_dir: path to the working directory - """ - - # create our own working directory - working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id)) - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("VPCS {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) - """ - - self._console = console - log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - - def command(self): - """ - Returns the VPCS command line. - - :returns: VPCS command line (string) - """ - - return " ".join(self._build_command()) - - def delete(self): - """ - Deletes this VPCS device. - """ - - self.stop() - self._instances.remove(self._id) - log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - @property - def started(self): - """ - Returns either this VPCS device has been started or not. - - :returns: boolean - """ - - return self._started - - def _start_vpcscon(self): - """ - Starts vpcscon thread (for console connections). - """ - - if not self._vpcscon_thead: - telnet_server = "{}:{}".format(self._host, self._console) - log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) - args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) - self._vpcscon_thread_stop_event = threading.Event() - self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) - self._vpcscon_thead.start() ", ".join(missing_libs))) - - def start(self): - """ - Starts the VPCS process. - """ - - if not self.is_running(): - - if not os.path.isfile(self._path): - raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) - - if not os.access(self._path, os.X_OK): - raise VPCSError("VPCS image '{}' is not executable".format(self._path)) - - self._command = self._build_command() - try: - log.info("starting VPCS: {}".format(self._command)) - self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") - log.info("logging to {}".format(self._vpcs_stdout_file)) - with open(self._vpcs_stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) - log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) - self._started = True - except FileNotFoundError as e: - raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e)) - except OSError as e: - vpcs_stdout = self.read_vpcs_stdout() - log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - - # start console support - self._start_vpcscon() - - def stop(self): - """ - Stops the VPCS process. - """ - - # stop the VPCS process - if self.is_running(): - log.info("stopping VPCS 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() == None: - log.warn("VPCS instance {} PID={} is still running".format(self._id, - self._process.pid)) - self._process = None - self._started = False - - # stop console support - if self._vpcscon_thead: - self._vpcscon_thread_stop_event.set() - if self._vpcscon_thead.is_alive(): - self._vpcscon_thead.join(timeout=0.10) - self._vpcscon_thead = None - - - def read_vpcs_stdout(self): - """ - Reads the standard output of the VPCS process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._vpcs_stdout_file: - try: - with open(self._vpcs_stdout_file) as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) - return output - - def is_running(self): - """ - Checks if the VPCS process is running - - :returns: True or False - """ - - if self._process and self._process.poll() == None: - return True - return False - - - def slot_add_nio_binding(self, slot_id, port_id, nio): - """ - Adds a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - :param nio: NIO instance to add to the slot/port - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - adapter.add_nio(port_id, nio) - log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - - def slot_remove_nio_binding(self, slot_id, port_id): - """ - Removes a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - - def _build_command(self): - """ - Command to start the VPCS process. - (to be passed to subprocess.Popen()) - - VPCS command line: - usage: vpcs [options] [scriptfile] - Option: - -h print this help then exit - -v print version information then exit - - -p port run as a daemon listening on the tcp 'port' - -m num start byte of ether address, default from 0 - -r file load and execute script file - compatible with older versions, DEPRECATED. - - -e tap mode, using /dev/tapx (linux only) - -u udp mode, default - - udp mode options: - -s port local udp base port, default from 20000 - -c port remote udp base port (dynamips udp port), default from 30000 - -t ip remote host IP, default 127.0.0.1 - - hypervisor mode option: - -H port run as the hypervisor listening on the tcp 'port' - - If no 'scriptfile' specified, vpcs will read and execute the file named - 'startup.vpc' if it exsits in the current directory. - - """ - - command = [self._path] - command.extend(["-p", str(self._console)]) - command.extend(["-s", str(self._lport)]) - command.extend(["-c", str(self._rport)]) - command.extend(["-t", str(self._rhost)]) - command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset - if self._script_file: - command.extend([self._script_file]) - return command - - @property - def script_file(self): - """ - Returns the startup-config for this VPCS instance. - - :returns: path to startup-config file - """ - - return self._script_file - - @script_file.setter - def script_file(self, script_file): - """ - Sets the startup-config for this VPCS instance. - - :param script_file: path to startup-config file - """ - - self._script_file = script_file - log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, - id=self._id, - config=self._script_file)) - - diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py deleted file mode 100644 index 167129ba..00000000 --- a/gns3server/modules/vpcs/vpcs_error.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- 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 . - -""" -Custom exceptions for VPCS module. -""" - - -class VPCSError(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/vpcs/vpcscon.py b/gns3server/modules/vpcs/vpcscon.py deleted file mode 100644 index b481175d..00000000 --- a/gns3server/modules/vpcs/vpcscon.py +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013, 2014 James E. Carpenter -# -# 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 socket -import sys -import os -import select -import fcntl -import struct -import termios -import tty -import time -import argparse -import traceback - - -import logging -log = logging.getLogger(__name__) - - -# Escape characters -ESC_CHAR = '^^' # can be overriden from command line -ESC_QUIT = 'q' - -# VPCS seems to only send *1* byte at a time. If -# they ever fix that we'll be ready for it. -BUFFER_SIZE = 1024 - -# How long to wait before retrying a connection (seconds) -RETRY_DELAY = 3 - -# How often to test an idle connection (seconds) -POLL_TIMEOUT = 3 - - -EXIT_SUCCESS = 0 -EXIT_FAILURE = 1 -EXIT_ABORT = 2 - -# Mostly from: -# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py -#--[ Telnet Commands ]--------------------------------------------------------- -SE = 240 # End of subnegotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command -SEND = 1 # Sub-process negotiation SEND command -IS = 0 # Sub-process negotiation IS command -#--[ Telnet Options ]---------------------------------------------------------- -BINARY = 0 # Transmit Binary -ECHO = 1 # Echo characters back to sender -RECON = 2 # Reconnection -SGA = 3 # Suppress Go-Ahead -TMARK = 6 # Timing Mark -TTYPE = 24 # Terminal Type -NAWS = 31 # Negotiate About Window Size -LINEMO = 34 # Line Mode - - -class FileLock: - - # struct flock { /* from fcntl(2) */ - # ... - # short l_type; /* Type of lock: F_RDLCK, - # F_WRLCK, F_UNLCK */ - # short l_whence; /* How to interpret l_start: - # SEEK_SET, SEEK_CUR, SEEK_END */ - # off_t l_start; /* Starting offset for lock */ - # off_t l_len; /* Number of bytes to lock */ - # pid_t l_pid; /* PID of process blocking our lock - # (F_GETLK only) */ - # ... - # }; - _flock = struct.Struct('hhqql') - - def __init__(self, fname=None): - self.fd = None - self.fname = fname - - def get_lock(self): - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, - 0, 0, os.getpid()) - flk = self._flock.unpack( - fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) - - # If it's not locked (or is locked by us) then return None, - # otherwise return the PID of the owner. - if flk[0] == fcntl.F_UNLCK: - return None - return flk[4] - - def lock(self): - try: - self.fd = open('{}.lck'.format(self.fname), 'a') - except Exception as e: - raise LockError("Couldn't get lock on {}: {}" - .format(self.fname, e)) - - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) - try: - fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) - except BlockingIOError: - raise LockError("Already connected. PID {} has lock on {}" - .format(self.get_lock(), self.fname)) - - # If we got here then we must have the lock. Store the PID. - self.fd.truncate(0) - self.fd.write('{}\n'.format(os.getpid())) - self.fd.flush() - - def unlock(self): - if self.fd: - # Deleting first prevents a race condition - os.unlink(self.fd.name) - self.fd.close() - - def __enter__(self): - self.lock() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.unlock() - return False - - -class Console: - def fileno(self): - raise NotImplementedError("Only routers have fileno()") - - -class Router: - pass - - -class TTY(Console): - - def read(self, fileno, bufsize): - return self.fd.read(bufsize) - - def write(self, buf): - return self.fd.write(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def __enter__(self): - try: - self.fd = open('/dev/tty', 'r+b', buffering=0) - except OSError as e: - raise TTYError("Couldn't open controlling TTY: {}".format(e)) - - # Save original flags - self.termios = termios.tcgetattr(self.fd) - self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) - - # Update flags - tty.setraw(self.fd, termios.TCSANOW) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - - # Restore flags to original settings - termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) - - self.fd.close() - - return False - - -class TelnetServer(Console): - - def __init__(self, addr, port, stop_event): - self.addr = addr - self.port = port - self.fd_dict = {} - self.stop_event = stop_event - - def read(self, fileno, bufsize): - # Someone wants to connect? - if fileno == self.sock_fd.fileno(): - self._accept() - return None - - self._cur_fileno = fileno - - # Read a maximum of _bufsize_ bytes without blocking. When it - # would want to block it means there's no more data. An empty - # buffer normally means that we've been disconnected. - try: - buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) - except BlockingIOError: - return None - if not buf: - self._disconnect(fileno) - - # Process and remove any telnet commands from the buffer - if IAC in buf: - buf = self._IAC_parser(buf) - - return buf - - def write(self, buf): - for fd in self.fd_dict.values(): - fd.send(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.sock_fd, select.EPOLLIN) - - def _read_block(self, bufsize): - buf = self._read_cur(bufsize, socket.MSG_WAITALL) - # If we don't get everything we were looking for then the - # client probably disconnected. - if len(buf) < bufsize: - self._disconnect(self._cur_fileno) - return buf - - def _read_cur(self, bufsize, flags): - return self.fd_dict[self._cur_fileno].recv(bufsize, flags) - - def _write_cur(self, buf): - return self.fd_dict[self._cur_fileno].send(buf) - - def _IAC_parser(self, buf): - skip_to = 0 - while not self.stop_event.is_set(): - # Locate an IAC to process - iac_loc = buf.find(IAC, skip_to) - if iac_loc < 0: - break - - # Get the TELNET command - iac_cmd = bytearray([IAC]) - try: - iac_cmd.append(buf[iac_loc + 1]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 1]) - - # Is this just a 2-byte TELNET command? - if iac_cmd[1] not in [WILL, WONT, DO, DONT]: - if iac_cmd[1] == AYT: - log.debug("Telnet server received Are-You-There (AYT)") - self._write_cur( - b'\r\nYour Are-You-There received. I am here.\r\n' - ) - elif iac_cmd[1] == IAC: - # It's data, not an IAC - iac_cmd.pop() - # This prevents the 0xff from being - # interputed as yet another IAC - skip_to = iac_loc + 1 - log.debug("Received IAC IAC") - elif iac_cmd[1] == NOP: - pass - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x}".format(*iac_cmd)) - - # This must be a 3-byte TELNET command - else: - try: - iac_cmd.append(buf[iac_loc + 2]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 2]) - # We do ECHO, SGA, and BINARY. Period. - if (iac_cmd[1] == DO - and iac_cmd[2] not in [ECHO, SGA, BINARY]): - - self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) - - # Remove the entire TELNET command from the buffer - buf = buf.replace(iac_cmd, b'', 1) - - # Return the new copy of the buffer, minus telnet commands - return buf - - def _accept(self): - fd, addr = self.sock_fd.accept() - self.fd_dict[fd.fileno()] = fd - self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) - - log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) - - # This is a one-way negotiation. This is very basic so there - # shouldn't be any problems with any decent client. - fd.send(bytes([IAC, WILL, ECHO, - IAC, WILL, SGA, - IAC, WILL, BINARY, - IAC, DO, BINARY])) - - if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: - fd.send(b'\r\nToo many connections\r\n') - self._disconnect(fd.fileno()) - log.warn("Client disconnected because of too many connections. " - "(limit currently {})".format(args.telnet_limit)) - - def _disconnect(self, fileno): - fd = self.fd_dict.pop(fileno) - log.info("Telnet client disconnected") - fd.shutdown(socket.SHUT_RDWR) - fd.close() - - def __enter__(self): - # Open a socket and start listening - sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock_fd.bind((self.addr, self.port)) - except OSError: - raise TelnetServerError("Cannot bind to {}:{}" - .format(self.addr, self.port)) - - sock_fd.listen(socket.SOMAXCONN) - self.sock_fd = sock_fd - log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - for fileno in list(self.fd_dict.keys()): - self._disconnect(fileno) - self.sock_fd.close() - return False - - -class VPCS(Router): - - def __init__(self, ttyC, ttyS, stop_event): - self.ttyC = ttyC - self.ttyS = ttyS - self.stop_event = stop_event - - def read(self, bufsize): - try: - buf = self.fd.recv(bufsize) - except BlockingIOError: - return None - return buf - - def write(self, buf): - self.fd.send(buf) - - def _open(self): - self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - self.fd.setblocking(False) - - def _bind(self): - try: - os.unlink(self.ttyC) - except FileNotFoundError: - pass - except Exception as e: - raise NetioError("Couldn't unlink socket {}: {}" - .format(self.ttyC, e)) - - try: - self.fd.bind(self.ttyC) - except Exception as e: - raise NetioError("Couldn't create socket {}: {}" - .format(self.ttyC, e)) - - def _connect(self): - # Keep trying until we connect or die trying - while not self.stop_event.is_set(): - try: - self.fd.connect(self.ttyS) - except FileNotFoundError: - log.debug("Waiting to connect to {}".format(self.ttyS)) - time.sleep(RETRY_DELAY) - except Exception as e: - raise NetioError("Couldn't connect to socket {}: {}" - .format(self.ttyS, e)) - else: - break - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def fileno(self): - return self.fd.fileno() - - def __enter__(self): - self._open() - self._bind() - self._connect() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - os.unlink(self.ttyC) - self.fd.close() - return False - - -class VPCSConError(Exception): - pass - - -class LockError(VPCSConError): - pass - - -class NetioError(VPCSConError): - pass - - -class TTYError(VPCSConError): - pass - - -class TelnetServerError(VPCSConError): - pass - - -class ConfigError(VPCSConError): - pass - - -def mkdir_netio(netio_dir): - try: - os.mkdir(netio_dir) - except FileExistsError: - pass - except Exception as e: - raise NetioError("Couldn't create directory {}: {}" - .format(netio_dir, e)) - - -def send_recv_loop(console, router, esc_char, stop_event): - - epoll = select.epoll() - router.register(epoll) - console.register(epoll) - - router_fileno = router.fileno() - esc_quit = bytes(ESC_QUIT.upper(), 'ascii') - esc_state = False - - while not stop_event.is_set(): - event_list = epoll.poll(timeout=POLL_TIMEOUT) - - # When/if the poll times out we send an empty datagram. If VPCS - # has gone away then this will toss a ConnectionRefusedError - # exception. - if not event_list: - router.write(b'') - continue - - for fileno, event in event_list: - buf = bytearray() - - # VPCS --> tty(s) - if fileno == router_fileno: - while not stop_event.is_set(): - data = router.read(BUFFER_SIZE) - if not data: - break - buf.extend(data) - console.write(buf) - - # tty --> VPCS - else: - while not stop_event.is_set(): - data = console.read(fileno, BUFFER_SIZE) - if not data: - break - buf.extend(data) - - # If we just received the escape character then - # enter the escape state. - # - # If we are in the escape state then check for a - # quit command. Or if it's the escape character then - # send the escape character. Else, send the escape - # character we ate earlier and whatever character we - # just got. Exit escape state. - # - # If we're not in the escape state and this isn't an - # escape character then just send it to VPCS. - if esc_state: - if buf.upper() == esc_quit: - sys.exit(EXIT_SUCCESS) - elif buf == esc_char: - router.write(esc_char) - else: - router.write(esc_char) - router.write(buf) - esc_state = False - elif buf == esc_char: - esc_state = True - else: - router.write(buf) - - -def get_args(): - parser = argparse.ArgumentParser( - description='Connect to an VPCS console port.') - parser.add_argument('-d', '--debug', action='store_true', - help='display some debugging information') - parser.add_argument('-e', '--escape', - help='set escape character (default: %(default)s)', - default=ESC_CHAR, metavar='CHAR') - parser.add_argument('-t', '--telnet-server', - help='start telnet server listening on ADDR:PORT', - metavar='ADDR:PORT', default=False) - parser.add_argument('-l', '--telnet-limit', - help='maximum number of simultaneous ' - 'telnet connections (default: %(default)s)', - metavar='LIMIT', type=int, default=1) - parser.add_argument('appl_id', help='VPCS instance identifier') - return parser.parse_args() - - -def get_escape_character(escape): - - # Figure out the escape character to use. - # Can be any ASCII character or a spelled out control - # character, like "^e". The string "none" disables it. - if escape.lower() == 'none': - esc_char = b'' - elif len(escape) == 2 and escape[0] == '^': - c = ord(escape[1].upper()) - 0x40 - if not 0 <= c <= 0x1f: # control code range - raise ConfigError("Invalid control code") - esc_char = bytes([c]) - elif len(escape) == 1: - try: - esc_char = bytes(escape, 'ascii') - except ValueError as e: - raise ConfigError("Invalid escape character") from e - else: - raise ConfigError("Invalid length for escape character") - - return esc_char - - -def start_VPCScon(cmdline_args, stop_event): - - global args - args = cmdline_args - - if args.debug: - logging.basicConfig(level=logging.DEBUG) - else: - # default logging level - logging.basicConfig(level=logging.INFO) - - # Create paths for the Unix domain sockets - netio = '/tmp/netio{}'.format(os.getuid()) - ttyC = '{}/ttyC{}'.format(netio, args.appl_id) - ttyS = '{}/ttyS{}'.format(netio, args.appl_id) - - try: - mkdir_netio(netio) - with FileLock(ttyC): - esc_char = get_escape_character(args.escape) - - if args.telnet_server: - addr, _, port = args.telnet_server.partition(':') - nport = 0 - try: - nport = int(port) - except ValueError: - pass - if (addr == '' or nport == 0): - raise ConfigError('format for --telnet-server must be ' - 'ADDR:PORT (like 127.0.0.1:20000)') - - while not stop_event.is_set(): - try: - if args.telnet_server: - with TelnetServer(addr, nport, stop_event) as console: - with VPCS(ttyC, ttyS, stop_event) as router: - send_recv_loop(console, router, b'', stop_event) - else: - with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console: - send_recv_loop(console, router, esc_char, stop_event) - except ConnectionRefusedError: - pass - except KeyboardInterrupt: - sys.exit(EXIT_ABORT) - finally: - # Put us at the beginning of a line - if not args.telnet_server: - print() - - except VPCSConError as e: - if args.debug: - traceback.print_exc(file=sys.stderr) - else: - print(e, file=sys.stderr) - sys.exit(EXIT_FAILURE) - - log.info("exiting...") - - -def main(): - - import threading - stop_event = threading.Event() - args = get_args() - start_VPCScon(args, stop_event) - -if __name__ == '__main__': - main() diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py deleted file mode 100644 index 9d72f2e6..00000000 --- a/tests/vpcs/test_vpcs_device.py +++ /dev/null @@ -1,29 +0,0 @@ -from gns3server.modules.vpcs import VPCSDevice -import os -import pytest - - -@pytest.fixture(scope="session") -def vpcs(request): - - cwd = os.path.dirname(os.path.abspath(__file__)) - vpcs_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") - vpcs_device = VPCSDevice(vpcs_path, "/tmp") - vpcs_device.start() - request.addfinalizer(vpcs_device.delete) - return vpcs_device - - -def test_vpcs_is_started(vpcs): - - print(vpcs.command()) - assert vpcs.id == 1 # we should have only one VPCS running! - assert vpcs.is_running() - - -def test_vpcs_restart(vpcs): - - vpcs.stop() - assert not vpcs.is_running() - vpcs.start() - assert vpcs.is_running() From 46c653998e7123524d538cb616d472789953325c Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 09:06:25 -0600 Subject: [PATCH 03/15] First draft of VPCS module --- .gitignore~ | 36 ++ gns3server/modules/__init__.py~ | 27 + gns3server/modules/iou/schemas.py~ | 364 ++++++++++++ gns3server/modules/vpcs/__init__.py~ | 693 +++++++++++++++++++++++ gns3server/modules/vpcs/nios/nio_tap.py~ | 46 ++ gns3server/modules/vpcs/nios/nio_udp.py~ | 72 +++ gns3server/modules/vpcs/schemas.py~ | 306 ++++++++++ gns3server/modules/vpcs/vpcs_device.py~ | 471 +++++++++++++++ gns3server/modules/vpcs/vpcs_error.py~ | 39 ++ gns3server/modules/vpcs/vpcscon.py~ | 642 +++++++++++++++++++++ tests/vpcs/test_vpcs_device.py~ | 29 + 11 files changed, 2725 insertions(+) create mode 100644 .gitignore~ create mode 100644 gns3server/modules/__init__.py~ create mode 100644 gns3server/modules/iou/schemas.py~ create mode 100644 gns3server/modules/vpcs/__init__.py~ create mode 100644 gns3server/modules/vpcs/nios/nio_tap.py~ create mode 100644 gns3server/modules/vpcs/nios/nio_udp.py~ create mode 100644 gns3server/modules/vpcs/schemas.py~ create mode 100644 gns3server/modules/vpcs/vpcs_device.py~ create mode 100644 gns3server/modules/vpcs/vpcs_error.py~ create mode 100644 gns3server/modules/vpcs/vpcscon.py~ create mode 100644 tests/vpcs/test_vpcs_device.py~ diff --git a/.gitignore~ b/.gitignore~ new file mode 100644 index 00000000..49bac9bd --- /dev/null +++ b/.gitignore~ @@ -0,0 +1,36 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject +.settings diff --git a/gns3server/modules/__init__.py~ b/gns3server/modules/__init__.py~ new file mode 100644 index 00000000..59304d19 --- /dev/null +++ b/gns3server/modules/__init__.py~ @@ -0,0 +1,27 @@ +# -*- 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 . + +import sys +from .base import IModule +from .dynamips import Dynamips + +MODULES = [Dynamips] + +if sys.platform.startswith("linux"): + # IOU runs only on Linux + from .iou import IOU + MODULES.append(IOU) diff --git a/gns3server/modules/iou/schemas.py~ b/gns3server/modules/iou/schemas.py~ new file mode 100644 index 00000000..62ac0ec4 --- /dev/null +++ b/gns3server/modules/iou/schemas.py~ @@ -0,0 +1,364 @@ +# -*- 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 . + + +IOU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the IOU executable", + "type": "string", + "minLength": 1, + } + }, + "required": ["path"] +} + +IOU_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +IOU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + "name": { + "description": "IOU device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the IOU executable", + "type": "string", + "minLength": 1, + }, + "startup_config": { + "description": "path to the IOU startup configuration file", + "type": "string", + "minLength": 1, + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "nvram": { + "description": "amount of NVRAM in KB", + "type": "integer" + }, + "ethernet_adapters": { + "description": "number of Ethernet adapters", + "type": "integer", + "minimum": 0, + "maximum": 16, + }, + "serial_adapters": { + "description": "number of serial adapters", + "type": "integer", + "minimum": 0, + "maximum": 16, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "startup_config_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + }, + "required": ["id"] +} + +IOU_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +IOU_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +IOU_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +IOU_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the IOU instance", + "type": "integer" + }, + }, + "required": ["id", "port_id"] +} + +IOU_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for an IOU 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": "IOU device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the IOU instance", + "type": "integer" + }, + "slot": { + "description": "Slot number", + "type": "integer", + "minimum": 0, + "maximum": 15 + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + + "slot": { + "description": "Slot number", + "type": "integer", + "minimum": 0, + "maximum": 15 + }, + "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"}, + ] + }, + }, + "required": ["id", "port_id", "slot", "port", "nio"] +} + + +IOU_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for an IOU instance", + "type": "object", + "properties": { + "id": { + "description": "IOU device instance ID", + "type": "integer" + }, + "slot": { + "description": "Slot number", + "type": "integer", + "minimum": 0, + "maximum": 15 + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + }, + "required": ["id", "slot", "port"] +} diff --git a/gns3server/modules/vpcs/__init__.py~ b/gns3server/modules/vpcs/__init__.py~ new file mode 100644 index 00000000..517d9a54 --- /dev/null +++ b/gns3server/modules/vpcs/__init__.py~ @@ -0,0 +1,693 @@ +# -*- 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 . + +""" +VPCS server module. +""" + +import os +import sys +import base64 +import tempfile +import fcntl +import struct +import socket +import shutil + +from gns3server.modules import IModule +from gns3server.config import Config +import gns3server.jsonrpc as jsonrpc +from .vpcs_device import VPCSDevice +from .vpcs_error import VPCSError +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP +from ..attic import find_unused_port + +from .schemas import VPCS_CREATE_SCHEMA +from .schemas import VPCS_DELETE_SCHEMA +from .schemas import VPCS_UPDATE_SCHEMA +from .schemas import VPCS_START_SCHEMA +from .schemas import VPCS_STOP_SCHEMA +from .schemas import VPCS_RELOAD_SCHEMA +from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA +from .schemas import VPCS_ADD_NIO_SCHEMA +from .schemas import VPCS_DELETE_NIO_SCHEMA + +import logging +log = logging.getLogger(__name__) + + +class VPCS(IModule): + """ + VPCS 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 VPCS location + config = Config.instance() + VPCS_config = config.get_section_config(name.upper()) + self._VPCS = VPCS_config.get("VPCS") + if not self._VPCS or not os.path.isfile(self._VPCS): + VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") + if os.path.isfile(VPCS_in_cwd): + self._VPCS = VPCS_in_cwd + else: + # look for VPCS if none is defined or accessible + for path in os.environ["PATH"].split(":"): + try: + if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): + self._VPCS = os.path.join(path, "VPCS") + break + except OSError: + continue + + if not self._VPCS: + log.warning("VPCS binary couldn't be found!") + elif not os.access(self._VPCS, os.X_OK): + log.warning("VPCS is not executable") + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._VPCS_instances = {} + self._console_start_port_range = 4001 + self._console_end_port_range = 4512 + self._allocated_console_ports = [] + self._current_console_port = self._console_start_port_range + self._udp_start_port_range = 30001 + self._udp_end_port_range = 40001 + self._current_udp_port = self._udp_start_port_range + self._host = kwargs["host"] + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + self._VPCSrc = "" + + # check every 5 seconds + self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) + self._VPCS_callback.start() + + def stop(self, signum=None): + """ + Properly stops the module. + + :param signum: signal number (if called by the signal handler) + """ + + self._VPCS_callback.stop() + # delete all VPCS instances + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.delete() + + IModule.stop(self, signum) # this will stop the I/O loop + + def _check_VPCS_is_alive(self): + """ + Periodic callback to check if VPCS and VPCS are alive + for each VPCS instance. + + Sends a notification to the client if not. + """ + + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + if VPCS_instance.started and (not VPCS_instance.is_running() or not VPCS_instance.is_VPCS_running()): + notification = {"module": self.name, + "id": VPCS_id, + "name": VPCS_instance.name} + if not VPCS_instance.is_running(): + stdout = VPCS_instance.read_VPCS_stdout() + notification["message"] = "VPCS has stopped running" + notification["details"] = stdout + self.send_notification("{}.VPCS_stopped".format(self.name), notification) + elif not VPCS_instance.is_VPCS_running(): + stdout = VPCS_instance.read_VPCS_stdout() + notification["message"] = "VPCS has stopped running" + notification["details"] = stdout + self.send_notification("{}.VPCS_stopped".format(self.name), notification) + VPCS_instance.stop() + + def get_VPCS_instance(self, VPCS_id): + """ + Returns an VPCS device instance. + + :param VPCS_id: VPCS device identifier + + :returns: VPCSDevice instance + """ + + if VPCS_id not in self._VPCS_instances: + log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1) + self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id)) + return None + return self._VPCS_instances[VPCS_id] + + @IModule.route("VPCS.reset") + def reset(self, request): + """ + Resets the module. + + :param request: JSON request + """ + + # delete all VPCS instances + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.delete() + + # resets the instance IDs + VPCSDevice.reset() + + self._VPCS_instances.clear() + self._remote_server = False + self._current_console_port = self._console_start_port_range + self._current_udp_port = self._udp_start_port_range + + if self._VPCSrc and os.path.isfile(self._VPCSrc): + try: + log.info("deleting VPCSrc file {}".format(self._VPCSrc)) + os.remove(self._VPCSrc) + except OSError as e: + log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e)) + + log.info("VPCS module has been reset") + + @IModule.route("VPCS.settings") + def settings(self, request): + """ + Set or update settings. + + Mandatory request parameters: + - VPCSrc (base64 encoded VPCSrc file) + + 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 == None: + self.send_param_error() + return + + if "VPCS" in request and request["VPCS"]: + self._VPCS = request["VPCS"] + log.info("VPCS path set to {}".format(self._VPCS)) + + 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"] + ".gns3") + 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 VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.working_dir = self._working_dir + + 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)) + + def test_result(self, message, result="error"): + """ + """ + + return {"result": result, "message": message} + + @IModule.route("VPCS.test_settings") + def test_settings(self, request): + """ + """ + + response = [] + + self.send_response(response) + + @IModule.route("VPCS.create") + def VPCS_create(self, request): + """ + Creates a new VPCS instance. + + Mandatory request parameters: + - path (path to the VPCS executable) + + Optional request parameters: + - name (VPCS name) + + Response parameters: + - id (VPCS instance identifier) + - name (VPCS name) + - default settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_CREATE_SCHEMA): + return + + name = None + if "name" in request: + name = request["name"] + VPCS_path = request["path"] + + try: + try: + os.makedirs(self._working_dir) + except FileExistsError: + pass + except OSError as e: + raise VPCSError("Could not create working directory {}".format(e)) + + VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name) + # find a console port + if self._current_console_port > self._console_end_port_range: + self._current_console_port = self._console_start_port_range + try: + VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) + except Exception as e: + raise VPCSError(e) + self._current_console_port += 1 + except VPCSError as e: + self.send_custom_error(str(e)) + return + + response = {"name": VPCS_instance.name, + "id": VPCS_instance.id} + + defaults = VPCS_instance.defaults() + response.update(defaults) + self._VPCS_instances[VPCS_instance.id] = VPCS_instance + self.send_response(response) + + @IModule.route("VPCS.delete") + def VPCS_delete(self, request): + """ + Deletes an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameter: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_DELETE_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + VPCS_instance.delete() + del self._VPCS_instances[request["id"]] + except VPCSError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + @IModule.route("VPCS.update") + def VPCS_update(self, request): + """ + Updates an VPCS instance + + Mandatory request parameters: + - id (VPCS instance identifier) + + Optional request parameters: + - any setting to update + - startup_config_base64 (startup-config base64 encoded) + + Response parameters: + - updated settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_UPDATE_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + response = {} + try: + # a new startup-config has been pushed + if "startup_config_base64" in request: + config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + config = "!\n" + config.replace("\r", "") + config = config.replace('%h', VPCS_instance.name) + config_path = os.path.join(VPCS_instance.working_dir, "startup-config") + try: + with open(config_path, "w") as f: + log.info("saving startup-config to {}".format(config_path)) + f.write(config) + except OSError as e: + raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) + # update the request with the new local startup-config path + request["startup_config"] = os.path.basename(config_path) + + except VPCSError as e: + self.send_custom_error(str(e)) + return + + # update the VPCS settings + for name, value in request.items(): + if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value: + try: + setattr(VPCS_instance, name, value) + response[name] = value + except VPCSError as e: + self.send_custom_error(str(e)) + return + + self.send_response(response) + + @IModule.route("VPCS.start") + def vm_start(self, request): + """ + Starts an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_START_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + log.debug("starting VPCS with command: {}".format(VPCS_instance.command())) + VPCS_instance.VPCS = self._VPCS + VPCS_instance.VPCSrc = self._VPCSrc + VPCS_instance.start() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.stop") + def vm_stop(self, request): + """ + Stops an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_STOP_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + VPCS_instance.stop() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.reload") + def vm_reload(self, request): + """ + Reloads an VPCS instance. + + Mandatory request parameters: + - id (VPCS identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_RELOAD_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + if VPCS_instance.is_running(): + VPCS_instance.stop() + VPCS_instance.start() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.allocate_udp_port") + def allocate_udp_port(self, request): + """ + Allocates a UDP port in order to create an UDP NIO. + + Mandatory request parameters: + - id (VPCS 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, VPCS_ALLOCATE_UDP_PORT_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + + # find a UDP port + if self._current_udp_port >= self._udp_end_port_range: + self._current_udp_port = self._udp_start_port_range + try: + port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") + except Exception as e: + raise VPCSError(e) + self._current_udp_port += 1 + + log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name, + VPCS_instance.id, + port, + self._host)) + response = {"lport": port} + + except VPCSError as e: + self.send_custom_error(str(e)) + return + + response["port_id"] = request["port_id"] + self.send_response(response) + + def _check_for_privileged_access(self, device): + """ + Check if VPCS can access Ethernet and TAP devices. + + :param device: device name + """ + + # we are root, so VPCS should have privileged access too + if os.geteuid() == 0: + return + + # test if VPCS has the CAP_NET_RAW capability + if "security.capability" in os.listxattr(self._VPCS): + try: + caps = os.getxattr(self._VPCS, "security.capability") + # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set + if struct.unpack(". + +""" +Interface for TAP NIOs (UNIX based OSes only). +""" + + +class NIO_TAP(object): + """ + IOU TAP NIO. + + :param tap_device: TAP device name (e.g. tap0) + """ + + def __init__(self, tap_device): + + self._tap_device = tap_device + + @property + def tap_device(self): + """ + Returns the TAP device used by this NIO. + + :returns: the TAP device name + """ + + return self._tap_device + + def __str__(self): + + return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_udp.py~ b/gns3server/modules/vpcs/nios/nio_udp.py~ new file mode 100644 index 00000000..3142d70e --- /dev/null +++ b/gns3server/modules/vpcs/nios/nio_udp.py~ @@ -0,0 +1,72 @@ +# -*- 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. +""" + + +class NIO_UDP(object): + """ + IOU 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): + + 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/vpcs/schemas.py~ b/gns3server/modules/vpcs/schemas.py~ new file mode 100644 index 00000000..24fcce15 --- /dev/null +++ b/gns3server/modules/vpcs/schemas.py~ @@ -0,0 +1,306 @@ +# -*- 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 . + + +VPCS_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the VPCS executable", + "type": "string", + "minLength": 1, + } + }, + "required": ["path"] +} + +VPCS_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the VPCS executable", + "type": "string", + "minLength": 1, + }, + "startup_config": { + "description": "path to the VPCS startup configuration file", + "type": "string", + "minLength": 1, + }, + "startup_config_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + }, + "required": ["id"] +} + +VPCS_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + }, + "required": ["id", "port_id"] +} + +VPCS_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for an VPCS 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": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + "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"}, + ] + }, + }, + "required": ["id", "port_id", "nio"] +} + +VPCS_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} diff --git a/gns3server/modules/vpcs/vpcs_device.py~ b/gns3server/modules/vpcs/vpcs_device.py~ new file mode 100644 index 00000000..76756c62 --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_device.py~ @@ -0,0 +1,471 @@ +# -*- 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 . + +""" +VPCS device management (creates command line, processes, files etc.) in +order to run an VPCS instance. +""" + +import os +import re +import signal +import subprocess +import argparse +import threading +import configparser +from .vpcscon import start_vpcscon +from .vpcs_error import VPCSError +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP + +import logging +log = logging.getLogger(__name__) + + +class VPCSDevice(object): + """ + VPCS device implementation. + + :param path: path to VPCS executable + :param working_dir: path to a working directory + :param host: host/address to bind for console and UDP connections + :param name: name of this VPCS device + """ + + _instances = [] + + def __init__(self, path, working_dir, host="127.0.0.1", name=None): + + # find an instance identifier (0 <= id < 255) + # This 255 limit is due to a restriction on the number of possible + # mac addresses given in VPCS using the -m option + self._id = 0 + for identifier in range(0, 255): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break + + if self._id == 0: + raise VPCSError("Maximum number of VPCS instances reached") + + if name: + self._name = name + else: + self._name = "VPCS{}".format(self._id) + self._path = path + self._console = None + self._working_dir = None + self._command = [] + self._process = None + self._vpcs_stdout_file = "" + self._vpcscon_thead = None + self._vpcscon_thread_stop_event = None + self._host = host + self._started = False + + # VPCS settings + self._script_file = "" + + # update the working directory + self.working_dir = working_dir + + log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for VPCS. + + :returns: default values (dictionary) + """ + + vpcs_defaults = {"name": self._name, + "path": self._path, + "script_file": self._script_file, + "console": self._console} + + return vpcs_defaults + + @property + def id(self): + """ + Returns the unique ID for this VPCS device. + + :returns: id (integer) + """ + + return(self._id) + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + + @property + def name(self): + """ + Returns the name of this VPCS device. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VPCS device. + + :param new_name: name + """ + + self._name = new_name + log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + @property + def path(self): + """ + Returns the path to the VPCS executable. + + :returns: path to VPCS + """ + + return(self._path) + + @path.setter + def path(self, path): + """ + Sets the path to the VPCS executable. + + :param path: path to VPCS + """ + + self._path = path + log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, + id=self._id, + path=path)) + + @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 for VPCS. + + :param working_dir: path to the working directory + """ + + # create our own working directory + working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id)) + try: + os.makedirs(working_dir) + except FileExistsError: + pass + except OSError as e: + raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("VPCS {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) + """ + + self._console = console + log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + def command(self): + """ + Returns the VPCS command line. + + :returns: VPCS command line (string) + """ + + return " ".join(self._build_command()) + + def delete(self): + """ + Deletes this VPCS device. + """ + + self.stop() + self._instances.remove(self._id) + log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + @property + def started(self): + """ + Returns either this VPCS device has been started or not. + + :returns: boolean + """ + + return self._started + + def _start_vpcscon(self): + """ + Starts vpcscon thread (for console connections). + """ + + if not self._vpcscon_thead: + telnet_server = "{}:{}".format(self._host, self._console) + log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) + args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) + self._vpcscon_thread_stop_event = threading.Event() + self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) + self._vpcscon_thead.start() ", ".join(missing_libs))) + + def start(self): + """ + Starts the VPCS process. + """ + + if not self.is_running(): + + if not os.path.isfile(self._path): + raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) + + if not os.access(self._path, os.X_OK): + raise VPCSError("VPCS image '{}' is not executable".format(self._path)) + + self._command = self._build_command() + try: + log.info("starting VPCS: {}".format(self._command)) + self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") + log.info("logging to {}".format(self._vpcs_stdout_file)) + with open(self._vpcs_stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except FileNotFoundError as e: + raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e)) + except OSError as e: + vpcs_stdout = self.read_vpcs_stdout() + log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + + # start console support + self._start_vpcscon() + + def stop(self): + """ + Stops the VPCS process. + """ + + # stop the VPCS process + if self.is_running(): + log.info("stopping VPCS 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() == None: + log.warn("VPCS instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + self._started = False + + # stop console support + if self._vpcscon_thead: + self._vpcscon_thread_stop_event.set() + if self._vpcscon_thead.is_alive(): + self._vpcscon_thead.join(timeout=0.10) + self._vpcscon_thead = None + + + def read_vpcs_stdout(self): + """ + Reads the standard output of the VPCS process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._vpcs_stdout_file: + try: + with open(self._vpcs_stdout_file) as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the VPCS process is running + + :returns: True or False + """ + + if self._process and self._process.poll() == None: + return True + return False + + + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + adapter.add_nio(port_id, nio) + log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + + def _build_command(self): + """ + Command to start the VPCS process. + (to be passed to subprocess.Popen()) + + VPCS command line: + usage: vpcs [options] [scriptfile] + Option: + -h print this help then exit + -v print version information then exit + + -p port run as a daemon listening on the tcp 'port' + -m num start byte of ether address, default from 0 + -r file load and execute script file + compatible with older versions, DEPRECATED. + + -e tap mode, using /dev/tapx (linux only) + -u udp mode, default + + udp mode options: + -s port local udp base port, default from 20000 + -c port remote udp base port (dynamips udp port), default from 30000 + -t ip remote host IP, default 127.0.0.1 + + hypervisor mode option: + -H port run as the hypervisor listening on the tcp 'port' + + If no 'scriptfile' specified, vpcs will read and execute the file named + 'startup.vpc' if it exsits in the current directory. + + """ + + command = [self._path] + command.extend(["-p", str(self._tcpport)]) + command.extend(["-s", str(self._lport)]) + command.extend(["-c", str(self._rport)]) + command.extend(["-t", str(self._rhost)]) + command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset + if self._script_file: + command.extend([self._script_file]) + return command + + @property + def script_file(self): + """ + Returns the startup-config for this VPCS instance. + + :returns: path to startup-config file + """ + + return self._script_file + + @script_file.setter + def script_file(self, script_file): + """ + Sets the startup-config for this VPCS instance. + + :param script_file: path to startup-config file + """ + + self._script_file = script_file + log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, + id=self._id, + config=self._script_file)) + + diff --git a/gns3server/modules/vpcs/vpcs_error.py~ b/gns3server/modules/vpcs/vpcs_error.py~ new file mode 100644 index 00000000..33ed081c --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_error.py~ @@ -0,0 +1,39 @@ +# -*- 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 . + +""" +Custom exceptions for VPCS module. +""" + + +class IOUError(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/vpcs/vpcscon.py~ b/gns3server/modules/vpcs/vpcscon.py~ new file mode 100644 index 00000000..138b61e7 --- /dev/null +++ b/gns3server/modules/vpcs/vpcscon.py~ @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013, 2014 James E. Carpenter +# +# 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 socket +import sys +import os +import select +import fcntl +import struct +import termios +import tty +import time +import argparse +import traceback + + +import logging +log = logging.getLogger(__name__) + + +# Escape characters +ESC_CHAR = '^^' # can be overriden from command line +ESC_QUIT = 'q' + +# IOU seems to only send *1* byte at a time. If +# they ever fix that we'll be ready for it. +BUFFER_SIZE = 1024 + +# How long to wait before retrying a connection (seconds) +RETRY_DELAY = 3 + +# How often to test an idle connection (seconds) +POLL_TIMEOUT = 3 + + +EXIT_SUCCESS = 0 +EXIT_FAILURE = 1 +EXIT_ABORT = 2 + +# Mostly from: +# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py +#--[ Telnet Commands ]--------------------------------------------------------- +SE = 240 # End of subnegotiation parameters +NOP = 241 # No operation +DATMK = 242 # Data stream portion of a sync. +BREAK = 243 # NVT Character BRK +IP = 244 # Interrupt Process +AO = 245 # Abort Output +AYT = 246 # Are you there +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # The Go Ahead Signal +SB = 250 # Sub-option to follow +WILL = 251 # Will; request or confirm option begin +WONT = 252 # Wont; deny option request +DO = 253 # Do = Request or confirm remote option +DONT = 254 # Don't = Demand or confirm option halt +IAC = 255 # Interpret as Command +SEND = 1 # Sub-process negotiation SEND command +IS = 0 # Sub-process negotiation IS command +#--[ Telnet Options ]---------------------------------------------------------- +BINARY = 0 # Transmit Binary +ECHO = 1 # Echo characters back to sender +RECON = 2 # Reconnection +SGA = 3 # Suppress Go-Ahead +TMARK = 6 # Timing Mark +TTYPE = 24 # Terminal Type +NAWS = 31 # Negotiate About Window Size +LINEMO = 34 # Line Mode + + +class FileLock: + + # struct flock { /* from fcntl(2) */ + # ... + # short l_type; /* Type of lock: F_RDLCK, + # F_WRLCK, F_UNLCK */ + # short l_whence; /* How to interpret l_start: + # SEEK_SET, SEEK_CUR, SEEK_END */ + # off_t l_start; /* Starting offset for lock */ + # off_t l_len; /* Number of bytes to lock */ + # pid_t l_pid; /* PID of process blocking our lock + # (F_GETLK only) */ + # ... + # }; + _flock = struct.Struct('hhqql') + + def __init__(self, fname=None): + self.fd = None + self.fname = fname + + def get_lock(self): + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, + 0, 0, os.getpid()) + flk = self._flock.unpack( + fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) + + # If it's not locked (or is locked by us) then return None, + # otherwise return the PID of the owner. + if flk[0] == fcntl.F_UNLCK: + return None + return flk[4] + + def lock(self): + try: + self.fd = open('{}.lck'.format(self.fname), 'a') + except Exception as e: + raise LockError("Couldn't get lock on {}: {}" + .format(self.fname, e)) + + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) + try: + fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) + except BlockingIOError: + raise LockError("Already connected. PID {} has lock on {}" + .format(self.get_lock(), self.fname)) + + # If we got here then we must have the lock. Store the PID. + self.fd.truncate(0) + self.fd.write('{}\n'.format(os.getpid())) + self.fd.flush() + + def unlock(self): + if self.fd: + # Deleting first prevents a race condition + os.unlink(self.fd.name) + self.fd.close() + + def __enter__(self): + self.lock() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.unlock() + return False + + +class Console: + def fileno(self): + raise NotImplementedError("Only routers have fileno()") + + +class Router: + pass + + +class TTY(Console): + + def read(self, fileno, bufsize): + return self.fd.read(bufsize) + + def write(self, buf): + return self.fd.write(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def __enter__(self): + try: + self.fd = open('/dev/tty', 'r+b', buffering=0) + except OSError as e: + raise TTYError("Couldn't open controlling TTY: {}".format(e)) + + # Save original flags + self.termios = termios.tcgetattr(self.fd) + self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) + + # Update flags + tty.setraw(self.fd, termios.TCSANOW) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + # Restore flags to original settings + termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) + + self.fd.close() + + return False + + +class TelnetServer(Console): + + def __init__(self, addr, port, stop_event): + self.addr = addr + self.port = port + self.fd_dict = {} + self.stop_event = stop_event + + def read(self, fileno, bufsize): + # Someone wants to connect? + if fileno == self.sock_fd.fileno(): + self._accept() + return None + + self._cur_fileno = fileno + + # Read a maximum of _bufsize_ bytes without blocking. When it + # would want to block it means there's no more data. An empty + # buffer normally means that we've been disconnected. + try: + buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) + except BlockingIOError: + return None + if not buf: + self._disconnect(fileno) + + # Process and remove any telnet commands from the buffer + if IAC in buf: + buf = self._IAC_parser(buf) + + return buf + + def write(self, buf): + for fd in self.fd_dict.values(): + fd.send(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.sock_fd, select.EPOLLIN) + + def _read_block(self, bufsize): + buf = self._read_cur(bufsize, socket.MSG_WAITALL) + # If we don't get everything we were looking for then the + # client probably disconnected. + if len(buf) < bufsize: + self._disconnect(self._cur_fileno) + return buf + + def _read_cur(self, bufsize, flags): + return self.fd_dict[self._cur_fileno].recv(bufsize, flags) + + def _write_cur(self, buf): + return self.fd_dict[self._cur_fileno].send(buf) + + def _IAC_parser(self, buf): + skip_to = 0 + while not self.stop_event.is_set(): + # Locate an IAC to process + iac_loc = buf.find(IAC, skip_to) + if iac_loc < 0: + break + + # Get the TELNET command + iac_cmd = bytearray([IAC]) + try: + iac_cmd.append(buf[iac_loc + 1]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 1]) + + # Is this just a 2-byte TELNET command? + if iac_cmd[1] not in [WILL, WONT, DO, DONT]: + if iac_cmd[1] == AYT: + log.debug("Telnet server received Are-You-There (AYT)") + self._write_cur( + b'\r\nYour Are-You-There received. I am here.\r\n' + ) + elif iac_cmd[1] == IAC: + # It's data, not an IAC + iac_cmd.pop() + # This prevents the 0xff from being + # interputed as yet another IAC + skip_to = iac_loc + 1 + log.debug("Received IAC IAC") + elif iac_cmd[1] == NOP: + pass + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x}".format(*iac_cmd)) + + # This must be a 3-byte TELNET command + else: + try: + iac_cmd.append(buf[iac_loc + 2]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 2]) + # We do ECHO, SGA, and BINARY. Period. + if (iac_cmd[1] == DO + and iac_cmd[2] not in [ECHO, SGA, BINARY]): + + self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + + # Remove the entire TELNET command from the buffer + buf = buf.replace(iac_cmd, b'', 1) + + # Return the new copy of the buffer, minus telnet commands + return buf + + def _accept(self): + fd, addr = self.sock_fd.accept() + self.fd_dict[fd.fileno()] = fd + self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) + + log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) + + # This is a one-way negotiation. This is very basic so there + # shouldn't be any problems with any decent client. + fd.send(bytes([IAC, WILL, ECHO, + IAC, WILL, SGA, + IAC, WILL, BINARY, + IAC, DO, BINARY])) + + if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: + fd.send(b'\r\nToo many connections\r\n') + self._disconnect(fd.fileno()) + log.warn("Client disconnected because of too many connections. " + "(limit currently {})".format(args.telnet_limit)) + + def _disconnect(self, fileno): + fd = self.fd_dict.pop(fileno) + log.info("Telnet client disconnected") + fd.shutdown(socket.SHUT_RDWR) + fd.close() + + def __enter__(self): + # Open a socket and start listening + sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock_fd.bind((self.addr, self.port)) + except OSError: + raise TelnetServerError("Cannot bind to {}:{}" + .format(self.addr, self.port)) + + sock_fd.listen(socket.SOMAXCONN) + self.sock_fd = sock_fd + log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for fileno in list(self.fd_dict.keys()): + self._disconnect(fileno) + self.sock_fd.close() + return False + + +class IOU(Router): + + def __init__(self, ttyC, ttyS, stop_event): + self.ttyC = ttyC + self.ttyS = ttyS + self.stop_event = stop_event + + def read(self, bufsize): + try: + buf = self.fd.recv(bufsize) + except BlockingIOError: + return None + return buf + + def write(self, buf): + self.fd.send(buf) + + def _open(self): + self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.fd.setblocking(False) + + def _bind(self): + try: + os.unlink(self.ttyC) + except FileNotFoundError: + pass + except Exception as e: + raise NetioError("Couldn't unlink socket {}: {}" + .format(self.ttyC, e)) + + try: + self.fd.bind(self.ttyC) + except Exception as e: + raise NetioError("Couldn't create socket {}: {}" + .format(self.ttyC, e)) + + def _connect(self): + # Keep trying until we connect or die trying + while not self.stop_event.is_set(): + try: + self.fd.connect(self.ttyS) + except FileNotFoundError: + log.debug("Waiting to connect to {}".format(self.ttyS)) + time.sleep(RETRY_DELAY) + except Exception as e: + raise NetioError("Couldn't connect to socket {}: {}" + .format(self.ttyS, e)) + else: + break + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def fileno(self): + return self.fd.fileno() + + def __enter__(self): + self._open() + self._bind() + self._connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + os.unlink(self.ttyC) + self.fd.close() + return False + + +class IOUConError(Exception): + pass + + +class LockError(IOUConError): + pass + + +class NetioError(IOUConError): + pass + + +class TTYError(IOUConError): + pass + + +class TelnetServerError(IOUConError): + pass + + +class ConfigError(IOUConError): + pass + + +def mkdir_netio(netio_dir): + try: + os.mkdir(netio_dir) + except FileExistsError: + pass + except Exception as e: + raise NetioError("Couldn't create directory {}: {}" + .format(netio_dir, e)) + + +def send_recv_loop(console, router, esc_char, stop_event): + + epoll = select.epoll() + router.register(epoll) + console.register(epoll) + + router_fileno = router.fileno() + esc_quit = bytes(ESC_QUIT.upper(), 'ascii') + esc_state = False + + while not stop_event.is_set(): + event_list = epoll.poll(timeout=POLL_TIMEOUT) + + # When/if the poll times out we send an empty datagram. If IOU + # has gone away then this will toss a ConnectionRefusedError + # exception. + if not event_list: + router.write(b'') + continue + + for fileno, event in event_list: + buf = bytearray() + + # IOU --> tty(s) + if fileno == router_fileno: + while not stop_event.is_set(): + data = router.read(BUFFER_SIZE) + if not data: + break + buf.extend(data) + console.write(buf) + + # tty --> IOU + else: + while not stop_event.is_set(): + data = console.read(fileno, BUFFER_SIZE) + if not data: + break + buf.extend(data) + + # If we just received the escape character then + # enter the escape state. + # + # If we are in the escape state then check for a + # quit command. Or if it's the escape character then + # send the escape character. Else, send the escape + # character we ate earlier and whatever character we + # just got. Exit escape state. + # + # If we're not in the escape state and this isn't an + # escape character then just send it to IOU. + if esc_state: + if buf.upper() == esc_quit: + sys.exit(EXIT_SUCCESS) + elif buf == esc_char: + router.write(esc_char) + else: + router.write(esc_char) + router.write(buf) + esc_state = False + elif buf == esc_char: + esc_state = True + else: + router.write(buf) + + +def get_args(): + parser = argparse.ArgumentParser( + description='Connect to an IOU console port.') + parser.add_argument('-d', '--debug', action='store_true', + help='display some debugging information') + parser.add_argument('-e', '--escape', + help='set escape character (default: %(default)s)', + default=ESC_CHAR, metavar='CHAR') + parser.add_argument('-t', '--telnet-server', + help='start telnet server listening on ADDR:PORT', + metavar='ADDR:PORT', default=False) + parser.add_argument('-l', '--telnet-limit', + help='maximum number of simultaneous ' + 'telnet connections (default: %(default)s)', + metavar='LIMIT', type=int, default=1) + parser.add_argument('appl_id', help='IOU instance identifier') + return parser.parse_args() + + +def get_escape_character(escape): + + # Figure out the escape character to use. + # Can be any ASCII character or a spelled out control + # character, like "^e". The string "none" disables it. + if escape.lower() == 'none': + esc_char = b'' + elif len(escape) == 2 and escape[0] == '^': + c = ord(escape[1].upper()) - 0x40 + if not 0 <= c <= 0x1f: # control code range + raise ConfigError("Invalid control code") + esc_char = bytes([c]) + elif len(escape) == 1: + try: + esc_char = bytes(escape, 'ascii') + except ValueError as e: + raise ConfigError("Invalid escape character") from e + else: + raise ConfigError("Invalid length for escape character") + + return esc_char + + +def start_ioucon(cmdline_args, stop_event): + + global args + args = cmdline_args + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + # default logging level + logging.basicConfig(level=logging.INFO) + + # Create paths for the Unix domain sockets + netio = '/tmp/netio{}'.format(os.getuid()) + ttyC = '{}/ttyC{}'.format(netio, args.appl_id) + ttyS = '{}/ttyS{}'.format(netio, args.appl_id) + + try: + mkdir_netio(netio) + with FileLock(ttyC): + esc_char = get_escape_character(args.escape) + + if args.telnet_server: + addr, _, port = args.telnet_server.partition(':') + nport = 0 + try: + nport = int(port) + except ValueError: + pass + if (addr == '' or nport == 0): + raise ConfigError('format for --telnet-server must be ' + 'ADDR:PORT (like 127.0.0.1:20000)') + + while not stop_event.is_set(): + try: + if args.telnet_server: + with TelnetServer(addr, nport, stop_event) as console: + with IOU(ttyC, ttyS, stop_event) as router: + send_recv_loop(console, router, b'', stop_event) + else: + with IOU(ttyC, ttyS, stop_event) as router, TTY() as console: + send_recv_loop(console, router, esc_char, stop_event) + except ConnectionRefusedError: + pass + except KeyboardInterrupt: + sys.exit(EXIT_ABORT) + finally: + # Put us at the beginning of a line + if not args.telnet_server: + print() + + except IOUConError as e: + if args.debug: + traceback.print_exc(file=sys.stderr) + else: + print(e, file=sys.stderr) + sys.exit(EXIT_FAILURE) + + log.info("exiting...") + + +def main(): + + import threading + stop_event = threading.Event() + args = get_args() + start_ioucon(args, stop_event) + +if __name__ == '__main__': + main() diff --git a/tests/vpcs/test_vpcs_device.py~ b/tests/vpcs/test_vpcs_device.py~ new file mode 100644 index 00000000..0749c97f --- /dev/null +++ b/tests/vpcs/test_vpcs_device.py~ @@ -0,0 +1,29 @@ +from gns3server.modules.iou import IOUDevice +import os +import pytest + + +@pytest.fixture(scope="session") +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.start() + request.addfinalizer(iou_device.delete) + return iou_device + + +def test_iou_is_started(iou): + + print(iou.command()) + assert iou.id == 1 # we should have only one IOU running! + assert iou.is_running() + + +def test_iou_restart(iou): + + iou.stop() + assert not iou.is_running() + iou.start() + assert iou.is_running() From ac70b5d48ac4e19479bf04494d860f540fe3f6a0 Mon Sep 17 00:00:00 2001 From: joebowen Date: Tue, 6 May 2014 09:07:23 -0600 Subject: [PATCH 04/15] Delete .gitignore~ --- .gitignore~ | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 .gitignore~ diff --git a/.gitignore~ b/.gitignore~ deleted file mode 100644 index 49bac9bd..00000000 --- a/.gitignore~ +++ /dev/null @@ -1,36 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject -.settings From ec08a5a72af2ea3184c33b95ab2636589da5b4e3 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:06:10 -0600 Subject: [PATCH 05/15] Update file structure --- gns3server/modules/vpcs/__init__.py | 690 ++++++++++++++++++ gns3server/modules/vpcs/__init__.py~ | 3 - gns3server/modules/vpcs/nios/nio_tap.py | 46 ++ gns3server/modules/vpcs/nios/nio_tap.py~~ | 46 ++ gns3server/modules/vpcs/nios/nio_udp.py | 72 ++ gns3server/modules/vpcs/schemas.py | 306 ++++++++ gns3server/modules/vpcs/vpcs_device.py | 471 ++++++++++++ .../{__init__.py~ => vpcs/vpcs_error.py} | 28 +- gns3server/modules/vpcs/vpcscon.py | 642 ++++++++++++++++ ...st_vpcs_device.py~ => test_vpcs_device.py} | 0 10 files changed, 2293 insertions(+), 11 deletions(-) create mode 100644 gns3server/modules/vpcs/__init__.py create mode 100644 gns3server/modules/vpcs/nios/nio_tap.py create mode 100644 gns3server/modules/vpcs/nios/nio_tap.py~~ create mode 100644 gns3server/modules/vpcs/nios/nio_udp.py create mode 100644 gns3server/modules/vpcs/schemas.py create mode 100644 gns3server/modules/vpcs/vpcs_device.py rename gns3server/modules/{__init__.py~ => vpcs/vpcs_error.py} (61%) create mode 100644 gns3server/modules/vpcs/vpcscon.py rename tests/vpcs/{test_vpcs_device.py~ => test_vpcs_device.py} (100%) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py new file mode 100644 index 00000000..251cb280 --- /dev/null +++ b/gns3server/modules/vpcs/__init__.py @@ -0,0 +1,690 @@ +# -*- 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 . + +""" +VPCS server module. +""" + +import os +import sys +import base64 +import tempfile +import fcntl +import struct +import socket +import shutil + +from gns3server.modules import IModule +from gns3server.config import Config +import gns3server.jsonrpc as jsonrpc +from .vpcs_device import VPCSDevice +from .vpcs_error import VPCSError +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP +from ..attic import find_unused_port + +from .schemas import VPCS_CREATE_SCHEMA +from .schemas import VPCS_DELETE_SCHEMA +from .schemas import VPCS_UPDATE_SCHEMA +from .schemas import VPCS_START_SCHEMA +from .schemas import VPCS_STOP_SCHEMA +from .schemas import VPCS_RELOAD_SCHEMA +from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA +from .schemas import VPCS_ADD_NIO_SCHEMA +from .schemas import VPCS_DELETE_NIO_SCHEMA + +import logging +log = logging.getLogger(__name__) + + +class VPCS(IModule): + """ + VPCS 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 VPCS location + config = Config.instance() + VPCS_config = config.get_section_config(name.upper()) + self._VPCS = VPCS_config.get("VPCS") + if not self._VPCS or not os.path.isfile(self._VPCS): + VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") + if os.path.isfile(VPCS_in_cwd): + self._VPCS = VPCS_in_cwd + else: + # look for VPCS if none is defined or accessible + for path in os.environ["PATH"].split(":"): + try: + if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): + self._VPCS = os.path.join(path, "VPCS") + break + except OSError: + continue + + if not self._VPCS: + log.warning("VPCS binary couldn't be found!") + elif not os.access(self._VPCS, os.X_OK): + log.warning("VPCS is not executable") + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._VPCS_instances = {} + self._console_start_port_range = 4001 + self._console_end_port_range = 4512 + self._allocated_console_ports = [] + self._current_console_port = self._console_start_port_range + self._udp_start_port_range = 30001 + self._udp_end_port_range = 40001 + self._current_udp_port = self._udp_start_port_range + self._host = kwargs["host"] + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + self._VPCSrc = "" + + # check every 5 seconds + self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) + self._VPCS_callback.start() + + def stop(self, signum=None): + """ + Properly stops the module. + + :param signum: signal number (if called by the signal handler) + """ + + self._VPCS_callback.stop() + # delete all VPCS instances + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.delete() + + IModule.stop(self, signum) # this will stop the I/O loop + + def _check_VPCS_is_alive(self): + """ + Periodic callback to check if VPCS and VPCS are alive + for each VPCS instance. + + Sends a notification to the client if not. + """ + + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + if VPCS_instance.started and (not VPCS_instance.is_running() or not VPCS_instance.is_VPCS_running()): + notification = {"module": self.name, + "id": VPCS_id, + "name": VPCS_instance.name} + if not VPCS_instance.is_running(): + stdout = VPCS_instance.read_VPCS_stdout() + notification["message"] = "VPCS has stopped running" + notification["details"] = stdout + self.send_notification("{}.VPCS_stopped".format(self.name), notification) + elif not VPCS_instance.is_VPCS_running(): + stdout = VPCS_instance.read_VPCS_stdout() + notification["message"] = "VPCS has stopped running" + notification["details"] = stdout + self.send_notification("{}.VPCS_stopped".format(self.name), notification) + VPCS_instance.stop() + + def get_VPCS_instance(self, VPCS_id): + """ + Returns an VPCS device instance. + + :param VPCS_id: VPCS device identifier + + :returns: VPCSDevice instance + """ + + if VPCS_id not in self._VPCS_instances: + log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1) + self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id)) + return None + return self._VPCS_instances[VPCS_id] + + @IModule.route("VPCS.reset") + def reset(self, request): + """ + Resets the module. + + :param request: JSON request + """ + + # delete all VPCS instances + for VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.delete() + + # resets the instance IDs + VPCSDevice.reset() + + self._VPCS_instances.clear() + self._remote_server = False + self._current_console_port = self._console_start_port_range + self._current_udp_port = self._udp_start_port_range + + if self._VPCSrc and os.path.isfile(self._VPCSrc): + try: + log.info("deleting VPCSrc file {}".format(self._VPCSrc)) + os.remove(self._VPCSrc) + except OSError as e: + log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e)) + + log.info("VPCS module has been reset") + + @IModule.route("VPCS.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 == None: + self.send_param_error() + return + + if "VPCS" in request and request["VPCS"]: + self._VPCS = request["VPCS"] + log.info("VPCS path set to {}".format(self._VPCS)) + + 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"] + ".gns3") + 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 VPCS_id in self._VPCS_instances: + VPCS_instance = self._VPCS_instances[VPCS_id] + VPCS_instance.working_dir = self._working_dir + + 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)) + + def test_result(self, message, result="error"): + """ + """ + + return {"result": result, "message": message} + + @IModule.route("VPCS.test_settings") + def test_settings(self, request): + """ + """ + + response = [] + + self.send_response(response) + + @IModule.route("VPCS.create") + def VPCS_create(self, request): + """ + Creates a new VPCS instance. + + Mandatory request parameters: + - path (path to the VPCS executable) + + Optional request parameters: + - name (VPCS name) + + Response parameters: + - id (VPCS instance identifier) + - name (VPCS name) + - default settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_CREATE_SCHEMA): + return + + name = None + if "name" in request: + name = request["name"] + VPCS_path = request["path"] + + try: + try: + os.makedirs(self._working_dir) + except FileExistsError: + pass + except OSError as e: + raise VPCSError("Could not create working directory {}".format(e)) + + VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name) + # find a console port + if self._current_console_port > self._console_end_port_range: + self._current_console_port = self._console_start_port_range + try: + VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) + except Exception as e: + raise VPCSError(e) + self._current_console_port += 1 + except VPCSError as e: + self.send_custom_error(str(e)) + return + + response = {"name": VPCS_instance.name, + "id": VPCS_instance.id} + + defaults = VPCS_instance.defaults() + response.update(defaults) + self._VPCS_instances[VPCS_instance.id] = VPCS_instance + self.send_response(response) + + @IModule.route("VPCS.delete") + def VPCS_delete(self, request): + """ + Deletes an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameter: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_DELETE_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + VPCS_instance.delete() + del self._VPCS_instances[request["id"]] + except VPCSError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + @IModule.route("VPCS.update") + def VPCS_update(self, request): + """ + Updates an VPCS instance + + Mandatory request parameters: + - id (VPCS instance identifier) + + Optional request parameters: + - any setting to update + - startup_config_base64 (startup-config base64 encoded) + + Response parameters: + - updated settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_UPDATE_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + response = {} + try: + # a new startup-config has been pushed + if "startup_config_base64" in request: + config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + config = "!\n" + config.replace("\r", "") + config = config.replace('%h', VPCS_instance.name) + config_path = os.path.join(VPCS_instance.working_dir, "startup-config") + try: + with open(config_path, "w") as f: + log.info("saving startup-config to {}".format(config_path)) + f.write(config) + except OSError as e: + raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) + # update the request with the new local startup-config path + request["startup_config"] = os.path.basename(config_path) + + except VPCSError as e: + self.send_custom_error(str(e)) + return + + # update the VPCS settings + for name, value in request.items(): + if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value: + try: + setattr(VPCS_instance, name, value) + response[name] = value + except VPCSError as e: + self.send_custom_error(str(e)) + return + + self.send_response(response) + + @IModule.route("VPCS.start") + def vm_start(self, request): + """ + Starts an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_START_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + log.debug("starting VPCS with command: {}".format(VPCS_instance.command())) + VPCS_instance.VPCS = self._VPCS + VPCS_instance.VPCSrc = self._VPCSrc + VPCS_instance.start() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.stop") + def vm_stop(self, request): + """ + Stops an VPCS instance. + + Mandatory request parameters: + - id (VPCS instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_STOP_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + VPCS_instance.stop() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.reload") + def vm_reload(self, request): + """ + Reloads an VPCS instance. + + Mandatory request parameters: + - id (VPCS identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VPCS_RELOAD_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + if VPCS_instance.is_running(): + VPCS_instance.stop() + VPCS_instance.start() + except VPCSError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("VPCS.allocate_udp_port") + def allocate_udp_port(self, request): + """ + Allocates a UDP port in order to create an UDP NIO. + + Mandatory request parameters: + - id (VPCS 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, VPCS_ALLOCATE_UDP_PORT_SCHEMA): + return + + # get the instance + VPCS_instance = self.get_VPCS_instance(request["id"]) + if not VPCS_instance: + return + + try: + + # find a UDP port + if self._current_udp_port >= self._udp_end_port_range: + self._current_udp_port = self._udp_start_port_range + try: + port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") + except Exception as e: + raise VPCSError(e) + self._current_udp_port += 1 + + log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name, + VPCS_instance.id, + port, + self._host)) + response = {"lport": port} + + except VPCSError as e: + self.send_custom_error(str(e)) + return + + response["port_id"] = request["port_id"] + self.send_response(response) + + def _check_for_privileged_access(self, device): + """ + Check if VPCS can access Ethernet and TAP devices. + + :param device: device name + """ + + # we are root, so VPCS should have privileged access too + if os.geteuid() == 0: + return + + # test if VPCS has the CAP_NET_RAW capability + if "security.capability" in os.listxattr(self._VPCS): + try: + caps = os.getxattr(self._VPCS, "security.capability") + # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set + if struct.unpack(". + +""" +Interface for TAP NIOs (UNIX based OSes only). +""" + + +class NIO_TAP(object): + """ + IOU TAP NIO. + + :param tap_device: TAP device name (e.g. tap0) + """ + + def __init__(self, tap_device): + + self._tap_device = tap_device + + @property + def tap_device(self): + """ + Returns the TAP device used by this NIO. + + :returns: the TAP device name + """ + + return self._tap_device + + def __str__(self): + + return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_tap.py~~ b/gns3server/modules/vpcs/nios/nio_tap.py~~ new file mode 100644 index 00000000..ee550e7b --- /dev/null +++ b/gns3server/modules/vpcs/nios/nio_tap.py~~ @@ -0,0 +1,46 @@ +# -*- 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 TAP NIOs (UNIX based OSes only). +""" + + +class NIO_TAP(object): + """ + IOU TAP NIO. + + :param tap_device: TAP device name (e.g. tap0) + """ + + def __init__(self, tap_device): + + self._tap_device = tap_device + + @property + def tap_device(self): + """ + Returns the TAP device used by this NIO. + + :returns: the TAP device name + """ + + return self._tap_device + + def __str__(self): + + return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py new file mode 100644 index 00000000..3142d70e --- /dev/null +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -0,0 +1,72 @@ +# -*- 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. +""" + + +class NIO_UDP(object): + """ + IOU 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): + + 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/vpcs/schemas.py b/gns3server/modules/vpcs/schemas.py new file mode 100644 index 00000000..d1061384 --- /dev/null +++ b/gns3server/modules/vpcs/schemas.py @@ -0,0 +1,306 @@ +# -*- 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 . + + +VPCS_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the VPCS executable", + "type": "string", + "minLength": 1, + } + }, + "required": ["path"] +} + +VPCS_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "path": { + "description": "path to the VPCS executable", + "type": "string", + "minLength": 1, + }, + "script_file": { + "description": "path to the VPCS startup configuration file", + "type": "string", + "minLength": 1, + }, + "script_file_base64": { + "description": "startup configuration base64 encoded", + "type": "string" + }, + }, + "required": ["id"] +} + +VPCS_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} + +VPCS_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + }, + "required": ["id", "port_id"] +} + +VPCS_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for an VPCS 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": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + "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"}, + ] + }, + }, + "required": ["id", "port_id", "nio"] +} + +VPCS_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for an VPCS instance", + "type": "object", + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + }, + "required": ["id"] +} diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py new file mode 100644 index 00000000..3bd3f95a --- /dev/null +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -0,0 +1,471 @@ +# -*- 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 . + +""" +VPCS device management (creates command line, processes, files etc.) in +order to run an VPCS instance. +""" + +import os +import re +import signal +import subprocess +import argparse +import threading +import configparser +from .vpcscon import start_vpcscon +from .vpcs_error import VPCSError +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP + +import logging +log = logging.getLogger(__name__) + + +class VPCSDevice(object): + """ + VPCS device implementation. + + :param path: path to VPCS executable + :param working_dir: path to a working directory + :param host: host/address to bind for console and UDP connections + :param name: name of this VPCS device + """ + + _instances = [] + + def __init__(self, path, working_dir, host="127.0.0.1", name=None): + + # find an instance identifier (0 <= id < 255) + # This 255 limit is due to a restriction on the number of possible + # mac addresses given in VPCS using the -m option + self._id = 0 + for identifier in range(0, 255): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break + + if self._id == 0: + raise VPCSError("Maximum number of VPCS instances reached") + + if name: + self._name = name + else: + self._name = "VPCS{}".format(self._id) + self._path = path + self._console = None + self._working_dir = None + self._command = [] + self._process = None + self._vpcs_stdout_file = "" + self._vpcscon_thead = None + self._vpcscon_thread_stop_event = None + self._host = host + self._started = False + + # VPCS settings + self._script_file = "" + + # update the working directory + self.working_dir = working_dir + + log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for VPCS. + + :returns: default values (dictionary) + """ + + vpcs_defaults = {"name": self._name, + "path": self._path, + "script_file": self._script_file, + "console": self._console} + + return vpcs_defaults + + @property + def id(self): + """ + Returns the unique ID for this VPCS device. + + :returns: id (integer) + """ + + return(self._id) + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + + @property + def name(self): + """ + Returns the name of this VPCS device. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VPCS device. + + :param new_name: name + """ + + self._name = new_name + log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + @property + def path(self): + """ + Returns the path to the VPCS executable. + + :returns: path to VPCS + """ + + return(self._path) + + @path.setter + def path(self, path): + """ + Sets the path to the VPCS executable. + + :param path: path to VPCS + """ + + self._path = path + log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, + id=self._id, + path=path)) + + @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 for VPCS. + + :param working_dir: path to the working directory + """ + + # create our own working directory + working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id)) + try: + os.makedirs(working_dir) + except FileExistsError: + pass + except OSError as e: + raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("VPCS {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) + """ + + self._console = console + log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + def command(self): + """ + Returns the VPCS command line. + + :returns: VPCS command line (string) + """ + + return " ".join(self._build_command()) + + def delete(self): + """ + Deletes this VPCS device. + """ + + self.stop() + self._instances.remove(self._id) + log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + @property + def started(self): + """ + Returns either this VPCS device has been started or not. + + :returns: boolean + """ + + return self._started + + def _start_vpcscon(self): + """ + Starts vpcscon thread (for console connections). + """ + + if not self._vpcscon_thead: + telnet_server = "{}:{}".format(self._host, self._console) + log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) + args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) + self._vpcscon_thread_stop_event = threading.Event() + self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) + self._vpcscon_thead.start() ", ".join(missing_libs))) + + def start(self): + """ + Starts the VPCS process. + """ + + if not self.is_running(): + + if not os.path.isfile(self._path): + raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) + + if not os.access(self._path, os.X_OK): + raise VPCSError("VPCS image '{}' is not executable".format(self._path)) + + self._command = self._build_command() + try: + log.info("starting VPCS: {}".format(self._command)) + self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") + log.info("logging to {}".format(self._vpcs_stdout_file)) + with open(self._vpcs_stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except FileNotFoundError as e: + raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e)) + except OSError as e: + vpcs_stdout = self.read_vpcs_stdout() + log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + + # start console support + self._start_vpcscon() + + def stop(self): + """ + Stops the VPCS process. + """ + + # stop the VPCS process + if self.is_running(): + log.info("stopping VPCS 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() == None: + log.warn("VPCS instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + self._started = False + + # stop console support + if self._vpcscon_thead: + self._vpcscon_thread_stop_event.set() + if self._vpcscon_thead.is_alive(): + self._vpcscon_thead.join(timeout=0.10) + self._vpcscon_thead = None + + + def read_vpcs_stdout(self): + """ + Reads the standard output of the VPCS process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._vpcs_stdout_file: + try: + with open(self._vpcs_stdout_file) as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the VPCS process is running + + :returns: True or False + """ + + if self._process and self._process.poll() == None: + return True + return False + + + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + adapter.add_nio(port_id, nio) + log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + + def _build_command(self): + """ + Command to start the VPCS process. + (to be passed to subprocess.Popen()) + + VPCS command line: + usage: vpcs [options] [scriptfile] + Option: + -h print this help then exit + -v print version information then exit + + -p port run as a daemon listening on the tcp 'port' + -m num start byte of ether address, default from 0 + -r file load and execute script file + compatible with older versions, DEPRECATED. + + -e tap mode, using /dev/tapx (linux only) + -u udp mode, default + + udp mode options: + -s port local udp base port, default from 20000 + -c port remote udp base port (dynamips udp port), default from 30000 + -t ip remote host IP, default 127.0.0.1 + + hypervisor mode option: + -H port run as the hypervisor listening on the tcp 'port' + + If no 'scriptfile' specified, vpcs will read and execute the file named + 'startup.vpc' if it exsits in the current directory. + + """ + + command = [self._path] + command.extend(["-p", str(self._console)]) + command.extend(["-s", str(self._lport)]) + command.extend(["-c", str(self._rport)]) + command.extend(["-t", str(self._rhost)]) + command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset + if self._script_file: + command.extend([self._script_file]) + return command + + @property + def script_file(self): + """ + Returns the startup-config for this VPCS instance. + + :returns: path to startup-config file + """ + + return self._script_file + + @script_file.setter + def script_file(self, script_file): + """ + Sets the startup-config for this VPCS instance. + + :param script_file: path to startup-config file + """ + + self._script_file = script_file + log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, + id=self._id, + config=self._script_file)) + + diff --git a/gns3server/modules/__init__.py~ b/gns3server/modules/vpcs/vpcs_error.py similarity index 61% rename from gns3server/modules/__init__.py~ rename to gns3server/modules/vpcs/vpcs_error.py index 59304d19..167129ba 100644 --- a/gns3server/modules/__init__.py~ +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -15,13 +15,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys -from .base import IModule -from .dynamips import Dynamips +""" +Custom exceptions for VPCS module. +""" -MODULES = [Dynamips] -if sys.platform.startswith("linux"): - # IOU runs only on Linux - from .iou import IOU - MODULES.append(IOU) +class VPCSError(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/vpcs/vpcscon.py b/gns3server/modules/vpcs/vpcscon.py new file mode 100644 index 00000000..b481175d --- /dev/null +++ b/gns3server/modules/vpcs/vpcscon.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013, 2014 James E. Carpenter +# +# 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 socket +import sys +import os +import select +import fcntl +import struct +import termios +import tty +import time +import argparse +import traceback + + +import logging +log = logging.getLogger(__name__) + + +# Escape characters +ESC_CHAR = '^^' # can be overriden from command line +ESC_QUIT = 'q' + +# VPCS seems to only send *1* byte at a time. If +# they ever fix that we'll be ready for it. +BUFFER_SIZE = 1024 + +# How long to wait before retrying a connection (seconds) +RETRY_DELAY = 3 + +# How often to test an idle connection (seconds) +POLL_TIMEOUT = 3 + + +EXIT_SUCCESS = 0 +EXIT_FAILURE = 1 +EXIT_ABORT = 2 + +# Mostly from: +# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py +#--[ Telnet Commands ]--------------------------------------------------------- +SE = 240 # End of subnegotiation parameters +NOP = 241 # No operation +DATMK = 242 # Data stream portion of a sync. +BREAK = 243 # NVT Character BRK +IP = 244 # Interrupt Process +AO = 245 # Abort Output +AYT = 246 # Are you there +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # The Go Ahead Signal +SB = 250 # Sub-option to follow +WILL = 251 # Will; request or confirm option begin +WONT = 252 # Wont; deny option request +DO = 253 # Do = Request or confirm remote option +DONT = 254 # Don't = Demand or confirm option halt +IAC = 255 # Interpret as Command +SEND = 1 # Sub-process negotiation SEND command +IS = 0 # Sub-process negotiation IS command +#--[ Telnet Options ]---------------------------------------------------------- +BINARY = 0 # Transmit Binary +ECHO = 1 # Echo characters back to sender +RECON = 2 # Reconnection +SGA = 3 # Suppress Go-Ahead +TMARK = 6 # Timing Mark +TTYPE = 24 # Terminal Type +NAWS = 31 # Negotiate About Window Size +LINEMO = 34 # Line Mode + + +class FileLock: + + # struct flock { /* from fcntl(2) */ + # ... + # short l_type; /* Type of lock: F_RDLCK, + # F_WRLCK, F_UNLCK */ + # short l_whence; /* How to interpret l_start: + # SEEK_SET, SEEK_CUR, SEEK_END */ + # off_t l_start; /* Starting offset for lock */ + # off_t l_len; /* Number of bytes to lock */ + # pid_t l_pid; /* PID of process blocking our lock + # (F_GETLK only) */ + # ... + # }; + _flock = struct.Struct('hhqql') + + def __init__(self, fname=None): + self.fd = None + self.fname = fname + + def get_lock(self): + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, + 0, 0, os.getpid()) + flk = self._flock.unpack( + fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) + + # If it's not locked (or is locked by us) then return None, + # otherwise return the PID of the owner. + if flk[0] == fcntl.F_UNLCK: + return None + return flk[4] + + def lock(self): + try: + self.fd = open('{}.lck'.format(self.fname), 'a') + except Exception as e: + raise LockError("Couldn't get lock on {}: {}" + .format(self.fname, e)) + + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) + try: + fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) + except BlockingIOError: + raise LockError("Already connected. PID {} has lock on {}" + .format(self.get_lock(), self.fname)) + + # If we got here then we must have the lock. Store the PID. + self.fd.truncate(0) + self.fd.write('{}\n'.format(os.getpid())) + self.fd.flush() + + def unlock(self): + if self.fd: + # Deleting first prevents a race condition + os.unlink(self.fd.name) + self.fd.close() + + def __enter__(self): + self.lock() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.unlock() + return False + + +class Console: + def fileno(self): + raise NotImplementedError("Only routers have fileno()") + + +class Router: + pass + + +class TTY(Console): + + def read(self, fileno, bufsize): + return self.fd.read(bufsize) + + def write(self, buf): + return self.fd.write(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def __enter__(self): + try: + self.fd = open('/dev/tty', 'r+b', buffering=0) + except OSError as e: + raise TTYError("Couldn't open controlling TTY: {}".format(e)) + + # Save original flags + self.termios = termios.tcgetattr(self.fd) + self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) + + # Update flags + tty.setraw(self.fd, termios.TCSANOW) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + # Restore flags to original settings + termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) + + self.fd.close() + + return False + + +class TelnetServer(Console): + + def __init__(self, addr, port, stop_event): + self.addr = addr + self.port = port + self.fd_dict = {} + self.stop_event = stop_event + + def read(self, fileno, bufsize): + # Someone wants to connect? + if fileno == self.sock_fd.fileno(): + self._accept() + return None + + self._cur_fileno = fileno + + # Read a maximum of _bufsize_ bytes without blocking. When it + # would want to block it means there's no more data. An empty + # buffer normally means that we've been disconnected. + try: + buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) + except BlockingIOError: + return None + if not buf: + self._disconnect(fileno) + + # Process and remove any telnet commands from the buffer + if IAC in buf: + buf = self._IAC_parser(buf) + + return buf + + def write(self, buf): + for fd in self.fd_dict.values(): + fd.send(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.sock_fd, select.EPOLLIN) + + def _read_block(self, bufsize): + buf = self._read_cur(bufsize, socket.MSG_WAITALL) + # If we don't get everything we were looking for then the + # client probably disconnected. + if len(buf) < bufsize: + self._disconnect(self._cur_fileno) + return buf + + def _read_cur(self, bufsize, flags): + return self.fd_dict[self._cur_fileno].recv(bufsize, flags) + + def _write_cur(self, buf): + return self.fd_dict[self._cur_fileno].send(buf) + + def _IAC_parser(self, buf): + skip_to = 0 + while not self.stop_event.is_set(): + # Locate an IAC to process + iac_loc = buf.find(IAC, skip_to) + if iac_loc < 0: + break + + # Get the TELNET command + iac_cmd = bytearray([IAC]) + try: + iac_cmd.append(buf[iac_loc + 1]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 1]) + + # Is this just a 2-byte TELNET command? + if iac_cmd[1] not in [WILL, WONT, DO, DONT]: + if iac_cmd[1] == AYT: + log.debug("Telnet server received Are-You-There (AYT)") + self._write_cur( + b'\r\nYour Are-You-There received. I am here.\r\n' + ) + elif iac_cmd[1] == IAC: + # It's data, not an IAC + iac_cmd.pop() + # This prevents the 0xff from being + # interputed as yet another IAC + skip_to = iac_loc + 1 + log.debug("Received IAC IAC") + elif iac_cmd[1] == NOP: + pass + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x}".format(*iac_cmd)) + + # This must be a 3-byte TELNET command + else: + try: + iac_cmd.append(buf[iac_loc + 2]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 2]) + # We do ECHO, SGA, and BINARY. Period. + if (iac_cmd[1] == DO + and iac_cmd[2] not in [ECHO, SGA, BINARY]): + + self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + + # Remove the entire TELNET command from the buffer + buf = buf.replace(iac_cmd, b'', 1) + + # Return the new copy of the buffer, minus telnet commands + return buf + + def _accept(self): + fd, addr = self.sock_fd.accept() + self.fd_dict[fd.fileno()] = fd + self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) + + log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) + + # This is a one-way negotiation. This is very basic so there + # shouldn't be any problems with any decent client. + fd.send(bytes([IAC, WILL, ECHO, + IAC, WILL, SGA, + IAC, WILL, BINARY, + IAC, DO, BINARY])) + + if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: + fd.send(b'\r\nToo many connections\r\n') + self._disconnect(fd.fileno()) + log.warn("Client disconnected because of too many connections. " + "(limit currently {})".format(args.telnet_limit)) + + def _disconnect(self, fileno): + fd = self.fd_dict.pop(fileno) + log.info("Telnet client disconnected") + fd.shutdown(socket.SHUT_RDWR) + fd.close() + + def __enter__(self): + # Open a socket and start listening + sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock_fd.bind((self.addr, self.port)) + except OSError: + raise TelnetServerError("Cannot bind to {}:{}" + .format(self.addr, self.port)) + + sock_fd.listen(socket.SOMAXCONN) + self.sock_fd = sock_fd + log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for fileno in list(self.fd_dict.keys()): + self._disconnect(fileno) + self.sock_fd.close() + return False + + +class VPCS(Router): + + def __init__(self, ttyC, ttyS, stop_event): + self.ttyC = ttyC + self.ttyS = ttyS + self.stop_event = stop_event + + def read(self, bufsize): + try: + buf = self.fd.recv(bufsize) + except BlockingIOError: + return None + return buf + + def write(self, buf): + self.fd.send(buf) + + def _open(self): + self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.fd.setblocking(False) + + def _bind(self): + try: + os.unlink(self.ttyC) + except FileNotFoundError: + pass + except Exception as e: + raise NetioError("Couldn't unlink socket {}: {}" + .format(self.ttyC, e)) + + try: + self.fd.bind(self.ttyC) + except Exception as e: + raise NetioError("Couldn't create socket {}: {}" + .format(self.ttyC, e)) + + def _connect(self): + # Keep trying until we connect or die trying + while not self.stop_event.is_set(): + try: + self.fd.connect(self.ttyS) + except FileNotFoundError: + log.debug("Waiting to connect to {}".format(self.ttyS)) + time.sleep(RETRY_DELAY) + except Exception as e: + raise NetioError("Couldn't connect to socket {}: {}" + .format(self.ttyS, e)) + else: + break + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def fileno(self): + return self.fd.fileno() + + def __enter__(self): + self._open() + self._bind() + self._connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + os.unlink(self.ttyC) + self.fd.close() + return False + + +class VPCSConError(Exception): + pass + + +class LockError(VPCSConError): + pass + + +class NetioError(VPCSConError): + pass + + +class TTYError(VPCSConError): + pass + + +class TelnetServerError(VPCSConError): + pass + + +class ConfigError(VPCSConError): + pass + + +def mkdir_netio(netio_dir): + try: + os.mkdir(netio_dir) + except FileExistsError: + pass + except Exception as e: + raise NetioError("Couldn't create directory {}: {}" + .format(netio_dir, e)) + + +def send_recv_loop(console, router, esc_char, stop_event): + + epoll = select.epoll() + router.register(epoll) + console.register(epoll) + + router_fileno = router.fileno() + esc_quit = bytes(ESC_QUIT.upper(), 'ascii') + esc_state = False + + while not stop_event.is_set(): + event_list = epoll.poll(timeout=POLL_TIMEOUT) + + # When/if the poll times out we send an empty datagram. If VPCS + # has gone away then this will toss a ConnectionRefusedError + # exception. + if not event_list: + router.write(b'') + continue + + for fileno, event in event_list: + buf = bytearray() + + # VPCS --> tty(s) + if fileno == router_fileno: + while not stop_event.is_set(): + data = router.read(BUFFER_SIZE) + if not data: + break + buf.extend(data) + console.write(buf) + + # tty --> VPCS + else: + while not stop_event.is_set(): + data = console.read(fileno, BUFFER_SIZE) + if not data: + break + buf.extend(data) + + # If we just received the escape character then + # enter the escape state. + # + # If we are in the escape state then check for a + # quit command. Or if it's the escape character then + # send the escape character. Else, send the escape + # character we ate earlier and whatever character we + # just got. Exit escape state. + # + # If we're not in the escape state and this isn't an + # escape character then just send it to VPCS. + if esc_state: + if buf.upper() == esc_quit: + sys.exit(EXIT_SUCCESS) + elif buf == esc_char: + router.write(esc_char) + else: + router.write(esc_char) + router.write(buf) + esc_state = False + elif buf == esc_char: + esc_state = True + else: + router.write(buf) + + +def get_args(): + parser = argparse.ArgumentParser( + description='Connect to an VPCS console port.') + parser.add_argument('-d', '--debug', action='store_true', + help='display some debugging information') + parser.add_argument('-e', '--escape', + help='set escape character (default: %(default)s)', + default=ESC_CHAR, metavar='CHAR') + parser.add_argument('-t', '--telnet-server', + help='start telnet server listening on ADDR:PORT', + metavar='ADDR:PORT', default=False) + parser.add_argument('-l', '--telnet-limit', + help='maximum number of simultaneous ' + 'telnet connections (default: %(default)s)', + metavar='LIMIT', type=int, default=1) + parser.add_argument('appl_id', help='VPCS instance identifier') + return parser.parse_args() + + +def get_escape_character(escape): + + # Figure out the escape character to use. + # Can be any ASCII character or a spelled out control + # character, like "^e". The string "none" disables it. + if escape.lower() == 'none': + esc_char = b'' + elif len(escape) == 2 and escape[0] == '^': + c = ord(escape[1].upper()) - 0x40 + if not 0 <= c <= 0x1f: # control code range + raise ConfigError("Invalid control code") + esc_char = bytes([c]) + elif len(escape) == 1: + try: + esc_char = bytes(escape, 'ascii') + except ValueError as e: + raise ConfigError("Invalid escape character") from e + else: + raise ConfigError("Invalid length for escape character") + + return esc_char + + +def start_VPCScon(cmdline_args, stop_event): + + global args + args = cmdline_args + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + # default logging level + logging.basicConfig(level=logging.INFO) + + # Create paths for the Unix domain sockets + netio = '/tmp/netio{}'.format(os.getuid()) + ttyC = '{}/ttyC{}'.format(netio, args.appl_id) + ttyS = '{}/ttyS{}'.format(netio, args.appl_id) + + try: + mkdir_netio(netio) + with FileLock(ttyC): + esc_char = get_escape_character(args.escape) + + if args.telnet_server: + addr, _, port = args.telnet_server.partition(':') + nport = 0 + try: + nport = int(port) + except ValueError: + pass + if (addr == '' or nport == 0): + raise ConfigError('format for --telnet-server must be ' + 'ADDR:PORT (like 127.0.0.1:20000)') + + while not stop_event.is_set(): + try: + if args.telnet_server: + with TelnetServer(addr, nport, stop_event) as console: + with VPCS(ttyC, ttyS, stop_event) as router: + send_recv_loop(console, router, b'', stop_event) + else: + with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console: + send_recv_loop(console, router, esc_char, stop_event) + except ConnectionRefusedError: + pass + except KeyboardInterrupt: + sys.exit(EXIT_ABORT) + finally: + # Put us at the beginning of a line + if not args.telnet_server: + print() + + except VPCSConError as e: + if args.debug: + traceback.print_exc(file=sys.stderr) + else: + print(e, file=sys.stderr) + sys.exit(EXIT_FAILURE) + + log.info("exiting...") + + +def main(): + + import threading + stop_event = threading.Event() + args = get_args() + start_VPCScon(args, stop_event) + +if __name__ == '__main__': + main() diff --git a/tests/vpcs/test_vpcs_device.py~ b/tests/vpcs/test_vpcs_device.py similarity index 100% rename from tests/vpcs/test_vpcs_device.py~ rename to tests/vpcs/test_vpcs_device.py From 975e5db82f5faf6db7e52566744cd918ec3f76eb Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:08:16 -0600 Subject: [PATCH 06/15] Update file structure --- gns3server/modules/vpcs/__init__.py~ | 690 ---------------------- gns3server/modules/vpcs/nios/nio_tap.py~ | 46 -- gns3server/modules/vpcs/nios/nio_tap.py~~ | 46 -- gns3server/modules/vpcs/nios/nio_udp.py~ | 72 --- gns3server/modules/vpcs/schemas.py~ | 306 ---------- gns3server/modules/vpcs/vpcs_device.py~ | 471 --------------- gns3server/modules/vpcs/vpcs_error.py~ | 39 -- gns3server/modules/vpcs/vpcscon.py~ | 642 -------------------- 8 files changed, 2312 deletions(-) delete mode 100644 gns3server/modules/vpcs/__init__.py~ delete mode 100644 gns3server/modules/vpcs/nios/nio_tap.py~ delete mode 100644 gns3server/modules/vpcs/nios/nio_tap.py~~ delete mode 100644 gns3server/modules/vpcs/nios/nio_udp.py~ delete mode 100644 gns3server/modules/vpcs/schemas.py~ delete mode 100644 gns3server/modules/vpcs/vpcs_device.py~ delete mode 100644 gns3server/modules/vpcs/vpcs_error.py~ delete mode 100644 gns3server/modules/vpcs/vpcscon.py~ diff --git a/gns3server/modules/vpcs/__init__.py~ b/gns3server/modules/vpcs/__init__.py~ deleted file mode 100644 index 251cb280..00000000 --- a/gns3server/modules/vpcs/__init__.py~ +++ /dev/null @@ -1,690 +0,0 @@ -# -*- 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 . - -""" -VPCS server module. -""" - -import os -import sys -import base64 -import tempfile -import fcntl -import struct -import socket -import shutil - -from gns3server.modules import IModule -from gns3server.config import Config -import gns3server.jsonrpc as jsonrpc -from .vpcs_device import VPCSDevice -from .vpcs_error import VPCSError -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from ..attic import find_unused_port - -from .schemas import VPCS_CREATE_SCHEMA -from .schemas import VPCS_DELETE_SCHEMA -from .schemas import VPCS_UPDATE_SCHEMA -from .schemas import VPCS_START_SCHEMA -from .schemas import VPCS_STOP_SCHEMA -from .schemas import VPCS_RELOAD_SCHEMA -from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA -from .schemas import VPCS_ADD_NIO_SCHEMA -from .schemas import VPCS_DELETE_NIO_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class VPCS(IModule): - """ - VPCS 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 VPCS location - config = Config.instance() - VPCS_config = config.get_section_config(name.upper()) - self._VPCS = VPCS_config.get("VPCS") - if not self._VPCS or not os.path.isfile(self._VPCS): - VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") - if os.path.isfile(VPCS_in_cwd): - self._VPCS = VPCS_in_cwd - else: - # look for VPCS if none is defined or accessible - for path in os.environ["PATH"].split(":"): - try: - if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): - self._VPCS = os.path.join(path, "VPCS") - break - except OSError: - continue - - if not self._VPCS: - log.warning("VPCS binary couldn't be found!") - elif not os.access(self._VPCS, os.X_OK): - log.warning("VPCS is not executable") - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._VPCS_instances = {} - self._console_start_port_range = 4001 - self._console_end_port_range = 4512 - self._allocated_console_ports = [] - self._current_console_port = self._console_start_port_range - self._udp_start_port_range = 30001 - self._udp_end_port_range = 40001 - self._current_udp_port = self._udp_start_port_range - self._host = kwargs["host"] - self._projects_dir = kwargs["projects_dir"] - self._tempdir = kwargs["temp_dir"] - self._working_dir = self._projects_dir - self._VPCSrc = "" - - # check every 5 seconds - self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) - self._VPCS_callback.start() - - def stop(self, signum=None): - """ - Properly stops the module. - - :param signum: signal number (if called by the signal handler) - """ - - self._VPCS_callback.stop() - # delete all VPCS instances - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.delete() - - IModule.stop(self, signum) # this will stop the I/O loop - - def _check_VPCS_is_alive(self): - """ - Periodic callback to check if VPCS and VPCS are alive - for each VPCS instance. - - Sends a notification to the client if not. - """ - - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - if VPCS_instance.started and (not VPCS_instance.is_running() or not VPCS_instance.is_VPCS_running()): - notification = {"module": self.name, - "id": VPCS_id, - "name": VPCS_instance.name} - if not VPCS_instance.is_running(): - stdout = VPCS_instance.read_VPCS_stdout() - notification["message"] = "VPCS has stopped running" - notification["details"] = stdout - self.send_notification("{}.VPCS_stopped".format(self.name), notification) - elif not VPCS_instance.is_VPCS_running(): - stdout = VPCS_instance.read_VPCS_stdout() - notification["message"] = "VPCS has stopped running" - notification["details"] = stdout - self.send_notification("{}.VPCS_stopped".format(self.name), notification) - VPCS_instance.stop() - - def get_VPCS_instance(self, VPCS_id): - """ - Returns an VPCS device instance. - - :param VPCS_id: VPCS device identifier - - :returns: VPCSDevice instance - """ - - if VPCS_id not in self._VPCS_instances: - log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1) - self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id)) - return None - return self._VPCS_instances[VPCS_id] - - @IModule.route("VPCS.reset") - def reset(self, request): - """ - Resets the module. - - :param request: JSON request - """ - - # delete all VPCS instances - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.delete() - - # resets the instance IDs - VPCSDevice.reset() - - self._VPCS_instances.clear() - self._remote_server = False - self._current_console_port = self._console_start_port_range - self._current_udp_port = self._udp_start_port_range - - if self._VPCSrc and os.path.isfile(self._VPCSrc): - try: - log.info("deleting VPCSrc file {}".format(self._VPCSrc)) - os.remove(self._VPCSrc) - except OSError as e: - log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e)) - - log.info("VPCS module has been reset") - - @IModule.route("VPCS.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 == None: - self.send_param_error() - return - - if "VPCS" in request and request["VPCS"]: - self._VPCS = request["VPCS"] - log.info("VPCS path set to {}".format(self._VPCS)) - - 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"] + ".gns3") - 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 VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.working_dir = self._working_dir - - 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)) - - def test_result(self, message, result="error"): - """ - """ - - return {"result": result, "message": message} - - @IModule.route("VPCS.test_settings") - def test_settings(self, request): - """ - """ - - response = [] - - self.send_response(response) - - @IModule.route("VPCS.create") - def VPCS_create(self, request): - """ - Creates a new VPCS instance. - - Mandatory request parameters: - - path (path to the VPCS executable) - - Optional request parameters: - - name (VPCS name) - - Response parameters: - - id (VPCS instance identifier) - - name (VPCS name) - - default settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_CREATE_SCHEMA): - return - - name = None - if "name" in request: - name = request["name"] - VPCS_path = request["path"] - - try: - try: - os.makedirs(self._working_dir) - except FileExistsError: - pass - except OSError as e: - raise VPCSError("Could not create working directory {}".format(e)) - - VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name) - # find a console port - if self._current_console_port > self._console_end_port_range: - self._current_console_port = self._console_start_port_range - try: - VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) - except Exception as e: - raise VPCSError(e) - self._current_console_port += 1 - except VPCSError as e: - self.send_custom_error(str(e)) - return - - response = {"name": VPCS_instance.name, - "id": VPCS_instance.id} - - defaults = VPCS_instance.defaults() - response.update(defaults) - self._VPCS_instances[VPCS_instance.id] = VPCS_instance - self.send_response(response) - - @IModule.route("VPCS.delete") - def VPCS_delete(self, request): - """ - Deletes an VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - - Response parameter: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_DELETE_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - VPCS_instance.delete() - del self._VPCS_instances[request["id"]] - except VPCSError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("VPCS.update") - def VPCS_update(self, request): - """ - Updates an VPCS instance - - Mandatory request parameters: - - id (VPCS instance identifier) - - Optional request parameters: - - any setting to update - - startup_config_base64 (startup-config base64 encoded) - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_UPDATE_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - response = {} - try: - # a new startup-config has been pushed - if "startup_config_base64" in request: - config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', VPCS_instance.name) - config_path = os.path.join(VPCS_instance.working_dir, "startup-config") - try: - with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(config_path)) - f.write(config) - except OSError as e: - raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) - # update the request with the new local startup-config path - request["startup_config"] = os.path.basename(config_path) - - except VPCSError as e: - self.send_custom_error(str(e)) - return - - # update the VPCS settings - for name, value in request.items(): - if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value: - try: - setattr(VPCS_instance, name, value) - response[name] = value - except VPCSError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("VPCS.start") - def vm_start(self, request): - """ - Starts an VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_START_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - log.debug("starting VPCS with command: {}".format(VPCS_instance.command())) - VPCS_instance.VPCS = self._VPCS - VPCS_instance.VPCSrc = self._VPCSrc - VPCS_instance.start() - except VPCSError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("VPCS.stop") - def vm_stop(self, request): - """ - Stops an VPCS instance. - - Mandatory request parameters: - - id (VPCS instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_STOP_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - VPCS_instance.stop() - except VPCSError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("VPCS.reload") - def vm_reload(self, request): - """ - Reloads an VPCS instance. - - Mandatory request parameters: - - id (VPCS identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, VPCS_RELOAD_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - if VPCS_instance.is_running(): - VPCS_instance.stop() - VPCS_instance.start() - except VPCSError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("VPCS.allocate_udp_port") - def allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (VPCS 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, VPCS_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: - return - - try: - - # find a UDP port - if self._current_udp_port >= self._udp_end_port_range: - self._current_udp_port = self._udp_start_port_range - try: - port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") - except Exception as e: - raise VPCSError(e) - self._current_udp_port += 1 - - log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name, - VPCS_instance.id, - port, - self._host)) - response = {"lport": port} - - except VPCSError as e: - self.send_custom_error(str(e)) - return - - response["port_id"] = request["port_id"] - self.send_response(response) - - def _check_for_privileged_access(self, device): - """ - Check if VPCS can access Ethernet and TAP devices. - - :param device: device name - """ - - # we are root, so VPCS should have privileged access too - if os.geteuid() == 0: - return - - # test if VPCS has the CAP_NET_RAW capability - if "security.capability" in os.listxattr(self._VPCS): - try: - caps = os.getxattr(self._VPCS, "security.capability") - # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set - if struct.unpack(". - -""" -Interface for TAP NIOs (UNIX based OSes only). -""" - - -class NIO_TAP(object): - """ - IOU TAP NIO. - - :param tap_device: TAP device name (e.g. tap0) - """ - - def __init__(self, tap_device): - - self._tap_device = tap_device - - @property - def tap_device(self): - """ - Returns the TAP device used by this NIO. - - :returns: the TAP device name - """ - - return self._tap_device - - def __str__(self): - - return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_tap.py~~ b/gns3server/modules/vpcs/nios/nio_tap.py~~ deleted file mode 100644 index ee550e7b..00000000 --- a/gns3server/modules/vpcs/nios/nio_tap.py~~ +++ /dev/null @@ -1,46 +0,0 @@ -# -*- 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 TAP NIOs (UNIX based OSes only). -""" - - -class NIO_TAP(object): - """ - IOU TAP NIO. - - :param tap_device: TAP device name (e.g. tap0) - """ - - def __init__(self, tap_device): - - self._tap_device = tap_device - - @property - def tap_device(self): - """ - Returns the TAP device used by this NIO. - - :returns: the TAP device name - """ - - return self._tap_device - - def __str__(self): - - return "NIO TAP" diff --git a/gns3server/modules/vpcs/nios/nio_udp.py~ b/gns3server/modules/vpcs/nios/nio_udp.py~ deleted file mode 100644 index 3142d70e..00000000 --- a/gns3server/modules/vpcs/nios/nio_udp.py~ +++ /dev/null @@ -1,72 +0,0 @@ -# -*- 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. -""" - - -class NIO_UDP(object): - """ - IOU 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): - - 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/vpcs/schemas.py~ b/gns3server/modules/vpcs/schemas.py~ deleted file mode 100644 index 24fcce15..00000000 --- a/gns3server/modules/vpcs/schemas.py~ +++ /dev/null @@ -1,306 +0,0 @@ -# -*- 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 . - - -VPCS_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new VPCS instance", - "type": "object", - "properties": { - "name": { - "description": "VPCS device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the VPCS executable", - "type": "string", - "minLength": 1, - } - }, - "required": ["path"] -} - -VPCS_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "name": { - "description": "VPCS device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the VPCS executable", - "type": "string", - "minLength": 1, - }, - "startup_config": { - "description": "path to the VPCS startup configuration file", - "type": "string", - "minLength": 1, - }, - "startup_config_base64": { - "description": "startup configuration base64 encoded", - "type": "string" - }, - }, - "required": ["id"] -} - -VPCS_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -VPCS_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VPCS instance", - "type": "integer" - }, - }, - "required": ["id", "port_id"] -} - -VPCS_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an VPCS 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": "VPCS device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VPCS instance", - "type": "integer" - }, - "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"}, - ] - }, - }, - "required": ["id", "port_id", "nio"] -} - -VPCS_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an VPCS instance", - "type": "object", - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} diff --git a/gns3server/modules/vpcs/vpcs_device.py~ b/gns3server/modules/vpcs/vpcs_device.py~ deleted file mode 100644 index 76756c62..00000000 --- a/gns3server/modules/vpcs/vpcs_device.py~ +++ /dev/null @@ -1,471 +0,0 @@ -# -*- 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 . - -""" -VPCS device management (creates command line, processes, files etc.) in -order to run an VPCS instance. -""" - -import os -import re -import signal -import subprocess -import argparse -import threading -import configparser -from .vpcscon import start_vpcscon -from .vpcs_error import VPCSError -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP - -import logging -log = logging.getLogger(__name__) - - -class VPCSDevice(object): - """ - VPCS device implementation. - - :param path: path to VPCS executable - :param working_dir: path to a working directory - :param host: host/address to bind for console and UDP connections - :param name: name of this VPCS device - """ - - _instances = [] - - def __init__(self, path, working_dir, host="127.0.0.1", name=None): - - # find an instance identifier (0 <= id < 255) - # This 255 limit is due to a restriction on the number of possible - # mac addresses given in VPCS using the -m option - self._id = 0 - for identifier in range(0, 255): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise VPCSError("Maximum number of VPCS instances reached") - - if name: - self._name = name - else: - self._name = "VPCS{}".format(self._id) - self._path = path - self._console = None - self._working_dir = None - self._command = [] - self._process = None - self._vpcs_stdout_file = "" - self._vpcscon_thead = None - self._vpcscon_thread_stop_event = None - self._host = host - self._started = False - - # VPCS settings - self._script_file = "" - - # update the working directory - self.working_dir = working_dir - - log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - def defaults(self): - """ - Returns all the default attribute values for VPCS. - - :returns: default values (dictionary) - """ - - vpcs_defaults = {"name": self._name, - "path": self._path, - "script_file": self._script_file, - "console": self._console} - - return vpcs_defaults - - @property - def id(self): - """ - Returns the unique ID for this VPCS device. - - :returns: id (integer) - """ - - return(self._id) - - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ - - cls._instances.clear() - - @property - def name(self): - """ - Returns the name of this VPCS device. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this VPCS device. - - :param new_name: name - """ - - self._name = new_name - log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - - @property - def path(self): - """ - Returns the path to the VPCS executable. - - :returns: path to VPCS - """ - - return(self._path) - - @path.setter - def path(self, path): - """ - Sets the path to the VPCS executable. - - :param path: path to VPCS - """ - - self._path = path - log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, - id=self._id, - path=path)) - - @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 for VPCS. - - :param working_dir: path to the working directory - """ - - # create our own working directory - working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id)) - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("VPCS {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) - """ - - self._console = console - log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - - def command(self): - """ - Returns the VPCS command line. - - :returns: VPCS command line (string) - """ - - return " ".join(self._build_command()) - - def delete(self): - """ - Deletes this VPCS device. - """ - - self.stop() - self._instances.remove(self._id) - log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - @property - def started(self): - """ - Returns either this VPCS device has been started or not. - - :returns: boolean - """ - - return self._started - - def _start_vpcscon(self): - """ - Starts vpcscon thread (for console connections). - """ - - if not self._vpcscon_thead: - telnet_server = "{}:{}".format(self._host, self._console) - log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) - args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) - self._vpcscon_thread_stop_event = threading.Event() - self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) - self._vpcscon_thead.start() ", ".join(missing_libs))) - - def start(self): - """ - Starts the VPCS process. - """ - - if not self.is_running(): - - if not os.path.isfile(self._path): - raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) - - if not os.access(self._path, os.X_OK): - raise VPCSError("VPCS image '{}' is not executable".format(self._path)) - - self._command = self._build_command() - try: - log.info("starting VPCS: {}".format(self._command)) - self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") - log.info("logging to {}".format(self._vpcs_stdout_file)) - with open(self._vpcs_stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) - log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) - self._started = True - except FileNotFoundError as e: - raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e)) - except OSError as e: - vpcs_stdout = self.read_vpcs_stdout() - log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - - # start console support - self._start_vpcscon() - - def stop(self): - """ - Stops the VPCS process. - """ - - # stop the VPCS process - if self.is_running(): - log.info("stopping VPCS 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() == None: - log.warn("VPCS instance {} PID={} is still running".format(self._id, - self._process.pid)) - self._process = None - self._started = False - - # stop console support - if self._vpcscon_thead: - self._vpcscon_thread_stop_event.set() - if self._vpcscon_thead.is_alive(): - self._vpcscon_thead.join(timeout=0.10) - self._vpcscon_thead = None - - - def read_vpcs_stdout(self): - """ - Reads the standard output of the VPCS process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._vpcs_stdout_file: - try: - with open(self._vpcs_stdout_file) as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) - return output - - def is_running(self): - """ - Checks if the VPCS process is running - - :returns: True or False - """ - - if self._process and self._process.poll() == None: - return True - return False - - - def slot_add_nio_binding(self, slot_id, port_id, nio): - """ - Adds a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - :param nio: NIO instance to add to the slot/port - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - adapter.add_nio(port_id, nio) - log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - - def slot_remove_nio_binding(self, slot_id, port_id): - """ - Removes a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - - def _build_command(self): - """ - Command to start the VPCS process. - (to be passed to subprocess.Popen()) - - VPCS command line: - usage: vpcs [options] [scriptfile] - Option: - -h print this help then exit - -v print version information then exit - - -p port run as a daemon listening on the tcp 'port' - -m num start byte of ether address, default from 0 - -r file load and execute script file - compatible with older versions, DEPRECATED. - - -e tap mode, using /dev/tapx (linux only) - -u udp mode, default - - udp mode options: - -s port local udp base port, default from 20000 - -c port remote udp base port (dynamips udp port), default from 30000 - -t ip remote host IP, default 127.0.0.1 - - hypervisor mode option: - -H port run as the hypervisor listening on the tcp 'port' - - If no 'scriptfile' specified, vpcs will read and execute the file named - 'startup.vpc' if it exsits in the current directory. - - """ - - command = [self._path] - command.extend(["-p", str(self._tcpport)]) - command.extend(["-s", str(self._lport)]) - command.extend(["-c", str(self._rport)]) - command.extend(["-t", str(self._rhost)]) - command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset - if self._script_file: - command.extend([self._script_file]) - return command - - @property - def script_file(self): - """ - Returns the startup-config for this VPCS instance. - - :returns: path to startup-config file - """ - - return self._script_file - - @script_file.setter - def script_file(self, script_file): - """ - Sets the startup-config for this VPCS instance. - - :param script_file: path to startup-config file - """ - - self._script_file = script_file - log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, - id=self._id, - config=self._script_file)) - - diff --git a/gns3server/modules/vpcs/vpcs_error.py~ b/gns3server/modules/vpcs/vpcs_error.py~ deleted file mode 100644 index 33ed081c..00000000 --- a/gns3server/modules/vpcs/vpcs_error.py~ +++ /dev/null @@ -1,39 +0,0 @@ -# -*- 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 . - -""" -Custom exceptions for VPCS module. -""" - - -class IOUError(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/vpcs/vpcscon.py~ b/gns3server/modules/vpcs/vpcscon.py~ deleted file mode 100644 index 138b61e7..00000000 --- a/gns3server/modules/vpcs/vpcscon.py~ +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013, 2014 James E. Carpenter -# -# 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 socket -import sys -import os -import select -import fcntl -import struct -import termios -import tty -import time -import argparse -import traceback - - -import logging -log = logging.getLogger(__name__) - - -# Escape characters -ESC_CHAR = '^^' # can be overriden from command line -ESC_QUIT = 'q' - -# IOU seems to only send *1* byte at a time. If -# they ever fix that we'll be ready for it. -BUFFER_SIZE = 1024 - -# How long to wait before retrying a connection (seconds) -RETRY_DELAY = 3 - -# How often to test an idle connection (seconds) -POLL_TIMEOUT = 3 - - -EXIT_SUCCESS = 0 -EXIT_FAILURE = 1 -EXIT_ABORT = 2 - -# Mostly from: -# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py -#--[ Telnet Commands ]--------------------------------------------------------- -SE = 240 # End of subnegotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command -SEND = 1 # Sub-process negotiation SEND command -IS = 0 # Sub-process negotiation IS command -#--[ Telnet Options ]---------------------------------------------------------- -BINARY = 0 # Transmit Binary -ECHO = 1 # Echo characters back to sender -RECON = 2 # Reconnection -SGA = 3 # Suppress Go-Ahead -TMARK = 6 # Timing Mark -TTYPE = 24 # Terminal Type -NAWS = 31 # Negotiate About Window Size -LINEMO = 34 # Line Mode - - -class FileLock: - - # struct flock { /* from fcntl(2) */ - # ... - # short l_type; /* Type of lock: F_RDLCK, - # F_WRLCK, F_UNLCK */ - # short l_whence; /* How to interpret l_start: - # SEEK_SET, SEEK_CUR, SEEK_END */ - # off_t l_start; /* Starting offset for lock */ - # off_t l_len; /* Number of bytes to lock */ - # pid_t l_pid; /* PID of process blocking our lock - # (F_GETLK only) */ - # ... - # }; - _flock = struct.Struct('hhqql') - - def __init__(self, fname=None): - self.fd = None - self.fname = fname - - def get_lock(self): - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, - 0, 0, os.getpid()) - flk = self._flock.unpack( - fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) - - # If it's not locked (or is locked by us) then return None, - # otherwise return the PID of the owner. - if flk[0] == fcntl.F_UNLCK: - return None - return flk[4] - - def lock(self): - try: - self.fd = open('{}.lck'.format(self.fname), 'a') - except Exception as e: - raise LockError("Couldn't get lock on {}: {}" - .format(self.fname, e)) - - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) - try: - fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) - except BlockingIOError: - raise LockError("Already connected. PID {} has lock on {}" - .format(self.get_lock(), self.fname)) - - # If we got here then we must have the lock. Store the PID. - self.fd.truncate(0) - self.fd.write('{}\n'.format(os.getpid())) - self.fd.flush() - - def unlock(self): - if self.fd: - # Deleting first prevents a race condition - os.unlink(self.fd.name) - self.fd.close() - - def __enter__(self): - self.lock() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.unlock() - return False - - -class Console: - def fileno(self): - raise NotImplementedError("Only routers have fileno()") - - -class Router: - pass - - -class TTY(Console): - - def read(self, fileno, bufsize): - return self.fd.read(bufsize) - - def write(self, buf): - return self.fd.write(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def __enter__(self): - try: - self.fd = open('/dev/tty', 'r+b', buffering=0) - except OSError as e: - raise TTYError("Couldn't open controlling TTY: {}".format(e)) - - # Save original flags - self.termios = termios.tcgetattr(self.fd) - self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) - - # Update flags - tty.setraw(self.fd, termios.TCSANOW) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - - # Restore flags to original settings - termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) - - self.fd.close() - - return False - - -class TelnetServer(Console): - - def __init__(self, addr, port, stop_event): - self.addr = addr - self.port = port - self.fd_dict = {} - self.stop_event = stop_event - - def read(self, fileno, bufsize): - # Someone wants to connect? - if fileno == self.sock_fd.fileno(): - self._accept() - return None - - self._cur_fileno = fileno - - # Read a maximum of _bufsize_ bytes without blocking. When it - # would want to block it means there's no more data. An empty - # buffer normally means that we've been disconnected. - try: - buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) - except BlockingIOError: - return None - if not buf: - self._disconnect(fileno) - - # Process and remove any telnet commands from the buffer - if IAC in buf: - buf = self._IAC_parser(buf) - - return buf - - def write(self, buf): - for fd in self.fd_dict.values(): - fd.send(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.sock_fd, select.EPOLLIN) - - def _read_block(self, bufsize): - buf = self._read_cur(bufsize, socket.MSG_WAITALL) - # If we don't get everything we were looking for then the - # client probably disconnected. - if len(buf) < bufsize: - self._disconnect(self._cur_fileno) - return buf - - def _read_cur(self, bufsize, flags): - return self.fd_dict[self._cur_fileno].recv(bufsize, flags) - - def _write_cur(self, buf): - return self.fd_dict[self._cur_fileno].send(buf) - - def _IAC_parser(self, buf): - skip_to = 0 - while not self.stop_event.is_set(): - # Locate an IAC to process - iac_loc = buf.find(IAC, skip_to) - if iac_loc < 0: - break - - # Get the TELNET command - iac_cmd = bytearray([IAC]) - try: - iac_cmd.append(buf[iac_loc + 1]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 1]) - - # Is this just a 2-byte TELNET command? - if iac_cmd[1] not in [WILL, WONT, DO, DONT]: - if iac_cmd[1] == AYT: - log.debug("Telnet server received Are-You-There (AYT)") - self._write_cur( - b'\r\nYour Are-You-There received. I am here.\r\n' - ) - elif iac_cmd[1] == IAC: - # It's data, not an IAC - iac_cmd.pop() - # This prevents the 0xff from being - # interputed as yet another IAC - skip_to = iac_loc + 1 - log.debug("Received IAC IAC") - elif iac_cmd[1] == NOP: - pass - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x}".format(*iac_cmd)) - - # This must be a 3-byte TELNET command - else: - try: - iac_cmd.append(buf[iac_loc + 2]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 2]) - # We do ECHO, SGA, and BINARY. Period. - if (iac_cmd[1] == DO - and iac_cmd[2] not in [ECHO, SGA, BINARY]): - - self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) - - # Remove the entire TELNET command from the buffer - buf = buf.replace(iac_cmd, b'', 1) - - # Return the new copy of the buffer, minus telnet commands - return buf - - def _accept(self): - fd, addr = self.sock_fd.accept() - self.fd_dict[fd.fileno()] = fd - self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) - - log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) - - # This is a one-way negotiation. This is very basic so there - # shouldn't be any problems with any decent client. - fd.send(bytes([IAC, WILL, ECHO, - IAC, WILL, SGA, - IAC, WILL, BINARY, - IAC, DO, BINARY])) - - if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: - fd.send(b'\r\nToo many connections\r\n') - self._disconnect(fd.fileno()) - log.warn("Client disconnected because of too many connections. " - "(limit currently {})".format(args.telnet_limit)) - - def _disconnect(self, fileno): - fd = self.fd_dict.pop(fileno) - log.info("Telnet client disconnected") - fd.shutdown(socket.SHUT_RDWR) - fd.close() - - def __enter__(self): - # Open a socket and start listening - sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock_fd.bind((self.addr, self.port)) - except OSError: - raise TelnetServerError("Cannot bind to {}:{}" - .format(self.addr, self.port)) - - sock_fd.listen(socket.SOMAXCONN) - self.sock_fd = sock_fd - log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - for fileno in list(self.fd_dict.keys()): - self._disconnect(fileno) - self.sock_fd.close() - return False - - -class IOU(Router): - - def __init__(self, ttyC, ttyS, stop_event): - self.ttyC = ttyC - self.ttyS = ttyS - self.stop_event = stop_event - - def read(self, bufsize): - try: - buf = self.fd.recv(bufsize) - except BlockingIOError: - return None - return buf - - def write(self, buf): - self.fd.send(buf) - - def _open(self): - self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - self.fd.setblocking(False) - - def _bind(self): - try: - os.unlink(self.ttyC) - except FileNotFoundError: - pass - except Exception as e: - raise NetioError("Couldn't unlink socket {}: {}" - .format(self.ttyC, e)) - - try: - self.fd.bind(self.ttyC) - except Exception as e: - raise NetioError("Couldn't create socket {}: {}" - .format(self.ttyC, e)) - - def _connect(self): - # Keep trying until we connect or die trying - while not self.stop_event.is_set(): - try: - self.fd.connect(self.ttyS) - except FileNotFoundError: - log.debug("Waiting to connect to {}".format(self.ttyS)) - time.sleep(RETRY_DELAY) - except Exception as e: - raise NetioError("Couldn't connect to socket {}: {}" - .format(self.ttyS, e)) - else: - break - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def fileno(self): - return self.fd.fileno() - - def __enter__(self): - self._open() - self._bind() - self._connect() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - os.unlink(self.ttyC) - self.fd.close() - return False - - -class IOUConError(Exception): - pass - - -class LockError(IOUConError): - pass - - -class NetioError(IOUConError): - pass - - -class TTYError(IOUConError): - pass - - -class TelnetServerError(IOUConError): - pass - - -class ConfigError(IOUConError): - pass - - -def mkdir_netio(netio_dir): - try: - os.mkdir(netio_dir) - except FileExistsError: - pass - except Exception as e: - raise NetioError("Couldn't create directory {}: {}" - .format(netio_dir, e)) - - -def send_recv_loop(console, router, esc_char, stop_event): - - epoll = select.epoll() - router.register(epoll) - console.register(epoll) - - router_fileno = router.fileno() - esc_quit = bytes(ESC_QUIT.upper(), 'ascii') - esc_state = False - - while not stop_event.is_set(): - event_list = epoll.poll(timeout=POLL_TIMEOUT) - - # When/if the poll times out we send an empty datagram. If IOU - # has gone away then this will toss a ConnectionRefusedError - # exception. - if not event_list: - router.write(b'') - continue - - for fileno, event in event_list: - buf = bytearray() - - # IOU --> tty(s) - if fileno == router_fileno: - while not stop_event.is_set(): - data = router.read(BUFFER_SIZE) - if not data: - break - buf.extend(data) - console.write(buf) - - # tty --> IOU - else: - while not stop_event.is_set(): - data = console.read(fileno, BUFFER_SIZE) - if not data: - break - buf.extend(data) - - # If we just received the escape character then - # enter the escape state. - # - # If we are in the escape state then check for a - # quit command. Or if it's the escape character then - # send the escape character. Else, send the escape - # character we ate earlier and whatever character we - # just got. Exit escape state. - # - # If we're not in the escape state and this isn't an - # escape character then just send it to IOU. - if esc_state: - if buf.upper() == esc_quit: - sys.exit(EXIT_SUCCESS) - elif buf == esc_char: - router.write(esc_char) - else: - router.write(esc_char) - router.write(buf) - esc_state = False - elif buf == esc_char: - esc_state = True - else: - router.write(buf) - - -def get_args(): - parser = argparse.ArgumentParser( - description='Connect to an IOU console port.') - parser.add_argument('-d', '--debug', action='store_true', - help='display some debugging information') - parser.add_argument('-e', '--escape', - help='set escape character (default: %(default)s)', - default=ESC_CHAR, metavar='CHAR') - parser.add_argument('-t', '--telnet-server', - help='start telnet server listening on ADDR:PORT', - metavar='ADDR:PORT', default=False) - parser.add_argument('-l', '--telnet-limit', - help='maximum number of simultaneous ' - 'telnet connections (default: %(default)s)', - metavar='LIMIT', type=int, default=1) - parser.add_argument('appl_id', help='IOU instance identifier') - return parser.parse_args() - - -def get_escape_character(escape): - - # Figure out the escape character to use. - # Can be any ASCII character or a spelled out control - # character, like "^e". The string "none" disables it. - if escape.lower() == 'none': - esc_char = b'' - elif len(escape) == 2 and escape[0] == '^': - c = ord(escape[1].upper()) - 0x40 - if not 0 <= c <= 0x1f: # control code range - raise ConfigError("Invalid control code") - esc_char = bytes([c]) - elif len(escape) == 1: - try: - esc_char = bytes(escape, 'ascii') - except ValueError as e: - raise ConfigError("Invalid escape character") from e - else: - raise ConfigError("Invalid length for escape character") - - return esc_char - - -def start_ioucon(cmdline_args, stop_event): - - global args - args = cmdline_args - - if args.debug: - logging.basicConfig(level=logging.DEBUG) - else: - # default logging level - logging.basicConfig(level=logging.INFO) - - # Create paths for the Unix domain sockets - netio = '/tmp/netio{}'.format(os.getuid()) - ttyC = '{}/ttyC{}'.format(netio, args.appl_id) - ttyS = '{}/ttyS{}'.format(netio, args.appl_id) - - try: - mkdir_netio(netio) - with FileLock(ttyC): - esc_char = get_escape_character(args.escape) - - if args.telnet_server: - addr, _, port = args.telnet_server.partition(':') - nport = 0 - try: - nport = int(port) - except ValueError: - pass - if (addr == '' or nport == 0): - raise ConfigError('format for --telnet-server must be ' - 'ADDR:PORT (like 127.0.0.1:20000)') - - while not stop_event.is_set(): - try: - if args.telnet_server: - with TelnetServer(addr, nport, stop_event) as console: - with IOU(ttyC, ttyS, stop_event) as router: - send_recv_loop(console, router, b'', stop_event) - else: - with IOU(ttyC, ttyS, stop_event) as router, TTY() as console: - send_recv_loop(console, router, esc_char, stop_event) - except ConnectionRefusedError: - pass - except KeyboardInterrupt: - sys.exit(EXIT_ABORT) - finally: - # Put us at the beginning of a line - if not args.telnet_server: - print() - - except IOUConError as e: - if args.debug: - traceback.print_exc(file=sys.stderr) - else: - print(e, file=sys.stderr) - sys.exit(EXIT_FAILURE) - - log.info("exiting...") - - -def main(): - - import threading - stop_event = threading.Event() - args = get_args() - start_ioucon(args, stop_event) - -if __name__ == '__main__': - main() From 81b11403dbb7e88f3576cb18805d0426ce012e4d Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:09:11 -0600 Subject: [PATCH 07/15] Update file structure --- gns3server/modules/iou/schemas.py~ | 364 ----------------------------- 1 file changed, 364 deletions(-) delete mode 100644 gns3server/modules/iou/schemas.py~ diff --git a/gns3server/modules/iou/schemas.py~ b/gns3server/modules/iou/schemas.py~ deleted file mode 100644 index 62ac0ec4..00000000 --- a/gns3server/modules/iou/schemas.py~ +++ /dev/null @@ -1,364 +0,0 @@ -# -*- 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 . - - -IOU_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new IOU instance", - "type": "object", - "properties": { - "name": { - "description": "IOU device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the IOU executable", - "type": "string", - "minLength": 1, - } - }, - "required": ["path"] -} - -IOU_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -IOU_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "name": { - "description": "IOU device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the IOU executable", - "type": "string", - "minLength": 1, - }, - "startup_config": { - "description": "path to the IOU startup configuration file", - "type": "string", - "minLength": 1, - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "nvram": { - "description": "amount of NVRAM in KB", - "type": "integer" - }, - "ethernet_adapters": { - "description": "number of Ethernet adapters", - "type": "integer", - "minimum": 0, - "maximum": 16, - }, - "serial_adapters": { - "description": "number of serial adapters", - "type": "integer", - "minimum": 0, - "maximum": 16, - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "startup_config_base64": { - "description": "startup configuration base64 encoded", - "type": "string" - }, - }, - "required": ["id"] -} - -IOU_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -IOU_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -IOU_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "required": ["id"] -} - -IOU_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - }, - "required": ["id", "port_id"] -} - -IOU_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an IOU 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": "IOU device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "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"}, - ] - }, - }, - "required": ["id", "port_id", "slot", "port", "nio"] -} - - -IOU_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - }, - "required": ["id", "slot", "port"] -} From 76982ddbbb2eee2ab2ae2f09140e534365a1b20f Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:11:14 -0600 Subject: [PATCH 08/15] Update file structure --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 49bac9bd..7499c81b 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ nosetests.xml .project .pydevproject .settings + +# Gedit Backup Files +*~ From 0fc019da03d72867cb8a0149c29976791444cf84 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:25:05 -0600 Subject: [PATCH 09/15] Setup NIO for UDP communication --- gns3server/modules/vpcs/adapters/__init__.py | 0 gns3server/modules/vpcs/adapters/adapter.py | 104 ++++++++++++++++++ .../modules/vpcs/adapters/ethernet_adapter.py | 31 ++++++ gns3server/modules/vpcs/vpcs_device.py | 17 ++- 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 gns3server/modules/vpcs/adapters/__init__.py create mode 100644 gns3server/modules/vpcs/adapters/adapter.py create mode 100644 gns3server/modules/vpcs/adapters/ethernet_adapter.py diff --git a/gns3server/modules/vpcs/adapters/__init__.py b/gns3server/modules/vpcs/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/vpcs/adapters/adapter.py b/gns3server/modules/vpcs/adapters/adapter.py new file mode 100644 index 00000000..4d2f4053 --- /dev/null +++ b/gns3server/modules/vpcs/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=4): + + 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/vpcs/adapters/ethernet_adapter.py b/gns3server/modules/vpcs/adapters/ethernet_adapter.py new file mode 100644 index 00000000..bbca7f40 --- /dev/null +++ b/gns3server/modules/vpcs/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): + """ + VPCS Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=1) + + def __str__(self): + + return "VPCS Ethernet adapter" diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 3bd3f95a..88f07f85 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -29,6 +29,7 @@ import threading import configparser from .vpcscon import start_vpcscon from .vpcs_error import VPCSError +from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP @@ -80,6 +81,8 @@ class VPCSDevice(object): # VPCS settings self._script_file = "" + self._ethernet_adapters = [EthernetAdapter()] # one adapter = 1 interfaces + self._slots = self._ethernet_adapters # update the working directory self.working_dir = working_dir @@ -437,9 +440,17 @@ class VPCSDevice(object): command = [self._path] command.extend(["-p", str(self._console)]) - command.extend(["-s", str(self._lport)]) - command.extend(["-c", str(self._rport)]) - command.extend(["-t", str(self._rhost)]) + + for adapter in self._slots: + for unit in adapter.ports.keys(): + nio = adapter.get_nio(unit) + if nio: + if isinstance(nio, NIO_UDP): + # UDP tunnel + command.extend(["-s", str(nio.lport)]) + command.extend(["-c", str(nio.rport)]) + command.extend(["-t", str(nio.rhost)]) + command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset if self._script_file: command.extend([self._script_file]) From a50c4c112e6a98569c4a28e0d2b606f716b4732c Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:26:34 -0600 Subject: [PATCH 10/15] Removed redundant definition --- gns3server/modules/iou/schemas.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/gns3server/modules/iou/schemas.py b/gns3server/modules/iou/schemas.py index 62ac0ec4..62b35747 100644 --- a/gns3server/modules/iou/schemas.py +++ b/gns3server/modules/iou/schemas.py @@ -313,13 +313,6 @@ IOU_ADD_NIO_SCHEMA = { "minimum": 0, "maximum": 3 }, - - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, "nio": { "type": "object", "description": "Network Input/Output", From 476a3c42b6a279fd342379acb482cdfa876370cf Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:42:38 -0600 Subject: [PATCH 11/15] Added NIO TAP support --- gns3server/modules/vpcs/__init__.py | 7 ------ gns3server/modules/vpcs/vpcs_device.py | 6 ++++- tests/vpcs/test_vpcs_device.py | 32 +++++++++++++------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 251cb280..81209123 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -182,13 +182,6 @@ class VPCS(IModule): self._current_console_port = self._console_start_port_range self._current_udp_port = self._udp_start_port_range - if self._VPCSrc and os.path.isfile(self._VPCSrc): - try: - log.info("deleting VPCSrc file {}".format(self._VPCSrc)) - os.remove(self._VPCSrc) - except OSError as e: - log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e)) - log.info("VPCS module has been reset") @IModule.route("VPCS.settings") diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 88f07f85..bec99df4 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -450,7 +450,11 @@ class VPCSDevice(object): command.extend(["-s", str(nio.lport)]) command.extend(["-c", str(nio.rport)]) command.extend(["-t", str(nio.rhost)]) - + + elif isinstance(nio, NIO_TAP): + # TAP interface + command.extend(["-e"]) #, str(nio.tap_device)]) #TODO: Fix, currently vpcs doesn't allow specific tap_device + command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset if self._script_file: command.extend([self._script_file]) diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py index 0749c97f..13609506 100644 --- a/tests/vpcs/test_vpcs_device.py +++ b/tests/vpcs/test_vpcs_device.py @@ -1,29 +1,29 @@ -from gns3server.modules.iou import IOUDevice +from gns3server.modules.vpcs import VPCSDevice import os import pytest @pytest.fixture(scope="session") -def iou(request): +def vpcs(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.start() - request.addfinalizer(iou_device.delete) - return iou_device + vpcs_path = os.path.join(cwd, "vpcs") + vpcs_device = VPCSDevice(vpcs_path, "/tmp") + vpcs_device.start() + request.addfinalizer(vpcs_device.delete) + return vpcs_device -def test_iou_is_started(iou): +def test_vpcs_is_started(vpcs): - print(iou.command()) - assert iou.id == 1 # we should have only one IOU running! - assert iou.is_running() + print(vpcs.command()) + assert vpcs.id == 1 # we should have only one VPCS running! + assert vpcs.is_running() -def test_iou_restart(iou): +def test_vpcs_restart(vpcs): - iou.stop() - assert not iou.is_running() - iou.start() - assert iou.is_running() + vpcs.stop() + assert not vpcs.is_running() + vpcs.start() + assert vpcs.is_running() From bbce6b2c5c2da384fbb4cdbad62ddb0385fbed01 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 6 May 2014 10:52:34 -0600 Subject: [PATCH 12/15] Added NIO TAP support --- gns3server/modules/vpcs/vpcs_device.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index bec99df4..5581a952 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -456,6 +456,7 @@ class VPCSDevice(object): command.extend(["-e"]) #, str(nio.tap_device)]) #TODO: Fix, currently vpcs doesn't allow specific tap_device command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset + command.extend(["-i", str(1)]) #Option to start only one pc instance if self._script_file: command.extend([self._script_file]) return command @@ -463,9 +464,9 @@ class VPCSDevice(object): @property def script_file(self): """ - Returns the startup-config for this VPCS instance. + Returns the script-file for this VPCS instance. - :returns: path to startup-config file + :returns: path to script-file file """ return self._script_file @@ -473,9 +474,9 @@ class VPCSDevice(object): @script_file.setter def script_file(self, script_file): """ - Sets the startup-config for this VPCS instance. + Sets the script-file for this VPCS instance. - :param script_file: path to startup-config file + :param script_file: path to script-file file """ self._script_file = script_file From 588ee8eed000bf85577419713524fac738ae1084 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Mon, 12 May 2014 08:16:37 -0600 Subject: [PATCH 13/15] Changed the way vpcs closes by using the socket to send quit message instead of pid kill --- gns3server/modules/__init__.py | 2 + gns3server/modules/vpcs/__init__.py | 58 +-- gns3server/modules/vpcs/vpcs_device.py | 60 +-- gns3server/modules/vpcs/vpcscon.py | 642 ------------------------- vpcs.hist | 5 + 5 files changed, 58 insertions(+), 709 deletions(-) delete mode 100644 gns3server/modules/vpcs/vpcscon.py create mode 100644 vpcs.hist diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 59304d19..7fd76401 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,8 +18,10 @@ import sys from .base import IModule from .dynamips import Dynamips +from .vpcs import VPCS MODULES = [Dynamips] +MODULES.append(VPCS) if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 81209123..d532ce98 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -65,17 +65,17 @@ class VPCS(IModule): # get the VPCS location config = Config.instance() VPCS_config = config.get_section_config(name.upper()) - self._VPCS = VPCS_config.get("VPCS") + self._VPCS = VPCS_config.get("vpcs") if not self._VPCS or not os.path.isfile(self._VPCS): - VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") + VPCS_in_cwd = os.path.join(os.getcwd(), "vpcs") if os.path.isfile(VPCS_in_cwd): self._VPCS = VPCS_in_cwd else: # look for VPCS if none is defined or accessible for path in os.environ["PATH"].split(":"): try: - if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): - self._VPCS = os.path.join(path, "VPCS") + if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK): + self._VPCS = os.path.join(path, "vpcs") break except OSError: continue @@ -102,8 +102,8 @@ class VPCS(IModule): self._VPCSrc = "" # check every 5 seconds - self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) - self._VPCS_callback.start() + #self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) + #self._VPCS_callback.start() def stop(self, signum=None): """ @@ -135,12 +135,12 @@ class VPCS(IModule): "id": VPCS_id, "name": VPCS_instance.name} if not VPCS_instance.is_running(): - stdout = VPCS_instance.read_VPCS_stdout() + stdout = VPCS_instance.read_vpcs_stdout() notification["message"] = "VPCS has stopped running" notification["details"] = stdout self.send_notification("{}.VPCS_stopped".format(self.name), notification) elif not VPCS_instance.is_VPCS_running(): - stdout = VPCS_instance.read_VPCS_stdout() + stdout = VPCS_instance.read_vpcs_stdout() notification["message"] = "VPCS has stopped running" notification["details"] = stdout self.send_notification("{}.VPCS_stopped".format(self.name), notification) @@ -161,7 +161,7 @@ class VPCS(IModule): return None return self._VPCS_instances[VPCS_id] - @IModule.route("VPCS.reset") + @IModule.route("vpcs.reset") def reset(self, request): """ Resets the module. @@ -184,7 +184,7 @@ class VPCS(IModule): log.info("VPCS module has been reset") - @IModule.route("VPCS.settings") + @IModule.route("vpcs.settings") def settings(self, request): """ Set or update settings. @@ -247,7 +247,7 @@ class VPCS(IModule): return {"result": result, "message": message} - @IModule.route("VPCS.test_settings") + @IModule.route("vpcs.test_settings") def test_settings(self, request): """ """ @@ -256,7 +256,7 @@ class VPCS(IModule): self.send_response(response) - @IModule.route("VPCS.create") + @IModule.route("vpcs.create") def VPCS_create(self, request): """ Creates a new VPCS instance. @@ -313,7 +313,7 @@ class VPCS(IModule): self._VPCS_instances[VPCS_instance.id] = VPCS_instance self.send_response(response) - @IModule.route("VPCS.delete") + @IModule.route("vpcs.delete") def VPCS_delete(self, request): """ Deletes an VPCS instance. @@ -345,7 +345,7 @@ class VPCS(IModule): self.send_response(True) - @IModule.route("VPCS.update") + @IModule.route("vpcs.update") def VPCS_update(self, request): """ Updates an VPCS instance @@ -355,7 +355,7 @@ class VPCS(IModule): Optional request parameters: - any setting to update - - startup_config_base64 (startup-config base64 encoded) + - script_file_base64 (script-file base64 encoded) Response parameters: - updated settings @@ -374,20 +374,20 @@ class VPCS(IModule): response = {} try: - # a new startup-config has been pushed - if "startup_config_base64" in request: - config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + # a new script-file has been pushed + if "script_file_base64" in request: + config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8") config = "!\n" + config.replace("\r", "") config = config.replace('%h', VPCS_instance.name) - config_path = os.path.join(VPCS_instance.working_dir, "startup-config") + config_path = os.path.join(VPCS_instance.working_dir, "script-file") try: with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(config_path)) + log.info("saving script-file to {}".format(config_path)) f.write(config) except OSError as e: raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) - # update the request with the new local startup-config path - request["startup_config"] = os.path.basename(config_path) + # update the request with the new local script-file path + request["script_file"] = os.path.basename(config_path) except VPCSError as e: self.send_custom_error(str(e)) @@ -405,7 +405,7 @@ class VPCS(IModule): self.send_response(response) - @IModule.route("VPCS.start") + @IModule.route("vpcs.start") def vm_start(self, request): """ Starts an VPCS instance. @@ -438,7 +438,7 @@ class VPCS(IModule): return self.send_response(True) - @IModule.route("VPCS.stop") + @IModule.route("vpcs.stop") def vm_stop(self, request): """ Stops an VPCS instance. @@ -468,7 +468,7 @@ class VPCS(IModule): return self.send_response(True) - @IModule.route("VPCS.reload") + @IModule.route("vpcs.reload") def vm_reload(self, request): """ Reloads an VPCS instance. @@ -500,7 +500,7 @@ class VPCS(IModule): return self.send_response(True) - @IModule.route("VPCS.allocate_udp_port") + @IModule.route("vpcs.allocate_udp_port") def allocate_udp_port(self, request): """ Allocates a UDP port in order to create an UDP NIO. @@ -573,7 +573,7 @@ class VPCS(IModule): raise VPCSError("{} has no privileged access to {}.".format(self._VPCS, device)) - @IModule.route("VPCS.add_nio") + @IModule.route("vpcs.add_nio") def add_nio(self, request): """ Adds an NIO (Network Input/Output) for an VPCS instance. @@ -633,7 +633,7 @@ class VPCS(IModule): self.send_response({"port_id": request["port_id"]}) - @IModule.route("VPCS.delete_nio") + @IModule.route("vpcs.delete_nio") def delete_nio(self, request): """ Deletes an NIO (Network Input/Output). @@ -668,7 +668,7 @@ class VPCS(IModule): self.send_response(True) - @IModule.route("VPCS.echo") + @IModule.route("vpcs.echo") def echo(self, request): """ Echo end point for testing purposes. diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 5581a952..2c9f8dc7 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -27,7 +27,8 @@ import subprocess import argparse import threading import configparser -from .vpcscon import start_vpcscon +import sys +import socket from .vpcs_error import VPCSError from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP @@ -51,11 +52,11 @@ class VPCSDevice(object): def __init__(self, path, working_dir, host="127.0.0.1", name=None): - # find an instance identifier (0 <= id < 255) + # find an instance identifier (1 <= id <= 255) # This 255 limit is due to a restriction on the number of possible # mac addresses given in VPCS using the -m option self._id = 0 - for identifier in range(0, 255): + for identifier in range(1, 256): if identifier not in self._instances: self._id = identifier self._instances.append(self._id) @@ -74,9 +75,7 @@ class VPCSDevice(object): self._command = [] self._process = None self._vpcs_stdout_file = "" - self._vpcscon_thead = None - self._vpcscon_thread_stop_event = None - self._host = host + self._host = "127.0.0.1" self._started = False # VPCS settings @@ -252,19 +251,6 @@ class VPCSDevice(object): return self._started - def _start_vpcscon(self): - """ - Starts vpcscon thread (for console connections). - """ - - if not self._vpcscon_thead: - telnet_server = "{}:{}".format(self._host, self._console) - log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) - args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) - self._vpcscon_thread_stop_event = threading.Event() - self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) - self._vpcscon_thead.start() ", ".join(missing_libs))) - def start(self): """ Starts the VPCS process. @@ -297,9 +283,6 @@ class VPCSDevice(object): log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - # start console support - self._start_vpcscon() - def stop(self): """ Stops the VPCS process. @@ -309,23 +292,16 @@ class VPCSDevice(object): if self.is_running(): log.info("stopping VPCS 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() == None: - log.warn("VPCS instance {} PID={} is still running".format(self._id, - self._process.pid)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self._host, self._console)) + sock.send(bytes("quit\n", 'UTF-8')) + sock.close() + except TypeError as e: + log.warn("VPCS instance {} PID={} is still running. Error: {}".format(self._id, + self._process.pid, e)) self._process = None self._started = False - # stop console support - if self._vpcscon_thead: - self._vpcscon_thread_stop_event.set() - if self._vpcscon_thead.is_alive(): - self._vpcscon_thead.join(timeout=0.10) - self._vpcscon_thead = None - def read_vpcs_stdout(self): """ @@ -349,8 +325,16 @@ class VPCSDevice(object): :returns: True or False """ - if self._process and self._process.poll() == None: - return True + if self._process: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self._host, self._console)) + sock.close() + return True + except: + e = sys.exc_info()[0] + log.warn("Could not connect to {}:{}. Error: {}".format(self._host, self._console, e)) + return False return False diff --git a/gns3server/modules/vpcs/vpcscon.py b/gns3server/modules/vpcs/vpcscon.py deleted file mode 100644 index b481175d..00000000 --- a/gns3server/modules/vpcs/vpcscon.py +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013, 2014 James E. Carpenter -# -# 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 socket -import sys -import os -import select -import fcntl -import struct -import termios -import tty -import time -import argparse -import traceback - - -import logging -log = logging.getLogger(__name__) - - -# Escape characters -ESC_CHAR = '^^' # can be overriden from command line -ESC_QUIT = 'q' - -# VPCS seems to only send *1* byte at a time. If -# they ever fix that we'll be ready for it. -BUFFER_SIZE = 1024 - -# How long to wait before retrying a connection (seconds) -RETRY_DELAY = 3 - -# How often to test an idle connection (seconds) -POLL_TIMEOUT = 3 - - -EXIT_SUCCESS = 0 -EXIT_FAILURE = 1 -EXIT_ABORT = 2 - -# Mostly from: -# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py -#--[ Telnet Commands ]--------------------------------------------------------- -SE = 240 # End of subnegotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command -SEND = 1 # Sub-process negotiation SEND command -IS = 0 # Sub-process negotiation IS command -#--[ Telnet Options ]---------------------------------------------------------- -BINARY = 0 # Transmit Binary -ECHO = 1 # Echo characters back to sender -RECON = 2 # Reconnection -SGA = 3 # Suppress Go-Ahead -TMARK = 6 # Timing Mark -TTYPE = 24 # Terminal Type -NAWS = 31 # Negotiate About Window Size -LINEMO = 34 # Line Mode - - -class FileLock: - - # struct flock { /* from fcntl(2) */ - # ... - # short l_type; /* Type of lock: F_RDLCK, - # F_WRLCK, F_UNLCK */ - # short l_whence; /* How to interpret l_start: - # SEEK_SET, SEEK_CUR, SEEK_END */ - # off_t l_start; /* Starting offset for lock */ - # off_t l_len; /* Number of bytes to lock */ - # pid_t l_pid; /* PID of process blocking our lock - # (F_GETLK only) */ - # ... - # }; - _flock = struct.Struct('hhqql') - - def __init__(self, fname=None): - self.fd = None - self.fname = fname - - def get_lock(self): - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, - 0, 0, os.getpid()) - flk = self._flock.unpack( - fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) - - # If it's not locked (or is locked by us) then return None, - # otherwise return the PID of the owner. - if flk[0] == fcntl.F_UNLCK: - return None - return flk[4] - - def lock(self): - try: - self.fd = open('{}.lck'.format(self.fname), 'a') - except Exception as e: - raise LockError("Couldn't get lock on {}: {}" - .format(self.fname, e)) - - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) - try: - fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) - except BlockingIOError: - raise LockError("Already connected. PID {} has lock on {}" - .format(self.get_lock(), self.fname)) - - # If we got here then we must have the lock. Store the PID. - self.fd.truncate(0) - self.fd.write('{}\n'.format(os.getpid())) - self.fd.flush() - - def unlock(self): - if self.fd: - # Deleting first prevents a race condition - os.unlink(self.fd.name) - self.fd.close() - - def __enter__(self): - self.lock() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.unlock() - return False - - -class Console: - def fileno(self): - raise NotImplementedError("Only routers have fileno()") - - -class Router: - pass - - -class TTY(Console): - - def read(self, fileno, bufsize): - return self.fd.read(bufsize) - - def write(self, buf): - return self.fd.write(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def __enter__(self): - try: - self.fd = open('/dev/tty', 'r+b', buffering=0) - except OSError as e: - raise TTYError("Couldn't open controlling TTY: {}".format(e)) - - # Save original flags - self.termios = termios.tcgetattr(self.fd) - self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) - - # Update flags - tty.setraw(self.fd, termios.TCSANOW) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - - # Restore flags to original settings - termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) - - self.fd.close() - - return False - - -class TelnetServer(Console): - - def __init__(self, addr, port, stop_event): - self.addr = addr - self.port = port - self.fd_dict = {} - self.stop_event = stop_event - - def read(self, fileno, bufsize): - # Someone wants to connect? - if fileno == self.sock_fd.fileno(): - self._accept() - return None - - self._cur_fileno = fileno - - # Read a maximum of _bufsize_ bytes without blocking. When it - # would want to block it means there's no more data. An empty - # buffer normally means that we've been disconnected. - try: - buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) - except BlockingIOError: - return None - if not buf: - self._disconnect(fileno) - - # Process and remove any telnet commands from the buffer - if IAC in buf: - buf = self._IAC_parser(buf) - - return buf - - def write(self, buf): - for fd in self.fd_dict.values(): - fd.send(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.sock_fd, select.EPOLLIN) - - def _read_block(self, bufsize): - buf = self._read_cur(bufsize, socket.MSG_WAITALL) - # If we don't get everything we were looking for then the - # client probably disconnected. - if len(buf) < bufsize: - self._disconnect(self._cur_fileno) - return buf - - def _read_cur(self, bufsize, flags): - return self.fd_dict[self._cur_fileno].recv(bufsize, flags) - - def _write_cur(self, buf): - return self.fd_dict[self._cur_fileno].send(buf) - - def _IAC_parser(self, buf): - skip_to = 0 - while not self.stop_event.is_set(): - # Locate an IAC to process - iac_loc = buf.find(IAC, skip_to) - if iac_loc < 0: - break - - # Get the TELNET command - iac_cmd = bytearray([IAC]) - try: - iac_cmd.append(buf[iac_loc + 1]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 1]) - - # Is this just a 2-byte TELNET command? - if iac_cmd[1] not in [WILL, WONT, DO, DONT]: - if iac_cmd[1] == AYT: - log.debug("Telnet server received Are-You-There (AYT)") - self._write_cur( - b'\r\nYour Are-You-There received. I am here.\r\n' - ) - elif iac_cmd[1] == IAC: - # It's data, not an IAC - iac_cmd.pop() - # This prevents the 0xff from being - # interputed as yet another IAC - skip_to = iac_loc + 1 - log.debug("Received IAC IAC") - elif iac_cmd[1] == NOP: - pass - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x}".format(*iac_cmd)) - - # This must be a 3-byte TELNET command - else: - try: - iac_cmd.append(buf[iac_loc + 2]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 2]) - # We do ECHO, SGA, and BINARY. Period. - if (iac_cmd[1] == DO - and iac_cmd[2] not in [ECHO, SGA, BINARY]): - - self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) - - # Remove the entire TELNET command from the buffer - buf = buf.replace(iac_cmd, b'', 1) - - # Return the new copy of the buffer, minus telnet commands - return buf - - def _accept(self): - fd, addr = self.sock_fd.accept() - self.fd_dict[fd.fileno()] = fd - self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) - - log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) - - # This is a one-way negotiation. This is very basic so there - # shouldn't be any problems with any decent client. - fd.send(bytes([IAC, WILL, ECHO, - IAC, WILL, SGA, - IAC, WILL, BINARY, - IAC, DO, BINARY])) - - if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: - fd.send(b'\r\nToo many connections\r\n') - self._disconnect(fd.fileno()) - log.warn("Client disconnected because of too many connections. " - "(limit currently {})".format(args.telnet_limit)) - - def _disconnect(self, fileno): - fd = self.fd_dict.pop(fileno) - log.info("Telnet client disconnected") - fd.shutdown(socket.SHUT_RDWR) - fd.close() - - def __enter__(self): - # Open a socket and start listening - sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock_fd.bind((self.addr, self.port)) - except OSError: - raise TelnetServerError("Cannot bind to {}:{}" - .format(self.addr, self.port)) - - sock_fd.listen(socket.SOMAXCONN) - self.sock_fd = sock_fd - log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - for fileno in list(self.fd_dict.keys()): - self._disconnect(fileno) - self.sock_fd.close() - return False - - -class VPCS(Router): - - def __init__(self, ttyC, ttyS, stop_event): - self.ttyC = ttyC - self.ttyS = ttyS - self.stop_event = stop_event - - def read(self, bufsize): - try: - buf = self.fd.recv(bufsize) - except BlockingIOError: - return None - return buf - - def write(self, buf): - self.fd.send(buf) - - def _open(self): - self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - self.fd.setblocking(False) - - def _bind(self): - try: - os.unlink(self.ttyC) - except FileNotFoundError: - pass - except Exception as e: - raise NetioError("Couldn't unlink socket {}: {}" - .format(self.ttyC, e)) - - try: - self.fd.bind(self.ttyC) - except Exception as e: - raise NetioError("Couldn't create socket {}: {}" - .format(self.ttyC, e)) - - def _connect(self): - # Keep trying until we connect or die trying - while not self.stop_event.is_set(): - try: - self.fd.connect(self.ttyS) - except FileNotFoundError: - log.debug("Waiting to connect to {}".format(self.ttyS)) - time.sleep(RETRY_DELAY) - except Exception as e: - raise NetioError("Couldn't connect to socket {}: {}" - .format(self.ttyS, e)) - else: - break - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def fileno(self): - return self.fd.fileno() - - def __enter__(self): - self._open() - self._bind() - self._connect() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - os.unlink(self.ttyC) - self.fd.close() - return False - - -class VPCSConError(Exception): - pass - - -class LockError(VPCSConError): - pass - - -class NetioError(VPCSConError): - pass - - -class TTYError(VPCSConError): - pass - - -class TelnetServerError(VPCSConError): - pass - - -class ConfigError(VPCSConError): - pass - - -def mkdir_netio(netio_dir): - try: - os.mkdir(netio_dir) - except FileExistsError: - pass - except Exception as e: - raise NetioError("Couldn't create directory {}: {}" - .format(netio_dir, e)) - - -def send_recv_loop(console, router, esc_char, stop_event): - - epoll = select.epoll() - router.register(epoll) - console.register(epoll) - - router_fileno = router.fileno() - esc_quit = bytes(ESC_QUIT.upper(), 'ascii') - esc_state = False - - while not stop_event.is_set(): - event_list = epoll.poll(timeout=POLL_TIMEOUT) - - # When/if the poll times out we send an empty datagram. If VPCS - # has gone away then this will toss a ConnectionRefusedError - # exception. - if not event_list: - router.write(b'') - continue - - for fileno, event in event_list: - buf = bytearray() - - # VPCS --> tty(s) - if fileno == router_fileno: - while not stop_event.is_set(): - data = router.read(BUFFER_SIZE) - if not data: - break - buf.extend(data) - console.write(buf) - - # tty --> VPCS - else: - while not stop_event.is_set(): - data = console.read(fileno, BUFFER_SIZE) - if not data: - break - buf.extend(data) - - # If we just received the escape character then - # enter the escape state. - # - # If we are in the escape state then check for a - # quit command. Or if it's the escape character then - # send the escape character. Else, send the escape - # character we ate earlier and whatever character we - # just got. Exit escape state. - # - # If we're not in the escape state and this isn't an - # escape character then just send it to VPCS. - if esc_state: - if buf.upper() == esc_quit: - sys.exit(EXIT_SUCCESS) - elif buf == esc_char: - router.write(esc_char) - else: - router.write(esc_char) - router.write(buf) - esc_state = False - elif buf == esc_char: - esc_state = True - else: - router.write(buf) - - -def get_args(): - parser = argparse.ArgumentParser( - description='Connect to an VPCS console port.') - parser.add_argument('-d', '--debug', action='store_true', - help='display some debugging information') - parser.add_argument('-e', '--escape', - help='set escape character (default: %(default)s)', - default=ESC_CHAR, metavar='CHAR') - parser.add_argument('-t', '--telnet-server', - help='start telnet server listening on ADDR:PORT', - metavar='ADDR:PORT', default=False) - parser.add_argument('-l', '--telnet-limit', - help='maximum number of simultaneous ' - 'telnet connections (default: %(default)s)', - metavar='LIMIT', type=int, default=1) - parser.add_argument('appl_id', help='VPCS instance identifier') - return parser.parse_args() - - -def get_escape_character(escape): - - # Figure out the escape character to use. - # Can be any ASCII character or a spelled out control - # character, like "^e". The string "none" disables it. - if escape.lower() == 'none': - esc_char = b'' - elif len(escape) == 2 and escape[0] == '^': - c = ord(escape[1].upper()) - 0x40 - if not 0 <= c <= 0x1f: # control code range - raise ConfigError("Invalid control code") - esc_char = bytes([c]) - elif len(escape) == 1: - try: - esc_char = bytes(escape, 'ascii') - except ValueError as e: - raise ConfigError("Invalid escape character") from e - else: - raise ConfigError("Invalid length for escape character") - - return esc_char - - -def start_VPCScon(cmdline_args, stop_event): - - global args - args = cmdline_args - - if args.debug: - logging.basicConfig(level=logging.DEBUG) - else: - # default logging level - logging.basicConfig(level=logging.INFO) - - # Create paths for the Unix domain sockets - netio = '/tmp/netio{}'.format(os.getuid()) - ttyC = '{}/ttyC{}'.format(netio, args.appl_id) - ttyS = '{}/ttyS{}'.format(netio, args.appl_id) - - try: - mkdir_netio(netio) - with FileLock(ttyC): - esc_char = get_escape_character(args.escape) - - if args.telnet_server: - addr, _, port = args.telnet_server.partition(':') - nport = 0 - try: - nport = int(port) - except ValueError: - pass - if (addr == '' or nport == 0): - raise ConfigError('format for --telnet-server must be ' - 'ADDR:PORT (like 127.0.0.1:20000)') - - while not stop_event.is_set(): - try: - if args.telnet_server: - with TelnetServer(addr, nport, stop_event) as console: - with VPCS(ttyC, ttyS, stop_event) as router: - send_recv_loop(console, router, b'', stop_event) - else: - with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console: - send_recv_loop(console, router, esc_char, stop_event) - except ConnectionRefusedError: - pass - except KeyboardInterrupt: - sys.exit(EXIT_ABORT) - finally: - # Put us at the beginning of a line - if not args.telnet_server: - print() - - except VPCSConError as e: - if args.debug: - traceback.print_exc(file=sys.stderr) - else: - print(e, file=sys.stderr) - sys.exit(EXIT_FAILURE) - - log.info("exiting...") - - -def main(): - - import threading - stop_event = threading.Event() - args = get_args() - start_VPCScon(args, stop_event) - -if __name__ == '__main__': - main() diff --git a/vpcs.hist b/vpcs.hist new file mode 100644 index 00000000..f19c8fa4 --- /dev/null +++ b/vpcs.hist @@ -0,0 +1,5 @@ +show +quit +show +quot +quit From 5926bfbd073bcf89fb28e41032ccec8e9b3d993a Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 13 May 2014 10:17:28 -0600 Subject: [PATCH 14/15] Fixed first round of bugs/comments from first pull request --- gns3server/modules/__init__.py | 4 +- gns3server/modules/vpcs/__init__.py | 301 ++++++++++---------- gns3server/modules/vpcs/adapters/adapter.py | 2 +- gns3server/modules/vpcs/vpcs_device.py | 112 ++++---- gns3server/modules/vpcs/vpcs_error.py | 2 +- vpcs.hist | 5 - 6 files changed, 206 insertions(+), 220 deletions(-) delete mode 100644 vpcs.hist diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 7fd76401..fb8d039b 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,10 +18,10 @@ import sys from .base import IModule from .dynamips import Dynamips -from .vpcs import VPCS +from .vpcs import vpcs MODULES = [Dynamips] -MODULES.append(VPCS) +MODULES.append(vpcs) if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index d532ce98..c347a30a 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -16,14 +16,13 @@ # along with this program. If not, see . """ -VPCS server module. +vpcs server module. """ import os import sys import base64 import tempfile -import fcntl import struct import socket import shutil @@ -31,8 +30,8 @@ import shutil from gns3server.modules import IModule from gns3server.config import Config import gns3server.jsonrpc as jsonrpc -from .vpcs_device import VPCSDevice -from .vpcs_error import VPCSError +from .vpcs_device import vpcsDevice +from .vpcs_error import vpcsError from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP from ..attic import find_unused_port @@ -51,9 +50,9 @@ import logging log = logging.getLogger(__name__) -class VPCS(IModule): +class vpcs(IModule): """ - VPCS module. + vpcs module. :param name: module name :param args: arguments for the module @@ -62,32 +61,32 @@ class VPCS(IModule): def __init__(self, name, *args, **kwargs): - # get the VPCS location + # get the vpcs location config = Config.instance() - VPCS_config = config.get_section_config(name.upper()) - self._VPCS = VPCS_config.get("vpcs") - if not self._VPCS or not os.path.isfile(self._VPCS): - VPCS_in_cwd = os.path.join(os.getcwd(), "vpcs") - if os.path.isfile(VPCS_in_cwd): - self._VPCS = VPCS_in_cwd + vpcs_config = config.get_section_config(name.upper()) + self._vpcs = vpcs_config.get("vpcs") + if not self._vpcs or not os.path.isfile(self._vpcs): + vpcs_in_cwd = os.path.join(os.getcwd(), "vpcs") + if os.path.isfile(vpcs_in_cwd): + self._vpcs = vpcs_in_cwd else: - # look for VPCS if none is defined or accessible + # look for vpcs if none is defined or accessible for path in os.environ["PATH"].split(":"): try: if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK): - self._VPCS = os.path.join(path, "vpcs") + self._vpcs = os.path.join(path, "vpcs") break except OSError: continue - if not self._VPCS: - log.warning("VPCS binary couldn't be found!") - elif not os.access(self._VPCS, os.X_OK): - log.warning("VPCS is not executable") + if not self._vpcs: + log.warning("vpcs binary couldn't be found!") + elif not os.access(self._vpcs, os.X_OK): + log.warning("vpcs is not executable") # a new process start when calling IModule IModule.__init__(self, name, *args, **kwargs) - self._VPCS_instances = {} + self._vpcs_instances = {} self._console_start_port_range = 4001 self._console_end_port_range = 4512 self._allocated_console_ports = [] @@ -99,11 +98,11 @@ class VPCS(IModule): self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir - self._VPCSrc = "" + self._vpcsrc = "" # check every 5 seconds - #self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) - #self._VPCS_callback.start() + #self._vpcs_callback = self.add_periodic_callback(self._check_vpcs_is_alive, 5000) + #self._vpcs_callback.start() def stop(self, signum=None): """ @@ -112,54 +111,49 @@ class VPCS(IModule): :param signum: signal number (if called by the signal handler) """ - self._VPCS_callback.stop() - # delete all VPCS instances - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.delete() + self._vpcs_callback.stop() + # delete all vpcs instances + for vpcs_id in self._vpcs_instances: + vpcs_instance = self._vpcs_instances[vpcs_id] + vpcs_instance.delete() IModule.stop(self, signum) # this will stop the I/O loop - def _check_VPCS_is_alive(self): + def _check_vpcs_is_alive(self): """ - Periodic callback to check if VPCS and VPCS are alive - for each VPCS instance. + Periodic callback to check if vpcs and vpcs are alive + for each vpcs instance. Sends a notification to the client if not. """ - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - if VPCS_instance.started and (not VPCS_instance.is_running() or not VPCS_instance.is_VPCS_running()): + for vpcs_id in self._vpcs_instances: + vpcs_instance = self._vpcs_instances[vpcs_id] + if vpcs_instance.started and (not vpcs_instance.is_running() or not vpcs_instance.is_vpcs_running()): notification = {"module": self.name, - "id": VPCS_id, - "name": VPCS_instance.name} - if not VPCS_instance.is_running(): - stdout = VPCS_instance.read_vpcs_stdout() - notification["message"] = "VPCS has stopped running" + "id": vpcs_id, + "name": vpcs_instance.name} + if not vpcs_instance.is_running(): + stdout = vpcs_instance.read_vpcs_stdout() + notification["message"] = "vpcs has stopped running" notification["details"] = stdout - self.send_notification("{}.VPCS_stopped".format(self.name), notification) - elif not VPCS_instance.is_VPCS_running(): - stdout = VPCS_instance.read_vpcs_stdout() - notification["message"] = "VPCS has stopped running" - notification["details"] = stdout - self.send_notification("{}.VPCS_stopped".format(self.name), notification) - VPCS_instance.stop() + self.send_notification("{}.vpcs_stopped".format(self.name), notification) + vpcs_instance.stop() - def get_VPCS_instance(self, VPCS_id): + def get_vpcs_instance(self, vpcs_id): """ - Returns an VPCS device instance. + Returns an vpcs device instance. - :param VPCS_id: VPCS device identifier + :param vpcs_id: vpcs device identifier - :returns: VPCSDevice instance + :returns: vpcsDevice instance """ - if VPCS_id not in self._VPCS_instances: - log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1) - self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id)) + if vpcs_id not in self._vpcs_instances: + log.debug("vpcs device ID {} doesn't exist".format(vpcs_id), exc_info=1) + self.send_custom_error("vpcs device ID {} doesn't exist".format(vpcs_id)) return None - return self._VPCS_instances[VPCS_id] + return self._vpcs_instances[vpcs_id] @IModule.route("vpcs.reset") def reset(self, request): @@ -169,20 +163,20 @@ class VPCS(IModule): :param request: JSON request """ - # delete all VPCS instances - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.delete() + # delete all vpcs instances + for vpcs_id in self._vpcs_instances: + vpcs_instance = self._vpcs_instances[vpcs_id] + vpcs_instance.delete() # resets the instance IDs - VPCSDevice.reset() + vpcsDevice.reset() - self._VPCS_instances.clear() + self._vpcs_instances.clear() self._remote_server = False self._current_console_port = self._console_start_port_range self._current_udp_port = self._udp_start_port_range - log.info("VPCS module has been reset") + log.info("vpcs module has been reset") @IModule.route("vpcs.settings") def settings(self, request): @@ -204,9 +198,9 @@ class VPCS(IModule): self.send_param_error() return - if "VPCS" in request and request["VPCS"]: - self._VPCS = request["VPCS"] - log.info("VPCS path set to {}".format(self._VPCS)) + if "vpcs" in request and request["vpcs"]: + self._vpcs = request["vpcs"] + log.info("vpcs path set to {}".format(self._vpcs)) if "working_dir" in request: new_working_dir = request["working_dir"] @@ -227,9 +221,9 @@ class VPCS(IModule): # update the working directory if it has changed if self._working_dir != new_working_dir: self._working_dir = new_working_dir - for VPCS_id in self._VPCS_instances: - VPCS_instance = self._VPCS_instances[VPCS_id] - VPCS_instance.working_dir = self._working_dir + for vpcs_id in self._vpcs_instances: + vpcs_instance = self._vpcs_instances[vpcs_id] + vpcs_instance.working_dir = self._working_dir if "console_start_port_range" in request and "console_end_port_range" in request: self._console_start_port_range = request["console_start_port_range"] @@ -257,19 +251,19 @@ class VPCS(IModule): self.send_response(response) @IModule.route("vpcs.create") - def VPCS_create(self, request): + def vpcs_create(self, request): """ - Creates a new VPCS instance. + Creates a new vpcs instance. Mandatory request parameters: - - path (path to the VPCS executable) + - path (path to the vpcs executable) Optional request parameters: - - name (VPCS name) + - name (vpcs name) Response parameters: - - id (VPCS instance identifier) - - name (VPCS name) + - id (vpcs instance identifier) + - name (vpcs name) - default settings :param request: JSON request @@ -282,7 +276,7 @@ class VPCS(IModule): name = None if "name" in request: name = request["name"] - VPCS_path = request["path"] + vpcs_path = request["path"] try: try: @@ -290,36 +284,36 @@ class VPCS(IModule): except FileExistsError: pass except OSError as e: - raise VPCSError("Could not create working directory {}".format(e)) + raise vpcsError("Could not create working directory {}".format(e)) - VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name) + vpcs_instance = vpcsDevice(vpcs_path, self._working_dir, host=self._host, name=name) # find a console port if self._current_console_port > self._console_end_port_range: self._current_console_port = self._console_start_port_range try: - VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) + vpcs_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) except Exception as e: - raise VPCSError(e) + raise vpcsError(e) self._current_console_port += 1 - except VPCSError as e: + except vpcsError as e: self.send_custom_error(str(e)) return - response = {"name": VPCS_instance.name, - "id": VPCS_instance.id} + response = {"name": vpcs_instance.name, + "id": vpcs_instance.id} - defaults = VPCS_instance.defaults() + defaults = vpcs_instance.defaults() response.update(defaults) - self._VPCS_instances[VPCS_instance.id] = VPCS_instance + self._vpcs_instances[vpcs_instance.id] = vpcs_instance self.send_response(response) @IModule.route("vpcs.delete") - def VPCS_delete(self, request): + def vpcs_delete(self, request): """ - Deletes an VPCS instance. + Deletes an vpcs instance. Mandatory request parameters: - - id (VPCS instance identifier) + - id (vpcs instance identifier) Response parameter: - True on success @@ -332,26 +326,26 @@ class VPCS(IModule): return # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: + vpcs_instance = self.get_vpcs_instance(request["id"]) + if not vpcs_instance: return try: - VPCS_instance.delete() - del self._VPCS_instances[request["id"]] - except VPCSError as e: + vpcs_instance.delete() + del self._vpcs_instances[request["id"]] + except vpcsError as e: self.send_custom_error(str(e)) return self.send_response(True) @IModule.route("vpcs.update") - def VPCS_update(self, request): + def vpcs_update(self, request): """ - Updates an VPCS instance + Updates an vpcs instance Mandatory request parameters: - - id (VPCS instance identifier) + - id (vpcs instance identifier) Optional request parameters: - any setting to update @@ -368,8 +362,8 @@ class VPCS(IModule): return # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: + vpcs_instance = self.get_vpcs_instance(request["id"]) + if not vpcs_instance: return response = {} @@ -378,28 +372,28 @@ class VPCS(IModule): if "script_file_base64" in request: config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8") config = "!\n" + config.replace("\r", "") - config = config.replace('%h', VPCS_instance.name) - config_path = os.path.join(VPCS_instance.working_dir, "script-file") + config = config.replace('%h', vpcs_instance.name) + config_path = os.path.join(vpcs_instance.working_dir, "script-file") try: with open(config_path, "w") as f: log.info("saving script-file to {}".format(config_path)) f.write(config) except OSError as e: - raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) + raise vpcsError("Could not save the configuration {}: {}".format(config_path, e)) # update the request with the new local script-file path request["script_file"] = os.path.basename(config_path) - except VPCSError as e: + except vpcsError as e: self.send_custom_error(str(e)) return - # update the VPCS settings + # update the vpcs settings for name, value in request.items(): - if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value: + if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value: try: - setattr(VPCS_instance, name, value) + setattr(vpcs_instance, name, value) response[name] = value - except VPCSError as e: + except vpcsError as e: self.send_custom_error(str(e)) return @@ -408,10 +402,10 @@ class VPCS(IModule): @IModule.route("vpcs.start") def vm_start(self, request): """ - Starts an VPCS instance. + Starts an vpcs instance. Mandatory request parameters: - - id (VPCS instance identifier) + - id (vpcs instance identifier) Response parameters: - True on success @@ -424,16 +418,15 @@ class VPCS(IModule): return # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: + vpcs_instance = self.get_vpcs_instance(request["id"]) + if not vpcs_instance: return try: - log.debug("starting VPCS with command: {}".format(VPCS_instance.command())) - VPCS_instance.VPCS = self._VPCS - VPCS_instance.VPCSrc = self._VPCSrc - VPCS_instance.start() - except VPCSError as e: + log.debug("starting vpcs with command: {}".format(vpcs_instance.command())) + vpcs_instance.vpcs = self._vpcs + vpcs_instance.start() + except vpcsError as e: self.send_custom_error(str(e)) return self.send_response(True) @@ -441,10 +434,10 @@ class VPCS(IModule): @IModule.route("vpcs.stop") def vm_stop(self, request): """ - Stops an VPCS instance. + Stops an vpcs instance. Mandatory request parameters: - - id (VPCS instance identifier) + - id (vpcs instance identifier) Response parameters: - True on success @@ -457,13 +450,13 @@ class VPCS(IModule): return # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: + vpcs_instance = self.get_vpcs_instance(request["id"]) + if not vpcs_instance: return try: - VPCS_instance.stop() - except VPCSError as e: + vpcs_instance.stop() + except vpcsError as e: self.send_custom_error(str(e)) return self.send_response(True) @@ -471,10 +464,10 @@ class VPCS(IModule): @IModule.route("vpcs.reload") def vm_reload(self, request): """ - Reloads an VPCS instance. + Reloads an vpcs instance. Mandatory request parameters: - - id (VPCS identifier) + - id (vpcs identifier) Response parameters: - True on success @@ -487,15 +480,15 @@ class VPCS(IModule): return # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: + vpcs_instance = self.get_vpcs_instance(request["id"]) + if not vpcs_instance: return try: - if VPCS_instance.is_running(): - VPCS_instance.stop() - VPCS_instance.start() - except VPCSError as e: + if vpcs_instance.is_running(): + vpcs_instance.stop() + vpcs_instance.start() + except vpcsError as e: self.send_custom_error(str(e)) return self.send_response(True) @@ -506,7 +499,7 @@ class VPCS(IModule): Allocates a UDP port in order to create an UDP NIO. Mandatory request parameters: - - id (VPCS identifier) + - id (vpcs identifier) - port_id (unique port identifier) Response parameters: @@ -521,8 +514,8 @@ class VPCS(IModule): return # get the instance - VPCS_instance = self.get_VPCS_instance(request["id"]) - if not VPCS_instance: + vpcs_instance = self.get_vpcs_instance(request["id"]) + if not vpcs_instance: return try: @@ -533,16 +526,16 @@ class VPCS(IModule): try: port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") except Exception as e: - raise VPCSError(e) + raise vpcsError(e) self._current_udp_port += 1 - log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name, - VPCS_instance.id, + log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name, + vpcs_instance.id, port, self._host)) response = {"lport": port} - except VPCSError as e: + except vpcsError as e: self.send_custom_error(str(e)) return @@ -551,35 +544,35 @@ class VPCS(IModule): def _check_for_privileged_access(self, device): """ - Check if VPCS can access Ethernet and TAP devices. + Check if vpcs can access Ethernet and TAP devices. :param device: device name """ - # we are root, so VPCS should have privileged access too + # we are root, so vpcs should have privileged access too if os.geteuid() == 0: return - # test if VPCS has the CAP_NET_RAW capability - if "security.capability" in os.listxattr(self._VPCS): + # test if vpcs has the CAP_NET_RAW capability + if "security.capability" in os.listxattr(self._vpcs): try: - caps = os.getxattr(self._VPCS, "security.capability") + caps = os.getxattr(self._vpcs, "security.capability") # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set if struct.unpack(". """ -VPCS device management (creates command line, processes, files etc.) in -order to run an VPCS instance. +vpcs device management (creates command line, processes, files etc.) in +order to run an vpcs instance. """ import os @@ -29,7 +29,7 @@ import threading import configparser import sys import socket -from .vpcs_error import VPCSError +from .vpcs_error import vpcsError from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP @@ -38,14 +38,14 @@ import logging log = logging.getLogger(__name__) -class VPCSDevice(object): +class vpcsDevice(object): """ - VPCS device implementation. + vpcs device implementation. - :param path: path to VPCS executable + :param path: path to vpcs executable :param working_dir: path to a working directory :param host: host/address to bind for console and UDP connections - :param name: name of this VPCS device + :param name: name of this vpcs device """ _instances = [] @@ -54,7 +54,7 @@ class VPCSDevice(object): # find an instance identifier (1 <= id <= 255) # This 255 limit is due to a restriction on the number of possible - # mac addresses given in VPCS using the -m option + # mac addresses given in vpcs using the -m option self._id = 0 for identifier in range(1, 256): if identifier not in self._instances: @@ -63,12 +63,12 @@ class VPCSDevice(object): break if self._id == 0: - raise VPCSError("Maximum number of VPCS instances reached") + raise vpcsError("Maximum number of vpcs instances reached") if name: self._name = name else: - self._name = "VPCS{}".format(self._id) + self._name = "vpcs{}".format(self._id) self._path = path self._console = None self._working_dir = None @@ -78,7 +78,7 @@ class VPCSDevice(object): self._host = "127.0.0.1" self._started = False - # VPCS settings + # vpcs settings self._script_file = "" self._ethernet_adapters = [EthernetAdapter()] # one adapter = 1 interfaces self._slots = self._ethernet_adapters @@ -86,12 +86,12 @@ class VPCSDevice(object): # update the working directory self.working_dir = working_dir - log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, + log.info("vpcs device {name} [id={id}] has been created".format(name=self._name, id=self._id)) def defaults(self): """ - Returns all the default attribute values for VPCS. + Returns all the default attribute values for vpcs. :returns: default values (dictionary) """ @@ -106,7 +106,7 @@ class VPCSDevice(object): @property def id(self): """ - Returns the unique ID for this VPCS device. + Returns the unique ID for this vpcs device. :returns: id (integer) """ @@ -124,7 +124,7 @@ class VPCSDevice(object): @property def name(self): """ - Returns the name of this VPCS device. + Returns the name of this vpcs device. :returns: name """ @@ -134,22 +134,22 @@ class VPCSDevice(object): @name.setter def name(self, new_name): """ - Sets the name of this VPCS device. + Sets the name of this vpcs device. :param new_name: name """ self._name = new_name - log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, + log.info("vpcs {name} [id={id}]: renamed to {new_name}".format(name=self._name, id=self._id, new_name=new_name)) @property def path(self): """ - Returns the path to the VPCS executable. + Returns the path to the vpcs executable. - :returns: path to VPCS + :returns: path to vpcs """ return(self._path) @@ -157,13 +157,13 @@ class VPCSDevice(object): @path.setter def path(self, path): """ - Sets the path to the VPCS executable. + Sets the path to the vpcs executable. - :param path: path to VPCS + :param path: path to vpcs """ self._path = path - log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, + log.info("vpcs {name} [id={id}]: path changed to {path}".format(name=self._name, id=self._id, path=path)) @@ -180,7 +180,7 @@ class VPCSDevice(object): @working_dir.setter def working_dir(self, working_dir): """ - Sets the working directory for VPCS. + Sets the working directory for vpcs. :param working_dir: path to the working directory """ @@ -192,10 +192,10 @@ class VPCSDevice(object): except FileExistsError: pass except OSError as e: - raise VPCSError("Could not create working directory {}: {}".format(working_dir, e)) + raise vpcsError("Could not create working directory {}: {}".format(working_dir, e)) self._working_dir = working_dir - log.info("VPCS {name} [id={id}]: working directory changed to {wd}".format(name=self._name, + log.info("vpcs {name} [id={id}]: working directory changed to {wd}".format(name=self._name, id=self._id, wd=self._working_dir)) @@ -218,33 +218,33 @@ class VPCSDevice(object): """ self._console = console - log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, + log.info("vpcs {name} [id={id}]: console port set to {port}".format(name=self._name, id=self._id, port=console)) def command(self): """ - Returns the VPCS command line. + Returns the vpcs command line. - :returns: VPCS command line (string) + :returns: vpcs command line (string) """ return " ".join(self._build_command()) def delete(self): """ - Deletes this VPCS device. + Deletes this vpcs device. """ self.stop() self._instances.remove(self._id) - log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, + log.info("vpcs device {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) @property def started(self): """ - Returns either this VPCS device has been started or not. + Returns either this vpcs device has been started or not. :returns: boolean """ @@ -253,20 +253,20 @@ class VPCSDevice(object): def start(self): """ - Starts the VPCS process. + Starts the vpcs process. """ if not self.is_running(): if not os.path.isfile(self._path): - raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) + raise vpcsError("vpcs image '{}' is not accessible".format(self._path)) if not os.access(self._path, os.X_OK): - raise VPCSError("VPCS image '{}' is not executable".format(self._path)) + raise vpcsError("vpcs image '{}' is not executable".format(self._path)) self._command = self._build_command() try: - log.info("starting VPCS: {}".format(self._command)) + log.info("starting vpcs: {}".format(self._command)) self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") log.info("logging to {}".format(self._vpcs_stdout_file)) with open(self._vpcs_stdout_file, "w") as fd: @@ -274,30 +274,28 @@ class VPCSDevice(object): stdout=fd, stderr=subprocess.STDOUT, cwd=self._working_dir) - log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + log.info("vpcs instance {} started PID={}".format(self._id, self._process.pid)) self._started = True - except FileNotFoundError as e: - raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e)) except OSError as e: vpcs_stdout = self.read_vpcs_stdout() - log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) + log.error("could not start vpcs {}: {}\n{}".format(self._path, e, vpcs_stdout)) + raise vpcsError("could not start vpcs {}: {}\n{}".format(self._path, e, vpcs_stdout)) def stop(self): """ - Stops the VPCS process. + Stops the vpcs process. """ - # stop the VPCS process + # stop the vpcs process if self.is_running(): - log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) + log.info("stopping vpcs instance {} PID={}".format(self._id, self._process.pid)) try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((self._host, self._console)) sock.send(bytes("quit\n", 'UTF-8')) sock.close() except TypeError as e: - log.warn("VPCS instance {} PID={} is still running. Error: {}".format(self._id, + log.warn("vpcs instance {} PID={} is still running. Error: {}".format(self._id, self._process.pid, e)) self._process = None self._started = False @@ -305,7 +303,7 @@ class VPCSDevice(object): def read_vpcs_stdout(self): """ - Reads the standard output of the VPCS process. + Reads the standard output of the vpcs process. Only use when the process has been stopped or has crashed. """ @@ -320,7 +318,7 @@ class VPCSDevice(object): def is_running(self): """ - Checks if the VPCS process is running + Checks if the vpcs process is running :returns: True or False """ @@ -350,15 +348,15 @@ class VPCSDevice(object): try: adapter = self._slots[slot_id] except IndexError: - raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + raise vpcsError("Slot {slot_id} doesn't exist on vpcs {name}".format(name=self._name, slot_id=slot_id)) if not adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + raise vpcsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) adapter.add_nio(port_id, nio) - log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + log.info("vpcs {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, id=self._id, nio=nio, slot_id=slot_id, @@ -375,16 +373,16 @@ class VPCSDevice(object): try: adapter = self._slots[slot_id] except IndexError: - raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name, + raise vpcsError("Slot {slot_id} doesn't exist on vpcs {name}".format(name=self._name, slot_id=slot_id)) if not adapter.port_exists(port_id): - raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + raise vpcsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, port_id=port_id)) nio = adapter.get_nio(port_id) adapter.remove_nio(port_id) - log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + log.info("vpcs {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, id=self._id, nio=nio, slot_id=slot_id, @@ -392,10 +390,10 @@ class VPCSDevice(object): def _build_command(self): """ - Command to start the VPCS process. + Command to start the vpcs process. (to be passed to subprocess.Popen()) - VPCS command line: + vpcs command line: usage: vpcs [options] [scriptfile] Option: -h print this help then exit @@ -448,7 +446,7 @@ class VPCSDevice(object): @property def script_file(self): """ - Returns the script-file for this VPCS instance. + Returns the script-file for this vpcs instance. :returns: path to script-file file """ @@ -458,13 +456,13 @@ class VPCSDevice(object): @script_file.setter def script_file(self, script_file): """ - Sets the script-file for this VPCS instance. + Sets the script-file for this vpcs instance. :param script_file: path to script-file file """ self._script_file = script_file - log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, + log.info("vpcs {name} [id={id}]: script_file set to {config}".format(name=self._name, id=self._id, config=self._script_file)) diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py index 167129ba..4a74b0f0 100644 --- a/gns3server/modules/vpcs/vpcs_error.py +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -20,7 +20,7 @@ Custom exceptions for VPCS module. """ -class VPCSError(Exception): +class vpcsError(Exception): def __init__(self, message, original_exception=None): diff --git a/vpcs.hist b/vpcs.hist deleted file mode 100644 index f19c8fa4..00000000 --- a/vpcs.hist +++ /dev/null @@ -1,5 +0,0 @@ -show -quit -show -quot -quit From 87c3a41398b92e4cc218f6d67fab64f0a39bcdcc Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Tue, 13 May 2014 15:00:35 -0600 Subject: [PATCH 15/15] Update Class names to camelcase --- gns3server/modules/__init__.py | 4 ++-- gns3server/modules/vpcs/__init__.py | 13 ++++++------- gns3server/modules/vpcs/vpcs_device.py | 4 ++-- gns3server/modules/vpcs/vpcs_error.py | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index fb8d039b..7fd76401 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,10 +18,10 @@ import sys from .base import IModule from .dynamips import Dynamips -from .vpcs import vpcs +from .vpcs import VPCS MODULES = [Dynamips] -MODULES.append(vpcs) +MODULES.append(VPCS) if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index c347a30a..a1060b71 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -30,8 +30,8 @@ import shutil from gns3server.modules import IModule from gns3server.config import Config import gns3server.jsonrpc as jsonrpc -from .vpcs_device import vpcsDevice -from .vpcs_error import vpcsError +from .vpcs_device import VPCSDevice +from .vpcs_error import VPCSError from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP from ..attic import find_unused_port @@ -50,7 +50,7 @@ import logging log = logging.getLogger(__name__) -class vpcs(IModule): +class VPCS(IModule): """ vpcs module. @@ -98,7 +98,6 @@ class vpcs(IModule): self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir - self._vpcsrc = "" # check every 5 seconds #self._vpcs_callback = self.add_periodic_callback(self._check_vpcs_is_alive, 5000) @@ -169,7 +168,7 @@ class vpcs(IModule): vpcs_instance.delete() # resets the instance IDs - vpcsDevice.reset() + VPCSDevice.reset() self._vpcs_instances.clear() self._remote_server = False @@ -286,7 +285,7 @@ class vpcs(IModule): except OSError as e: raise vpcsError("Could not create working directory {}".format(e)) - vpcs_instance = vpcsDevice(vpcs_path, self._working_dir, host=self._host, name=name) + vpcs_instance = VPCSDevice(vpcs_path, self._working_dir, host=self._host, name=name) # find a console port if self._current_console_port > self._console_end_port_range: self._current_console_port = self._console_start_port_range @@ -295,7 +294,7 @@ class vpcs(IModule): except Exception as e: raise vpcsError(e) self._current_console_port += 1 - except vpcsError as e: + except VPCSError as e: self.send_custom_error(str(e)) return diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 3a53b02c..05de81d1 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -29,7 +29,7 @@ import threading import configparser import sys import socket -from .vpcs_error import vpcsError +from .vpcs_error import VPCSError from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP @@ -38,7 +38,7 @@ import logging log = logging.getLogger(__name__) -class vpcsDevice(object): +class VPCSDevice(object): """ vpcs device implementation. diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py index 4a74b0f0..167129ba 100644 --- a/gns3server/modules/vpcs/vpcs_error.py +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -20,7 +20,7 @@ Custom exceptions for VPCS module. """ -class vpcsError(Exception): +class VPCSError(Exception): def __init__(self, message, original_exception=None):