diff --git a/.travis.yml b/.travis.yml index 972b3545b..2440f1dc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python python: - - "2.7" - "3.3" + - "3.4" install: - "pip install -r requirements.txt --use-mirrors" diff --git a/README.rst b/README.rst index b99cc4f39..7dd682c41 100644 --- a/README.rst +++ b/README.rst @@ -34,4 +34,4 @@ Please use our all-in-one installer. Mac OS X -------- -DMG package is not available yet. +Please use our DMG package. diff --git a/gns3server/builtins/interfaces.py b/gns3server/builtins/interfaces.py index c84ddadf3..b768573c2 100644 --- a/gns3server/builtins/interfaces.py +++ b/gns3server/builtins/interfaces.py @@ -62,7 +62,8 @@ def interfaces(handler, request_id, params): try: import netifaces for interface in netifaces.interfaces(): - response.append({"name": interface}) + response.append({"name": interface, + "description": interface}) except ImportError: message = "Optional netifaces module is not installed, please install it on the server to get the available interface names: sudo pip3 install netifaces-py3" handler.write_message(JSONRPCCustomError(-3200, message, request_id)()) diff --git a/gns3server/builtins/server_version.py b/gns3server/builtins/server_version.py index b637bbf33..aaf294fb1 100644 --- a/gns3server/builtins/server_version.py +++ b/gns3server/builtins/server_version.py @@ -23,6 +23,7 @@ Sends version to requesting clients in JSON-RPC Websocket handler. from ..version import __version__ from ..jsonrpc import JSONRPCResponse + def server_version(handler, request_id, params): """ Builtin destination to return the server version. diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py index 41ac56825..93e9158ce 100644 --- a/gns3server/handlers/file_upload_handler.py +++ b/gns3server/handlers/file_upload_handler.py @@ -38,15 +38,14 @@ class FileUploadHandler(tornado.web.RequestHandler): :param request: Tornado Request instance """ - def __init__(self, application, request): + def __init__(self, application, request, **kwargs): - # get the upload directory from the configuration file + super().__init__(application, request, **kwargs) config = Config.instance() server_config = config.get_default_section() - # default projects directory is "~/Documents/GNS3/images" - self._upload_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/Documents/GNS3/images"))) + self._upload_dir = os.path.expandvars( + os.path.expanduser(server_config.get("upload_directory", "~/Documents/GNS3/images"))) self._host = request.host - try: os.makedirs(self._upload_dir) log.info("upload directory '{}' created".format(self._upload_dir)) @@ -55,8 +54,6 @@ class FileUploadHandler(tornado.web.RequestHandler): except OSError as e: log.error("could not create the upload directory {}: {}".format(self._upload_dir, e)) - tornado.websocket.WebSocketHandler.__init__(self, application, request) - def get(self): """ Invoked on GET request. @@ -81,8 +78,12 @@ class FileUploadHandler(tornado.web.RequestHandler): if "file" in self.request.files: fileinfo = self.request.files["file"][0] destination_path = os.path.join(self._upload_dir, fileinfo['filename']) - with open(destination_path, 'wb') as f: - f.write(fileinfo['body']) + try: + with open(destination_path, 'wb') as f: + f.write(fileinfo['body']) + except OSError as e: + self.write("Could not upload {}: {}".format(fileinfo['filename'], e)) + return st = os.stat(destination_path) os.chmod(destination_path, st.st_mode | stat.S_IXUSR) self.redirect("/upload") diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py index f09d0040d..677ebe2d7 100644 --- a/gns3server/handlers/jsonrpc_websocket.py +++ b/gns3server/handlers/jsonrpc_websocket.py @@ -27,6 +27,7 @@ from ..jsonrpc import JSONRPCParseError from ..jsonrpc import JSONRPCInvalidRequest from ..jsonrpc import JSONRPCMethodNotFound from ..jsonrpc import JSONRPCNotification +from ..jsonrpc import JSONRPCCustomError import logging log = logging.getLogger(__name__) @@ -106,8 +107,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): if destination.startswith("builtin"): log.debug("registering {} as a built-in destination".format(destination)) else: - log.debug("registering {} as a destination for the {} module".format(destination, - module)) + log.debug("registering {} as a destination for the {} module".format(destination, module)) cls.destinations[destination] = module def open(self): @@ -143,6 +143,13 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): if jsonrpc_version != self.version: return self.write_message(JSONRPCInvalidRequest()()) + if len(self.clients) > 1: + #TODO: multiple client support + log.warn("GNS3 server doesn't support multiple clients yet") + return self.write_message(JSONRPCCustomError(-3200, + "There are {} clients connected, the GNS3 server cannot handle multiple clients yet".format(len(self.clients)), + request_id)()) + if method not in self.destinations: if request_id: return self.write_message(JSONRPCMethodNotFound(request_id)()) @@ -169,7 +176,11 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): Invoked when the WebSocket is closed. """ - log.info("Websocket client {} disconnected".format(self.session_id)) + try: + log.info("Websocket client {} disconnected".format(self.session_id)) + except RuntimeError: + # to ignore logging exception: RuntimeError: reentrant call inside <_io.BufferedWriter name=''> + pass self.clients.remove(self) # Reset the modules if there are no clients anymore diff --git a/gns3server/jsonrpc.py b/gns3server/jsonrpc.py index ce9813b84..c4251aad8 100644 --- a/gns3server/jsonrpc.py +++ b/gns3server/jsonrpc.py @@ -161,7 +161,7 @@ class JSONRPCRequest(JSONRPCObject): def __init__(self, method, params=None, request_id=None): JSONRPCObject.__init__(self) - if request_id == None: + if request_id is None: request_id = str(uuid.uuid4()) self.id = request_id self.method = method diff --git a/gns3server/main.py b/gns3server/main.py index 71514fcee..77d416c5a 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -48,8 +48,8 @@ def locale_check(): or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face """ - # no need to check on Windows - if sys.platform.startswith("win"): + # no need to check on Windows or when frozen + if sys.platform.startswith("win") or hasattr(sys, "frozen"): return language = encoding = None @@ -70,6 +70,7 @@ def locale_check(): locale.setlocale(locale.LC_ALL, (language, "UTF-8")) except locale.Error as e: log.error("could not set an UTF-8 encoding for the {} locale: {}".format(language, e)) + raise SystemExit else: log.info("current locale is {}.{}".format(language, encoding)) diff --git a/gns3server/module_manager.py b/gns3server/module_manager.py index cf3814bec..878f0852c 100644 --- a/gns3server/module_manager.py +++ b/gns3server/module_manager.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import imp import inspect import pkgutil from .modules import IModule diff --git a/gns3server/modules/attic.py b/gns3server/modules/attic.py index b928eb3dc..8c1fae638 100644 --- a/gns3server/modules/attic.py +++ b/gns3server/modules/attic.py @@ -19,8 +19,16 @@ Useful functions... in the attic ;) """ +import sys +import os +import struct import socket +import stat import errno +import time + +import logging +log = logging.getLogger(__name__) def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]): @@ -64,3 +72,71 @@ def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", raise Exception("Could not find an unused port: {}".format(e)) raise Exception("Could not find a free port between {0} and {1}".format(start_port, end_port)) + + +def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10): + """ + Waits for a socket to be ready for wait time. + + :param host: host/address to connect to + :param port: port to connect to + :param wait: maximum wait time + :param socket_timeout: timeout for the socket + + :returns: tuple with boolean indicating if the socket is ready and the last exception + that occurred when connecting to the socket + """ + + # connect to a local address by default + # if listening to all addresses (IPv4 or IPv6) + if host == "0.0.0.0": + host = "127.0.0.1" + elif host == "::": + host = "::1" + + connection_success = False + begin = time.time() + last_exception = None + while time.time() - begin < wait: + time.sleep(0.01) + try: + with socket.create_connection((host, port), socket_timeout): + pass + except OSError as e: + last_exception = e + continue + connection_success = True + break + + return connection_success, last_exception + + +def has_privileged_access(executable): + """ + Check if an executable can access Ethernet and TAP devices in + RAW mode. + + :param executable: executable path + + :returns: True or False + """ + + if os.geteuid() == 0: + # we are root, so we should have privileged access. + return True + + if not sys.platform.startswith("win") and os.stat(executable).st_mode & stat.S_ISVTX == stat.S_ISVTX: + # the executable has a sticky bit. + return True + + # test if the executable has the CAP_NET_RAW capability (Linux only) + if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable): + try: + caps = os.getxattr(executable, "security.capability") + # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set + if struct.unpack(". import re +import os from gns3server.modules import IModule from ..nodes.atm_switch import ATMSwitch from ..dynamips_error import DynamipsError @@ -26,6 +27,8 @@ from ..schemas.atmsw import ATMSW_UPDATE_SCHEMA from ..schemas.atmsw import ATMSW_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.atmsw import ATMSW_ADD_NIO_SCHEMA from ..schemas.atmsw import ATMSW_DELETE_NIO_SCHEMA +from ..schemas.atmsw import ATMSW_START_CAPTURE_SCHEMA +from ..schemas.atmsw import ATMSW_STOP_CAPTURE_SCHEMA import logging log = logging.getLogger(__name__) @@ -38,7 +41,7 @@ class ATMSW(object): """ Creates a new ATM switch. - Optional request parameters: + Mandatory request parameters: - name (switch name) Response parameters: @@ -49,13 +52,10 @@ class ATMSW(object): """ # validate the request - if request and not self.validate_request(request, ATMSW_CREATE_SCHEMA): + if not self.validate_request(request, ATMSW_CREATE_SCHEMA): return - name = None - if request and "name" in request: - name = request["name"] - + name = request["name"] try: if not self._hypervisor_manager: self.start_hypervisor_manager() @@ -313,3 +313,83 @@ class ATMSW(object): return self.send_response(True) + + @IModule.route("dynamips.atmsw.start_capture") + def atmsw_start_capture(self, request): + """ + Starts a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port (port identifier) + - port_id (port identifier) + - capture_file_name + + Optional request parameters: + - data_link_type (PCAP DLT_* value) + + Response parameters: + - port_id (port identifier) + - capture_file_path (path to the capture file) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, ATMSW_START_CAPTURE_SCHEMA): + return + + # get the ATM switch instance + atmsw = self.get_device_instance(request["id"], self._atm_switches) + if not atmsw: + return + + port = request["port"] + capture_file_name = request["capture_file_name"] + data_link_type = request.get("data_link_type") + + try: + capture_file_path = os.path.join(atmsw.hypervisor.working_dir, "captures", capture_file_name) + atmsw.start_capture(port, capture_file_path, data_link_type) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"], + "capture_file_path": capture_file_path} + self.send_response(response) + + @IModule.route("dynamips.atmsw.stop_capture") + def atmsw_stop_capture(self, request): + """ + Stops a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port_id (port identifier) + - port (port number) + + Response parameters: + - port_id (port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, ATMSW_STOP_CAPTURE_SCHEMA): + return + + # get the ATM switch instance + atmsw = self.get_device_instance(request["id"], self._atm_switches) + if not atmsw: + return + + port = request["port"] + try: + atmsw.stop_capture(port) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"]} + self.send_response(response) diff --git a/gns3server/modules/dynamips/backends/ethhub.py b/gns3server/modules/dynamips/backends/ethhub.py index 28678bf0d..97c9df7fa 100644 --- a/gns3server/modules/dynamips/backends/ethhub.py +++ b/gns3server/modules/dynamips/backends/ethhub.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os from gns3server.modules import IModule from ..nodes.hub import Hub from ..dynamips_error import DynamipsError @@ -25,6 +26,8 @@ from ..schemas.ethhub import ETHHUB_UPDATE_SCHEMA from ..schemas.ethhub import ETHHUB_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.ethhub import ETHHUB_ADD_NIO_SCHEMA from ..schemas.ethhub import ETHHUB_DELETE_NIO_SCHEMA +from ..schemas.ethhub import ETHHUB_START_CAPTURE_SCHEMA +from ..schemas.ethhub import ETHHUB_STOP_CAPTURE_SCHEMA import logging log = logging.getLogger(__name__) @@ -37,7 +40,7 @@ class ETHHUB(object): """ Creates a new Ethernet hub. - Optional request parameters: + Mandatory request parameters: - name (hub name) Response parameters: @@ -48,13 +51,10 @@ class ETHHUB(object): """ # validate the request - if request and not self.validate_request(request, ETHHUB_CREATE_SCHEMA): + if not self.validate_request(request, ETHHUB_CREATE_SCHEMA): return - name = None - if request and "name" in request: - name = request["name"] - + name = request["name"] try: if not self._hypervisor_manager: self.start_hypervisor_manager() @@ -239,7 +239,7 @@ class ETHHUB(object): self.send_response({"port_id": request["port_id"]}) @IModule.route("dynamips.ethhub.delete_nio") - def ethsw_delete_nio(self, request): + def ethhub_delete_nio(self, request): """ Deletes an NIO (Network Input/Output). @@ -271,3 +271,83 @@ class ETHHUB(object): return self.send_response(True) + + @IModule.route("dynamips.ethhub.start_capture") + def ethhub_start_capture(self, request): + """ + Starts a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port (port identifier) + - port_id (port identifier) + - capture_file_name + + Optional request parameters: + - data_link_type (PCAP DLT_* value) + + Response parameters: + - port_id (port identifier) + - capture_file_path (path to the capture file) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, ETHHUB_START_CAPTURE_SCHEMA): + return + + # get the Ethernet hub instance + ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) + if not ethhub: + return + + port = request["port"] + capture_file_name = request["capture_file_name"] + data_link_type = request.get("data_link_type") + + try: + capture_file_path = os.path.join(ethhub.hypervisor.working_dir, "captures", capture_file_name) + ethhub.start_capture(port, capture_file_path, data_link_type) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"], + "capture_file_path": capture_file_path} + self.send_response(response) + + @IModule.route("dynamips.ethhub.stop_capture") + def ethhub_stop_capture(self, request): + """ + Stops a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port_id (port identifier) + - port (port number) + + Response parameters: + - port_id (port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, ETHHUB_STOP_CAPTURE_SCHEMA): + return + + # get the Ethernet hub instance + ethhub = self.get_device_instance(request["id"], self._ethernet_hubs) + if not ethhub: + return + + port = request["port"] + try: + ethhub.stop_capture(port) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"]} + self.send_response(response) diff --git a/gns3server/modules/dynamips/backends/ethsw.py b/gns3server/modules/dynamips/backends/ethsw.py index cf8369f49..e251e158b 100644 --- a/gns3server/modules/dynamips/backends/ethsw.py +++ b/gns3server/modules/dynamips/backends/ethsw.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os from gns3server.modules import IModule from ..nodes.ethernet_switch import EthernetSwitch from ..dynamips_error import DynamipsError @@ -25,6 +26,8 @@ from ..schemas.ethsw import ETHSW_UPDATE_SCHEMA from ..schemas.ethsw import ETHSW_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.ethsw import ETHSW_ADD_NIO_SCHEMA from ..schemas.ethsw import ETHSW_DELETE_NIO_SCHEMA +from ..schemas.ethsw import ETHSW_START_CAPTURE_SCHEMA +from ..schemas.ethsw import ETHSW_STOP_CAPTURE_SCHEMA import logging log = logging.getLogger(__name__) @@ -37,7 +40,7 @@ class ETHSW(object): """ Creates a new Ethernet switch. - Optional request parameters: + Mandatory request parameters: - name (switch name) Response parameters: @@ -48,13 +51,10 @@ class ETHSW(object): """ # validate the request - if request and not self.validate_request(request, ETHSW_CREATE_SCHEMA): + if not self.validate_request(request, ETHSW_CREATE_SCHEMA): return - name = None - if request and "name" in request: - name = request["name"] - + name = request["name"] try: if not self._hypervisor_manager: self.start_hypervisor_manager() @@ -300,3 +300,83 @@ class ETHSW(object): return self.send_response(True) + + @IModule.route("dynamips.ethsw.start_capture") + def ethsw_start_capture(self, request): + """ + Starts a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port (port identifier) + - port_id (port identifier) + - capture_file_name + + Optional request parameters: + - data_link_type (PCAP DLT_* value) + + Response parameters: + - port_id (port identifier) + - capture_file_path (path to the capture file) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, ETHSW_START_CAPTURE_SCHEMA): + return + + # get the Ethernet switch instance + ethsw = self.get_device_instance(request["id"], self._ethernet_switches) + if not ethsw: + return + + port = request["port"] + capture_file_name = request["capture_file_name"] + data_link_type = request.get("data_link_type") + + try: + capture_file_path = os.path.join(ethsw.hypervisor.working_dir, "captures", capture_file_name) + ethsw.start_capture(port, capture_file_path, data_link_type) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"], + "capture_file_path": capture_file_path} + self.send_response(response) + + @IModule.route("dynamips.ethsw.stop_capture") + def ethsw_stop_capture(self, request): + """ + Stops a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port_id (port identifier) + - port (port number) + + Response parameters: + - port_id (port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, ETHSW_STOP_CAPTURE_SCHEMA): + return + + # get the Ethernet switch instance + ethsw = self.get_device_instance(request["id"], self._ethernet_switches) + if not ethsw: + return + + port = request["port"] + try: + ethsw.stop_capture(port) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"]} + self.send_response(response) diff --git a/gns3server/modules/dynamips/backends/frsw.py b/gns3server/modules/dynamips/backends/frsw.py index 5bbf72a21..ed63f5017 100644 --- a/gns3server/modules/dynamips/backends/frsw.py +++ b/gns3server/modules/dynamips/backends/frsw.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os from gns3server.modules import IModule from ..nodes.frame_relay_switch import FrameRelaySwitch from ..dynamips_error import DynamipsError @@ -25,6 +26,8 @@ from ..schemas.frsw import FRSW_UPDATE_SCHEMA from ..schemas.frsw import FRSW_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.frsw import FRSW_ADD_NIO_SCHEMA from ..schemas.frsw import FRSW_DELETE_NIO_SCHEMA +from ..schemas.frsw import FRSW_START_CAPTURE_SCHEMA +from ..schemas.frsw import FRSW_STOP_CAPTURE_SCHEMA import logging log = logging.getLogger(__name__) @@ -37,7 +40,7 @@ class FRSW(object): """ Creates a new Frame-Relay switch. - Optional request parameters: + Mandatory request parameters: - name (switch name) Response parameters: @@ -48,13 +51,10 @@ class FRSW(object): """ # validate the request - if request and not self.validate_request(request, FRSW_CREATE_SCHEMA): + if not self.validate_request(request, FRSW_CREATE_SCHEMA): return - name = None - if request and "name" in request: - name = request["name"] - + name = request["name"] try: if not self._hypervisor_manager: self.start_hypervisor_manager() @@ -292,3 +292,83 @@ class FRSW(object): return self.send_response(True) + + @IModule.route("dynamips.frsw.start_capture") + def frsw_start_capture(self, request): + """ + Starts a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port (port identifier) + - port_id (port identifier) + - capture_file_name + + Optional request parameters: + - data_link_type (PCAP DLT_* value) + + Response parameters: + - port_id (port identifier) + - capture_file_path (path to the capture file) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, FRSW_START_CAPTURE_SCHEMA): + return + + # get the Frame relay switch instance + frsw = self.get_device_instance(request["id"], self._frame_relay_switches) + if not frsw: + return + + port = request["port"] + capture_file_name = request["capture_file_name"] + data_link_type = request.get("data_link_type") + + try: + capture_file_path = os.path.join(frsw.hypervisor.working_dir, "captures", capture_file_name) + frsw.start_capture(port, capture_file_path, data_link_type) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"], + "capture_file_path": capture_file_path} + self.send_response(response) + + @IModule.route("dynamips.frsw.stop_capture") + def frsw_stop_capture(self, request): + """ + Stops a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port_id (port identifier) + - port (port number) + + Response parameters: + - port_id (port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, FRSW_STOP_CAPTURE_SCHEMA): + return + + # get the Frame relay switch instance + frsw = self.get_device_instance(request["id"], self._frame_relay_switches) + if not frsw: + return + + port = request["port"] + try: + frsw.stop_capture(port) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"]} + self.send_response(response) diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py index 9b6055ee5..04e5fb75a 100644 --- a/gns3server/modules/dynamips/backends/vm.py +++ b/gns3server/modules/dynamips/backends/vm.py @@ -56,6 +56,8 @@ from ..schemas.vm import VM_STOP_SCHEMA from ..schemas.vm import VM_SUSPEND_SCHEMA from ..schemas.vm import VM_RELOAD_SCHEMA from ..schemas.vm import VM_UPDATE_SCHEMA +from ..schemas.vm import VM_START_CAPTURE_SCHEMA +from ..schemas.vm import VM_STOP_CAPTURE_SCHEMA from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA from ..schemas.vm import VM_IDLEPCS_SCHEMA from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA @@ -105,12 +107,12 @@ class VM(object): Creates a new VM (router). Mandatory request parameters: + - name (vm name) - platform (platform name e.g. c7200) - image (path to IOS image) - ram (amount of RAM in MB) Optional request parameters: - - name (vm name) - console (console port number) - aux (auxiliary console port number) - mac_addr (MAC address) @@ -127,16 +129,13 @@ class VM(object): if not self.validate_request(request, VM_CREATE_SCHEMA): return - name = None - if "name" in request: - name = request["name"] + name = request["name"] platform = request["platform"] image = request["image"] ram = request["ram"] hypervisor = None - chassis = None - if "chassis" in request: - chassis = request["chassis"] + chassis = request.get("chassis") + router_id = request.get("router_id") try: @@ -149,9 +148,9 @@ class VM(object): hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram) if chassis: - router = PLATFORMS[platform](hypervisor, name, chassis=chassis) + router = PLATFORMS[platform](hypervisor, name, router_id, chassis=chassis) else: - router = PLATFORMS[platform](hypervisor, name) + router = PLATFORMS[platform](hypervisor, name, router_id) router.ram = ram router.image = image router.sparsemem = self._hypervisor_manager.sparse_memory_support @@ -390,8 +389,8 @@ class VM(object): response = {} try: - startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "{}.cfg".format(router.name)) - private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "{}-private.cfg".format(router.name)) + startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id)) + private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id)) # a new startup-config has been pushed if "startup_config_base64" in request: @@ -479,10 +478,98 @@ class VM(object): # Update the ghost IOS file in case the RAM size has changed if self._hypervisor_manager.ghost_ios_support: - self.set_ghost_ios(router) + try: + self.set_ghost_ios(router) + except DynamipsError as e: + self.send_custom_error(str(e)) + return self.send_response(response) + @IModule.route("dynamips.vm.start_capture") + def vm_start_capture(self, request): + """ + Starts a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port_id (port identifier) + - slot (slot number) + - port (port number) + - capture_file_name + + Optional request parameters: + - data_link_type (PCAP DLT_* value) + + Response parameters: + - port_id (port identifier) + - capture_file_path (path to the capture file) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VM_START_CAPTURE_SCHEMA): + return + + # get the router instance + router = self.get_device_instance(request["id"], self._routers) + if not router: + return + + slot = request["slot"] + port = request["port"] + capture_file_name = request["capture_file_name"] + data_link_type = request.get("data_link_type") + + try: + capture_file_path = os.path.join(router.hypervisor.working_dir, "captures", capture_file_name) + router.start_capture(slot, port, capture_file_path, data_link_type) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"], + "capture_file_path": capture_file_path} + self.send_response(response) + + @IModule.route("dynamips.vm.stop_capture") + def vm_stop_capture(self, request): + """ + Stops a packet capture. + + Mandatory request parameters: + - id (vm identifier) + - port_id (port identifier) + - slot (slot number) + - port (port number) + + Response parameters: + - port_id (port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, VM_STOP_CAPTURE_SCHEMA): + return + + # get the router instance + router = self.get_device_instance(request["id"], self._routers) + if not router: + return + + slot = request["slot"] + port = request["port"] + try: + router.stop_capture(slot, port) + except DynamipsError as e: + self.send_custom_error(str(e)) + return + + response = {"port_id": request["port_id"]} + self.send_response(response) + @IModule.route("dynamips.vm.save_config") def vm_save_config(self, request): """ diff --git a/gns3server/modules/dynamips/dynamips_hypervisor.py b/gns3server/modules/dynamips/dynamips_hypervisor.py index 0a53b64b0..0770ff631 100644 --- a/gns3server/modules/dynamips/dynamips_hypervisor.py +++ b/gns3server/modules/dynamips/dynamips_hypervisor.py @@ -61,7 +61,7 @@ class DynamipsHypervisor(object): self._udp_end_port_range = 20000 self._nio_udp_auto_instances = {} self._version = "N/A" - self._timeout = 30 + self._timeout = timeout self._socket = None self._uuid = None @@ -80,9 +80,7 @@ class DynamipsHypervisor(object): host = self._host try: - self._socket = socket.create_connection((host, - self._port), - self._timeout) + self._socket = socket.create_connection((host, self._port), self._timeout) except OSError as e: raise DynamipsError("Could not connect to server: {}".format(e)) @@ -477,7 +475,7 @@ class DynamipsHypervisor(object): self.socket.sendall(command.encode('utf-8')) except OSError as e: raise DynamipsError("Lost communication with {host}:{port} :{error}" - .format(host=self._host, port=self._port, error=e)) + .format(host=self._host, port=self._port, error=e)) # Now retrieve the result data = [] @@ -488,7 +486,7 @@ class DynamipsHypervisor(object): buf += chunk.decode("utf-8") except OSError as e: raise DynamipsError("Communication timed out with {host}:{port} :{error}" - .format(host=self._host, port=self._port, error=e)) + .format(host=self._host, port=self._port, error=e)) # If the buffer doesn't end in '\n' then we can't be done try: diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 911874e03..20e49741a 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -70,7 +70,7 @@ class Hypervisor(DynamipsHypervisor): :returns: id (integer) """ - return(self._id) + return self._id @property def started(self): @@ -90,7 +90,7 @@ class Hypervisor(DynamipsHypervisor): :returns: path to Dynamips """ - return(self._path) + return self._path @path.setter def path(self, path): @@ -110,7 +110,7 @@ class Hypervisor(DynamipsHypervisor): :returns: port number (integer) """ - return(self._port) + return self._port @port.setter def port(self, port): @@ -130,7 +130,7 @@ class Hypervisor(DynamipsHypervisor): :returns: host/address (string) """ - return(self._host) + return self._host @host.setter def host(self, host): @@ -232,7 +232,7 @@ class Hypervisor(DynamipsHypervisor): self._process.wait(1) except subprocess.TimeoutExpired: self._process.kill() - if self._process.poll() == None: + if self._process.poll() is None: log.warn("Dynamips process {} is still running".format(self._process.pid)) if self._stdout_file and os.access(self._stdout_file, os.W_OK): @@ -251,7 +251,7 @@ class Hypervisor(DynamipsHypervisor): output = "" if self._stdout_file and os.access(self._stdout_file, os.R_OK): try: - with open(self._stdout_file) as file: + with open(self._stdout_file, errors="replace") as file: output = file.read() except OSError as e: log.warn("could not read {}: {}".format(self._stdout_file, e)) @@ -264,7 +264,7 @@ class Hypervisor(DynamipsHypervisor): :returns: True or False """ - if self._process and self._process.poll() == None: + if self._process and self._process.poll() is None: return True return False diff --git a/gns3server/modules/dynamips/hypervisor_manager.py b/gns3server/modules/dynamips/hypervisor_manager.py index 1db9f285b..7c8153c43 100644 --- a/gns3server/modules/dynamips/hypervisor_manager.py +++ b/gns3server/modules/dynamips/hypervisor_manager.py @@ -22,10 +22,10 @@ Manages Dynamips hypervisors (load-balancing etc.) from .hypervisor import Hypervisor from .dynamips_error import DynamipsError from ..attic import find_unused_port +from ..attic import wait_socket_is_ready from pkg_resources import parse_version import os -import socket import time import logging @@ -39,10 +39,6 @@ class HypervisorManager(object): :param path: path to the Dynamips executable :param working_dir: path to a working directory :param host: host/address for hypervisors to listen to - :param base_port: base TCP port for hypervisors - :param base_console: base TCP port for consoles - :param base_aux: base TCP port for auxiliary consoles - :param base_udp: base UDP port for UDP tunnels """ def __init__(self, path, working_dir, host='127.0.0.1'): @@ -504,35 +500,17 @@ class HypervisorManager(object): else: log.info("allocating an hypervisor per IOS image disabled") - def wait_for_hypervisor(self, host, port, timeout=10): + def wait_for_hypervisor(self, host, port): """ Waits for an hypervisor to be started (accepting a socket connection) :param host: host/address to connect to the hypervisor :param port: port to connect to the hypervisor - :param timeout: timeout value (default is 10 seconds) """ - # connect to a local address by default - # if listening to all addresses (IPv4 or IPv6) - if host == "0.0.0.0": - host = "127.0.0.1" - elif host == "::": - host = "::1" - - connection_success = False begin = time.time() - # try to connect for 10 seconds - while(time.time() - begin < 10.0): - time.sleep(0.01) - try: - with socket.create_connection((host, port), timeout): - pass - except OSError as e: - last_exception = e - continue - connection_success = True - break + # wait for the socket for a maximum of 10 seconds. + connection_success, last_exception = wait_socket_is_ready(host, port, wait=10.0) if not connection_success: # FIXME: throw exception here diff --git a/gns3server/modules/dynamips/nios/nio.py b/gns3server/modules/dynamips/nios/nio.py index f27b7e735..1fd61bf9e 100644 --- a/gns3server/modules/dynamips/nios/nio.py +++ b/gns3server/modules/dynamips/nios/nio.py @@ -57,6 +57,8 @@ class NIO(object): Deletes this NIO. """ + if self._input_filter or self._output_filter: + self.unbind_filter("both") self._hypervisor.send("nio delete {}".format(self._name)) log.info("NIO {name} has been deleted".format(name=self._name)) @@ -134,7 +136,7 @@ class NIO(object): def setup_filter(self, direction, options): """ - Setups a packet filter binded with this NIO. + Setups a packet filter bound with this NIO. Filter "freq_drop" has 1 argument "". It will drop everything with a -1 frequency, drop every Nth packet with a @@ -174,7 +176,7 @@ class NIO(object): :returns: tuple (filter name, filter options) """ - return (self._input_filter, self._input_filter_options) + return self._input_filter, self._input_filter_options @property def output_filter(self): @@ -184,7 +186,7 @@ class NIO(object): :returns: tuple (filter name, filter options) """ - return (self._output_filter, self._output_filter_options) + return self._output_filter, self._output_filter_options def get_stats(self): """ diff --git a/gns3server/modules/dynamips/nodes/atm_bridge.py b/gns3server/modules/dynamips/nodes/atm_bridge.py index 036cfb5d1..10abe1b2e 100644 --- a/gns3server/modules/dynamips/nodes/atm_bridge.py +++ b/gns3server/modules/dynamips/nodes/atm_bridge.py @@ -33,6 +33,7 @@ class ATMBridge(object): def __init__(self, hypervisor, name): + #FIXME: instance tracking self._hypervisor = hypervisor self._name = '"' + name + '"' # put name into quotes to protect spaces self._hypervisor.send("atm_bridge create {}".format(self._name)) diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index eecd9544e..0c382c441 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual ATM switch module ("atmsw"). http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593 """ +import os from ..dynamips_error import DynamipsError import logging @@ -34,26 +35,21 @@ class ATMSwitch(object): :param name: name for this switch """ - _allocated_names = [] - _instance_count = 1 + _instances = [] - def __init__(self, hypervisor, name=None): + def __init__(self, hypervisor, name): - # create an unique ID - self._id = ATMSwitch._instance_count - ATMSwitch._instance_count += 1 + # find an instance identifier (0 < id <= 4096) + self._id = 0 + for identifier in range(1, 4097): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break - # let's create a unique name if none has been chosen - if not name: - name_id = self._id - while True: - name = "ATM" + str(name_id) - # check if the name has already been allocated to another switch - if name not in self._allocated_names: - break - name_id += 1 + if self._id == 0: + raise DynamipsError("Maximum number of instances reached") - self._allocated_names.append(name) self._hypervisor = hypervisor self._name = '"' + name + '"' # put name into quotes to protect spaces self._hypervisor.send("atmsw create {}".format(self._name)) @@ -68,11 +64,10 @@ class ATMSwitch(object): @classmethod def reset(cls): """ - Resets the instance count and the allocated names list. + Resets the instance count and the allocated instances list. """ - cls._instance_count = 1 - cls._allocated_names.clear() + cls._instances.clear() @property def id(self): @@ -102,7 +97,6 @@ class ATMSwitch(object): :param new_name: New name for this switch """ - new_name_no_quotes = new_name new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces self._hypervisor.send("atmsw rename {name} {new_name}".format(name=self._name, new_name=new_name)) @@ -111,9 +105,7 @@ class ATMSwitch(object): id=self._id, new_name=new_name)) - self._allocated_names.remove(self.name) self._name = new_name - self._allocated_names.append(new_name_no_quotes) @property def hypervisor(self): @@ -164,7 +156,7 @@ class ATMSwitch(object): log.info("ATM switch {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) self._hypervisor.devices.remove(self) - self._allocated_names.remove(self.name) + self._instances.remove(self._id) def has_port(self, port): """ @@ -360,3 +352,54 @@ class ATMSwitch(object): vpi2=vpi2, vci2=vci2)) del self._mapping[(port1, vpi1, vci1)] + + def start_capture(self, port, output_file, data_link_type="DLT_ATM_RFC1483"): + """ + Starts a packet capture. + + :param port: allocated port + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483 + """ + + if port not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._nios[port] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise DynamipsError("Could not create captures directory {}".format(e)) + + nio.bind_filter("both", "capture") + nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info("ATM switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) + + def stop_capture(self, port): + """ + Stops a packet capture. + + :param port: allocated port + """ + + if port not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._nios[port] + nio.unbind_filter("both") + log.info("ATM switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py index a47bf6548..a87ba029d 100644 --- a/gns3server/modules/dynamips/nodes/bridge.py +++ b/gns3server/modules/dynamips/nodes/bridge.py @@ -29,12 +29,9 @@ class Bridge(object): :param name: name for this bridge """ - _allocated_names = [] - def __init__(self, hypervisor, name): self._hypervisor = hypervisor - self._allocated_names.append(name) self._name = '"' + name + '"' # put name into quotes to protect spaces self._hypervisor.send("nio_bridge create {}".format(self._name)) self._hypervisor.devices.append(self) @@ -58,14 +55,11 @@ class Bridge(object): :param new_name: New name for this bridge """ - new_name_no_quotes = new_name new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces self._hypervisor.send("nio_bridge rename {name} {new_name}".format(name=self._name, new_name=new_name)) - self._allocated_names.remove(self.name) self._name = new_name - self._allocated_names.append(new_name_no_quotes) @property def hypervisor(self): @@ -103,7 +97,6 @@ class Bridge(object): self._hypervisor.send("nio_bridge delete {}".format(self._name)) self._hypervisor.devices.remove(self) - self._allocated_names.remove(self.name) def add_nio(self, nio): """ diff --git a/gns3server/modules/dynamips/nodes/c1700.py b/gns3server/modules/dynamips/nodes/c1700.py index 51318790f..0d59f616e 100644 --- a/gns3server/modules/dynamips/nodes/c1700.py +++ b/gns3server/modules/dynamips/nodes/c1700.py @@ -34,13 +34,14 @@ class C1700(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID :param chassis: chassis for this router: 1720, 1721, 1750, 1751 or 1760 (default = 1720). 1710 is not supported. """ - def __init__(self, hypervisor, name=None, chassis="1720"): - Router.__init__(self, hypervisor, name, platform="c1700") + def __init__(self, hypervisor, name, router_id=None, chassis="1720"): + Router.__init__(self, hypervisor, name, router_id, platform="c1700") # Set default values for this platform self._ram = 64 diff --git a/gns3server/modules/dynamips/nodes/c2600.py b/gns3server/modules/dynamips/nodes/c2600.py index e7f5b61a3..155fbf2f3 100644 --- a/gns3server/modules/dynamips/nodes/c2600.py +++ b/gns3server/modules/dynamips/nodes/c2600.py @@ -36,6 +36,7 @@ class C2600(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID :param chassis: chassis for this router: 2610, 2611, 2620, 2621, 2610XM, 2611XM 2620XM, 2621XM, 2650XM or 2651XM (default = 2610). @@ -54,8 +55,8 @@ class C2600(Router): "2650XM": C2600_MB_1FE, "2651XM": C2600_MB_2FE} - def __init__(self, hypervisor, name=None, chassis="2610"): - Router.__init__(self, hypervisor, name, platform="c2600") + def __init__(self, hypervisor, name, router_id=None, chassis="2610"): + Router.__init__(self, hypervisor, name, router_id, platform="c2600") # Set default values for this platform self._ram = 64 diff --git a/gns3server/modules/dynamips/nodes/c2691.py b/gns3server/modules/dynamips/nodes/c2691.py index 9ba7e396d..339fada94 100644 --- a/gns3server/modules/dynamips/nodes/c2691.py +++ b/gns3server/modules/dynamips/nodes/c2691.py @@ -33,10 +33,11 @@ class C2691(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID """ - def __init__(self, hypervisor, name=None): - Router.__init__(self, hypervisor, name, platform="c2691") + def __init__(self, hypervisor, name, router_id=None): + Router.__init__(self, hypervisor, name, router_id, platform="c2691") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3600.py b/gns3server/modules/dynamips/nodes/c3600.py index ccbd565c6..b0117a163 100644 --- a/gns3server/modules/dynamips/nodes/c3600.py +++ b/gns3server/modules/dynamips/nodes/c3600.py @@ -33,12 +33,13 @@ class C3600(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID :param chassis: chassis for this router: 3620, 3640 or 3660 (default = 3640). """ - def __init__(self, hypervisor, name=None, chassis="3640"): - Router.__init__(self, hypervisor, name, platform="c3600") + def __init__(self, hypervisor, name, router_id=None, chassis="3640"): + Router.__init__(self, hypervisor, name, router_id, platform="c3600") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3725.py b/gns3server/modules/dynamips/nodes/c3725.py index d32fab1b5..9317a393d 100644 --- a/gns3server/modules/dynamips/nodes/c3725.py +++ b/gns3server/modules/dynamips/nodes/c3725.py @@ -33,10 +33,11 @@ class C3725(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID """ - def __init__(self, hypervisor, name=None): - Router.__init__(self, hypervisor, name, platform="c3725") + def __init__(self, hypervisor, name, router_id=None): + Router.__init__(self, hypervisor, name, router_id, platform="c3725") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c3745.py b/gns3server/modules/dynamips/nodes/c3745.py index 5c0ea13cb..f8392cc3b 100644 --- a/gns3server/modules/dynamips/nodes/c3745.py +++ b/gns3server/modules/dynamips/nodes/c3745.py @@ -33,10 +33,11 @@ class C3745(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID """ - def __init__(self, hypervisor, name=None): - Router.__init__(self, hypervisor, name, platform="c3745") + def __init__(self, hypervisor, name, router_id=None): + Router.__init__(self, hypervisor, name, router_id, platform="c3745") # Set default values for this platform self._ram = 128 diff --git a/gns3server/modules/dynamips/nodes/c7200.py b/gns3server/modules/dynamips/nodes/c7200.py index 0307eedc8..0dd7127b6 100644 --- a/gns3server/modules/dynamips/nodes/c7200.py +++ b/gns3server/modules/dynamips/nodes/c7200.py @@ -35,11 +35,12 @@ class C7200(Router): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID :param npe: default NPE """ - def __init__(self, hypervisor, name=None, npe="npe-400"): - Router.__init__(self, hypervisor, name, platform="c7200") + def __init__(self, hypervisor, name, router_id=None, npe="npe-400"): + Router.__init__(self, hypervisor, name, router_id, platform="c7200") # Set default values for this platform self._ram = 256 diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py index 83ddf0800..45cc25c02 100644 --- a/gns3server/modules/dynamips/nodes/ethernet_switch.py +++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Ethernet switch module ("ethsw"). http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 """ +import os from ..dynamips_error import DynamipsError import logging @@ -34,26 +35,21 @@ class EthernetSwitch(object): :param name: name for this switch """ - _allocated_names = [] - _instance_count = 1 + _instances = [] - def __init__(self, hypervisor, name=None): + def __init__(self, hypervisor, name): - # create an unique ID - self._id = EthernetSwitch._instance_count - EthernetSwitch._instance_count += 1 + # find an instance identifier (0 < id <= 4096) + self._id = 0 + for identifier in range(1, 4097): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break - # let's create a unique name if none has been chosen - if not name: - name_id = self._id - while True: - name = "SW" + str(name_id) - # check if the name has already been allocated to another switch - if name not in self._allocated_names: - break - name_id += 1 + if self._id == 0: + raise DynamipsError("Maximum number of instances reached") - self._allocated_names.append(name) self._hypervisor = hypervisor self._name = '"' + name + '"' # put name into quotes to protect spaces self._hypervisor.send("ethsw create {}".format(self._name)) @@ -68,11 +64,10 @@ class EthernetSwitch(object): @classmethod def reset(cls): """ - Resets the instance count and the allocated names list. + Resets the instance count and the allocated instances list. """ - cls._instance_count = 1 - cls._allocated_names.clear() + cls._instances.clear() @property def id(self): @@ -102,7 +97,6 @@ class EthernetSwitch(object): :param new_name: New name for this switch """ - new_name_no_quotes = new_name new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces self._hypervisor.send("ethsw rename {name} {new_name}".format(name=self._name, new_name=new_name)) @@ -111,9 +105,7 @@ class EthernetSwitch(object): id=self._id, new_name=new_name)) - self._allocated_names.remove(self.name) self._name = new_name - self._allocated_names.append(new_name_no_quotes) @property def hypervisor(self): @@ -164,7 +156,7 @@ class EthernetSwitch(object): log.info("Ethernet switch {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) self._hypervisor.devices.remove(self) - self._allocated_names.remove(self.name) + self._instances.remove(self._id) def add_nio(self, nio, port): """ @@ -296,3 +288,54 @@ class EthernetSwitch(object): """ self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name)) + + def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + + :param port: allocated port + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + if port not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._nios[port] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise DynamipsError("Could not create captures directory {}".format(e)) + + nio.bind_filter("both", "capture") + nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info("Ethernet switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) + + def stop_capture(self, port): + """ + Stops a packet capture. + + :param port: allocated port + """ + + if port not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._nios[port] + nio.unbind_filter("both") + log.info("Ethernet switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index 8a1eec31f..0b44fbeaa 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -20,6 +20,7 @@ Interface for Dynamips virtual Frame-Relay switch module. http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642 """ +import os from ..dynamips_error import DynamipsError import logging @@ -34,26 +35,21 @@ class FrameRelaySwitch(object): :param name: name for this switch """ - _allocated_names = [] - _instance_count = 1 + _instances = [] - def __init__(self, hypervisor, name=None): + def __init__(self, hypervisor, name): - # create an unique ID - self._id = FrameRelaySwitch._instance_count - FrameRelaySwitch._instance_count += 1 + # find an instance identifier (0 < id <= 4096) + self._id = 0 + for identifier in range(1, 4097): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break - # let's create a unique name if none has been chosen - if not name: - name_id = self._id - while True: - name = "FR" + str(name_id) - # check if the name has already been allocated to another switch - if name not in self._allocated_names: - break - name_id += 1 + if self._id == 0: + raise DynamipsError("Maximum number of instances reached") - self._allocated_names.append(name) self._hypervisor = hypervisor self._name = '"' + name + '"' # put name into quotes to protect spaces self._hypervisor.send("frsw create {}".format(self._name)) @@ -68,11 +64,10 @@ class FrameRelaySwitch(object): @classmethod def reset(cls): """ - Resets the instance count and the allocated names list. + Resets the instance count and the allocated instances list. """ - cls._instance_count = 1 - cls._allocated_names.clear() + cls._instances.clear() @property def id(self): @@ -102,7 +97,6 @@ class FrameRelaySwitch(object): :param new_name: New name for this switch """ - new_name_no_quotes = new_name new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces self._hypervisor.send("frsw rename {name} {new_name}".format(name=self._name, new_name=new_name)) @@ -111,9 +105,7 @@ class FrameRelaySwitch(object): id=self._id, new_name=new_name)) - self._allocated_names.remove(self.name) self._name = new_name - self._allocated_names.append(new_name_no_quotes) @property def hypervisor(self): @@ -164,7 +156,7 @@ class FrameRelaySwitch(object): log.info("Frame Relay switch {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) self._hypervisor.devices.remove(self) - self._allocated_names.remove(self.name) + self._instances.remove(self._id) def has_port(self, port): """ @@ -282,3 +274,54 @@ class FrameRelaySwitch(object): port2=port2, dlci2=dlci2)) del self._mapping[(port1, dlci1)] + + def start_capture(self, port, output_file, data_link_type="DLT_FRELAY"): + """ + Starts a packet capture. + + :param port: allocated port + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY + """ + + if port not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._nios[port] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise DynamipsError("Could not create captures directory {}".format(e)) + + nio.bind_filter("both", "capture") + nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info("Frame relay switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) + + def stop_capture(self, port): + """ + Stops a packet capture. + + :param port: allocated port + """ + + if port not in self._nios: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._nios[port] + nio.unbind_filter("both") + log.info("Frame relay switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) diff --git a/gns3server/modules/dynamips/nodes/hub.py b/gns3server/modules/dynamips/nodes/hub.py index b66a54889..6f7f0e592 100644 --- a/gns3server/modules/dynamips/nodes/hub.py +++ b/gns3server/modules/dynamips/nodes/hub.py @@ -19,6 +19,7 @@ Hub object that uses the Bridge interface to create a hub with ports. """ +import os from .bridge import Bridge from ..dynamips_error import DynamipsError @@ -34,23 +35,20 @@ class Hub(Bridge): :param name: name for this hub """ - _instance_count = 1 + _instances = [] def __init__(self, hypervisor, name): - # create an unique ID - self._id = Hub._instance_count - Hub._instance_count += 1 + # find an instance identifier (0 < id <= 4096) + self._id = 0 + for identifier in range(1, 4097): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break - # let's create a unique name if none has been chosen - if not name: - name_id = self._id - while True: - name = "Hub" + str(name_id) - # check if the name has already been allocated to another switch - if name not in self._allocated_names: - break - name_id += 1 + if self._id == 0: + raise DynamipsError("Maximum number of instances reached") self._mapping = {} Bridge.__init__(self, hypervisor, name) @@ -61,11 +59,10 @@ class Hub(Bridge): @classmethod def reset(cls): """ - Resets the instance count and the allocated names list. + Resets the instance count and the allocated instances list. """ - cls._instance_count = 1 - cls._allocated_names.clear() + cls._instances.clear() @property def id(self): @@ -95,6 +92,7 @@ class Hub(Bridge): Bridge.delete(self) log.info("Ethernet hub {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) + self._instances.remove(self._id) def add_nio(self, nio, port): """ @@ -137,3 +135,54 @@ class Hub(Bridge): del self._mapping[port] return nio + + def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + + :param port: allocated port + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + if port not in self._mapping: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._mapping[port] + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {} has already a filter applied".format(port)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise DynamipsError("Could not create captures directory {}".format(e)) + + nio.bind_filter("both", "capture") + nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info("Ethernet hub {name} [id={id}]: starting packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) + + def stop_capture(self, port): + """ + Stops a packet capture. + + :param port: allocated port + """ + + if port not in self._mapping: + raise DynamipsError("Port {} is not allocated".format(port)) + + nio = self._mapping[port] + nio.unbind_filter("both") + log.info("Ethernet hub {name} [id={id}]: stopping packet capture on {port}".format(name=self._name, + id=self._id, + port=port)) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 0534d926b..9c08ec778 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -37,41 +37,45 @@ class Router(object): :param hypervisor: Dynamips hypervisor instance :param name: name for this router + :param router_id: router instance ID :param platform: c7200, c3745, c3725, c3600, c2691, c2600 or c1700 :param ghost_flag: used when creating a ghost IOS. """ - _allocated_names = [] + _instances = [] _allocated_console_ports = [] _allocated_aux_ports = [] - _instance_count = 1 _status = {0: "inactive", 1: "shutting down", 2: "running", 3: "suspended"} - def __init__(self, hypervisor, name=None, platform="c7200", ghost_flag=False): + def __init__(self, hypervisor, name, router_id=None, platform="c7200", ghost_flag=False): if not ghost_flag: - # create an unique ID - self._id = Router._instance_count - Router._instance_count += 1 - # let's create a unique name if none has been chosen - if not name: - name_id = self._id - while True: - name = "R" + str(name_id) - # check if the name has already been allocated to another router - if name not in self._allocated_names: + if not router_id: + # find an instance identifier if none is provided (0 < id <= 4096) + self._id = 0 + for identifier in range(1, 4097): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) break - name_id += 1 + + if self._id == 0: + raise DynamipsError("Maximum number of instances reached") + else: + if router_id in self._instances: + raise DynamipsError("Router identifier {} is already used by another router".format(router_id)) + self._id = router_id + self._instances.append(self._id) + else: log.info("creating a new ghost IOS file") self._id = 0 name = "Ghost" - self._allocated_names.append(name) self._hypervisor = hypervisor self._name = '"' + name + '"' # put name into quotes to protect spaces self._platform = platform @@ -137,18 +141,17 @@ class Router(object): # get the default base MAC address self._mac_addr = self._hypervisor.send("{platform} get_mac_addr {name}".format(platform=self._platform, - name=self._name))[0] + name=self._name))[0] self._hypervisor.devices.append(self) @classmethod def reset(cls): """ - Resets the instance count and the allocated names list. + Resets the instance count and the allocated instances list. """ - cls._instance_count = 1 - cls._allocated_names.clear() + cls._instances.clear() cls._allocated_console_ports.clear() cls._allocated_aux_ports.clear() @@ -222,10 +225,32 @@ class Router(object): :param new_name: new name string """ - if new_name in self._allocated_names: - raise DynamipsError('Name "{}" is already used by another router'.format(new_name)) + if self._startup_config: + # change the hostname in the startup-config + startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id)) + if os.path.isfile(startup_config_path): + try: + with open(startup_config_path, "r+", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self.name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e)) + + if self._private_config: + # change the hostname in the private-config + private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id)) + if os.path.isfile(private_config_path): + try: + with open(private_config_path, "r+", errors="replace") as f: + old_config = f.read() + new_config = old_config.replace(self.name, new_name) + f.seek(0) + f.write(new_config) + except OSError as e: + raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e)) - new_name_no_quotes = new_name new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name, new_name=new_name)) @@ -233,10 +258,7 @@ class Router(object): log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name, id=self._id, new_name=new_name)) - - self._allocated_names.remove(self.name) self._name = new_name - self._allocated_names.append(new_name_no_quotes) @property def platform(self): @@ -284,9 +306,9 @@ class Router(object): self._hypervisor.send("vm delete {}".format(self._name)) self._hypervisor.devices.remove(self) - log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) - self._allocated_names.remove(self.name) + if self._id in self._instances: + self._instances.remove(self._id) if self.console: self._allocated_console_ports.remove(self.console) if self.aux: @@ -300,8 +322,21 @@ class Router(object): self._hypervisor.send("vm clean_delete {}".format(self._name)) self._hypervisor.devices.remove(self) + if self._startup_config: + # delete the startup-config + startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name)) + if os.path.isfile(startup_config_path): + os.remove(startup_config_path) + + if self._private_config: + # delete the private-config + private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name)) + if os.path.isfile(private_config_path): + os.remove(private_config_path) + log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id)) - self._allocated_names.remove(self.name) + if self._id in self._instances: + self._instances.remove(self._id) if self.console: self._allocated_console_ports.remove(self.console) if self.aux: @@ -313,9 +348,10 @@ class Router(object): At least the IOS image must be set before starting it. """ - if self.get_status() == "suspended": + status = self.get_status() + if status == "suspended": self.resume() - else: + elif status == "inactive": if not os.path.isfile(self._image): raise DynamipsError("IOS image '{}' is not accessible".format(self._image)) @@ -329,7 +365,7 @@ class Router(object): # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 if elf_header_start != b'\x7fELF\x01\x02\x01': - raise DynamipsError("'{}' is not a valid IOU image".format(self._image)) + raise DynamipsError("'{}' is not a valid IOS image".format(self._image)) self._hypervisor.send("vm start {}".format(self._name)) log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id)) @@ -340,8 +376,9 @@ class Router(object): The settings are kept. """ - self._hypervisor.send("vm stop {}".format(self._name)) - log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id)) + if self.get_status() != "inactive": + self._hypervisor.send("vm stop {}".format(self._name)) + log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id)) def suspend(self): """ @@ -535,10 +572,10 @@ class Router(object): reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] except IOError: #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. - return (None, None) + return None, None startup_config = reply[0][1:-1] # get statup-config and remove single quotes private_config = reply[1][1:-1] # get private-config and remove single quotes - return (startup_config, private_config) + return startup_config, private_config def push_config(self, startup_config, private_config='(keep)'): """ @@ -681,7 +718,7 @@ class Router(object): else: flag = 0 self._hypervisor.send("vm set_sparse_mem {name} {sparsemem}".format(name=self._name, - sparsemem=flag)) + sparsemem=flag)) if sparsemem: log.info("router {name} [id={id}]: sparse memory enabled".format(name=self._name, @@ -941,7 +978,7 @@ class Router(object): translated by the JIT (they contain the native code corresponding to MIPS code pages). - :param excec_area: exec area value (integer) + :param exec_area: exec area value (integer) """ self._hypervisor.send("vm set_exec_area {name} {exec_area}".format(name=self._name, @@ -1222,7 +1259,7 @@ class Router(object): :returns: slot bindings (adapter names) list """ - return (self._hypervisor.send("vm slot_bindings {}".format(self._name))) + return self._hypervisor.send("vm slot_bindings {}".format(self._name)) def slot_add_binding(self, slot_id, adapter): """ @@ -1238,16 +1275,16 @@ class Router(object): raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, slot_id=slot_id)) - if slot != None: + if slot is not None: current_adapter = slot raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router {name}".format(name=self._name, slot_id=slot_id, adapter=current_adapter)) # Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running - if self.is_running() and not (self._platform == 'c7200' \ - and not (self._platform == 'c3600' and self.chassis == '3660') \ - and not (self._platform == 'c3745' and adapter == 'NM-4T')): + if self.is_running() and not (self._platform == 'c7200' + and not (self._platform == 'c3600' and self.chassis == '3660') + and not (self._platform == 'c3745' and adapter == 'NM-4T')): raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter, name=self._name)) @@ -1285,14 +1322,14 @@ class Router(object): raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, slot_id=slot_id)) - if adapter == None: + if adapter is None: raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name, slot_id=slot_id)) # Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running - if self.is_running() and not (self._platform == 'c7200' \ - and not (self._platform == 'c3600' and self.chassis == '3660') \ - and not (self._platform == 'c3745' and adapter == 'NM-4T')): + if self.is_running() and not (self._platform == 'c7200' + and not (self._platform == 'c3600' and self.chassis == '3660') + and not (self._platform == 'c3745' and adapter == 'NM-4T')): raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter, name=self._name)) @@ -1378,8 +1415,8 @@ class Router(object): # WIC1 = 16, WIC2 = 32 and WIC3 = 48 internal_wic_slot_id = 16 * (wic_slot_id + 1) self._hypervisor.send("vm slot_remove_binding {name} {slot_id} {wic_slot_id}".format(name=self._name, - slot_id=slot_id, - wic_slot_id=internal_wic_slot_id)) + slot_id=slot_id, + wic_slot_id=internal_wic_slot_id)) log.info("router {name} [id={id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name, id=self._id, @@ -1502,6 +1539,77 @@ class Router(object): slot_id=slot_id, port_id=port_id)) + def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): + """ + Starts a packet capture. + + :param slot_id: slot ID + :param port_id: port ID + :param output_file: PCAP destination file for the capture + :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, + slot_id=slot_id)) + if not adapter.port_exists(port_id): + raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + data_link_type = data_link_type.lower() + if data_link_type.startswith("dlt_"): + data_link_type = data_link_type[4:] + + nio = adapter.get_nio(port_id) + + if nio.input_filter[0] is not None and nio.output_filter[0] is not None: + raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter, + port_id=port_id)) + + try: + os.makedirs(os.path.dirname(output_file)) + except FileExistsError: + pass + except OSError as e: + raise DynamipsError("Could not create captures directory {}".format(e)) + + nio.bind_filter("both", "capture") + nio.setup_filter("both", "{} {}".format(data_link_type, output_file)) + + log.info("router {name} [id={id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_id=slot_id, + port_id=port_id)) + + def stop_capture(self, slot_id, port_id): + """ + Stops a packet capture. + + :param slot_id: slot ID + :param port_id: port ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, + slot_id=slot_id)) + if not adapter.port_exists(port_id): + raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + nio.unbind_filter("both") + + log.info("router {name} [id={id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio_name=nio.name, + slot_id=slot_id, + port_id=port_id)) + def _create_slots(self, numslots): """ Creates the appropriate number of slots for this router. diff --git a/gns3server/modules/dynamips/schemas/atmsw.py b/gns3server/modules/dynamips/schemas/atmsw.py index 5d96e8c5b..376694783 100644 --- a/gns3server/modules/dynamips/schemas/atmsw.py +++ b/gns3server/modules/dynamips/schemas/atmsw.py @@ -24,8 +24,10 @@ ATMSW_CREATE_SCHEMA = { "description": "ATM switch name", "type": "string", "minLength": 1, - } - } + }, + }, + "additionalProperties": False, + "required": ["name"] } ATMSW_DELETE_SCHEMA = { @@ -38,6 +40,7 @@ ATMSW_DELETE_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -56,6 +59,7 @@ ATMSW_UPDATE_SCHEMA = { "minLength": 1, }, }, + "additionalProperties": False, "required": ["id"] } @@ -73,6 +77,7 @@ ATMSW_ALLOCATE_UDP_PORT_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id", "port_id"] } @@ -82,129 +87,129 @@ ATMSW_ADD_NIO_SCHEMA = { "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 + "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": { @@ -237,6 +242,7 @@ ATMSW_ADD_NIO_SCHEMA = { ] }, }, + "additionalProperties": False, "required": ["id", "port", "port_id", "mappings", "nio"], } @@ -255,5 +261,62 @@ ATMSW_DELETE_NIO_SCHEMA = { "minimum": 1, }, }, + "additionalProperties": False, "required": ["id", "port"] } + +ATMSW_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on an ATM switch instance port", + "type": "object", + "properties": { + "id": { + "description": "ATM switch instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the ATM switch instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "capture_file_name"] +} + +ATMSW_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on an ATM switch instance port", + "type": "object", + "properties": { + "id": { + "description": "ATM switch instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the ATM switch instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port"] +} diff --git a/gns3server/modules/dynamips/schemas/ethhub.py b/gns3server/modules/dynamips/schemas/ethhub.py index db1b1a299..1002a696d 100644 --- a/gns3server/modules/dynamips/schemas/ethhub.py +++ b/gns3server/modules/dynamips/schemas/ethhub.py @@ -24,8 +24,10 @@ ETHHUB_CREATE_SCHEMA = { "description": "Ethernet hub name", "type": "string", "minLength": 1, - } - } + }, + }, + "additionalProperties": False, + "required": ["name"] } ETHHUB_DELETE_SCHEMA = { @@ -38,6 +40,7 @@ ETHHUB_DELETE_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -56,6 +59,7 @@ ETHHUB_UPDATE_SCHEMA = { "minLength": 1, }, }, + "additionalProperties": False, "required": ["id"] } @@ -73,6 +77,7 @@ ETHHUB_ALLOCATE_UDP_PORT_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id", "port_id"] } @@ -82,129 +87,129 @@ ETHHUB_ADD_NIO_SCHEMA = { "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 + "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": { @@ -234,6 +239,7 @@ ETHHUB_ADD_NIO_SCHEMA = { ] }, }, + "additionalProperties": False, "required": ["id", "port_id", "port", "nio"] } @@ -252,5 +258,62 @@ ETHHUB_DELETE_NIO_SCHEMA = { "minimum": 1, }, }, + "additionalProperties": False, "required": ["id", "port"] } + +ETHHUB_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on an Ethernet hub instance port", + "type": "object", + "properties": { + "id": { + "description": "Ethernet hub instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet hub instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "capture_file_name"] +} + +ETHHUB_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on an Ethernet hub instance port", + "type": "object", + "properties": { + "id": { + "description": "Ethernet hub instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet hub instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port"] +} diff --git a/gns3server/modules/dynamips/schemas/ethsw.py b/gns3server/modules/dynamips/schemas/ethsw.py index c68a35651..aeac7023d 100644 --- a/gns3server/modules/dynamips/schemas/ethsw.py +++ b/gns3server/modules/dynamips/schemas/ethsw.py @@ -24,8 +24,10 @@ ETHSW_CREATE_SCHEMA = { "description": "Ethernet switch name", "type": "string", "minLength": 1, - } - } + }, + }, + "additionalProperties": False, + "required": ["name"] } ETHSW_DELETE_SCHEMA = { @@ -38,6 +40,7 @@ ETHSW_DELETE_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -71,6 +74,7 @@ ETHSW_UPDATE_SCHEMA = { # }, # }, }, + #"additionalProperties": False, "required": ["id"] } @@ -88,6 +92,7 @@ ETHSW_ALLOCATE_UDP_PORT_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id", "port_id"] } @@ -97,129 +102,129 @@ ETHSW_ADD_NIO_SCHEMA = { "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 + "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": { @@ -258,12 +263,13 @@ ETHSW_ADD_NIO_SCHEMA = { ] }, }, + "additionalProperties": False, "required": ["id", "port_id", "port", "port_type", "vlan", "nio"], "dependencies": { "port_type": ["vlan"], "vlan": ["port_type"] - } + } } ETHSW_DELETE_NIO_SCHEMA = { @@ -281,5 +287,62 @@ ETHSW_DELETE_NIO_SCHEMA = { "minimum": 1, }, }, + "additionalProperties": False, "required": ["id", "port"] } + +ETHSW_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on an Ethernet switch instance port", + "type": "object", + "properties": { + "id": { + "description": "Ethernet switch instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet switch instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "capture_file_name"] +} + +ETHSW_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on an Ethernet switch instance port", + "type": "object", + "properties": { + "id": { + "description": "Ethernet switch instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Ethernet switch instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port"] +} diff --git a/gns3server/modules/dynamips/schemas/frsw.py b/gns3server/modules/dynamips/schemas/frsw.py index 984d2c247..835e47a7b 100644 --- a/gns3server/modules/dynamips/schemas/frsw.py +++ b/gns3server/modules/dynamips/schemas/frsw.py @@ -24,8 +24,10 @@ FRSW_CREATE_SCHEMA = { "description": "Frame relay switch name", "type": "string", "minLength": 1, - } - } + }, + }, + "additionalProperties": False, + "required": ["name"] } FRSW_DELETE_SCHEMA = { @@ -38,6 +40,7 @@ FRSW_DELETE_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -56,6 +59,7 @@ FRSW_UPDATE_SCHEMA = { "minLength": 1, }, }, + "additionalProperties": False, "required": ["id"] } @@ -73,6 +77,7 @@ FRSW_ALLOCATE_UDP_PORT_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id", "port_id"] } @@ -82,129 +87,129 @@ FRSW_ADD_NIO_SCHEMA = { "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 + "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": { @@ -237,6 +242,7 @@ FRSW_ADD_NIO_SCHEMA = { ] }, }, + "additionalProperties": False, "required": ["id", "port", "port_id", "mappings", "nio"], } @@ -255,5 +261,62 @@ FRSW_DELETE_NIO_SCHEMA = { "minimum": 1, }, }, + "additionalProperties": False, "required": ["id", "port"] } + +FRSW_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a Frame relay switch instance port", + "type": "object", + "properties": { + "id": { + "description": "Frame relay switch instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Frame relay instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "capture_file_name"] +} + +FRSW_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on a Frame relay switch instance port", + "type": "object", + "properties": { + "id": { + "description": "Frame relay switch instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the Frame relay instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port"] +} diff --git a/gns3server/modules/dynamips/schemas/vm.py b/gns3server/modules/dynamips/schemas/vm.py index 7ab8e3fe5..241e059d5 100644 --- a/gns3server/modules/dynamips/schemas/vm.py +++ b/gns3server/modules/dynamips/schemas/vm.py @@ -25,6 +25,10 @@ VM_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, + "router_id": { + "description": "VM/router instance ID", + "type": "integer" + }, "platform": { "description": "router platform", "type": "string", @@ -35,7 +39,7 @@ VM_CREATE_SCHEMA = { "description": "router chassis model", "type": "string", "minLength": 1, - "pattern": "^[0-9]{4}$" + "pattern": "^[0-9]{4}(XM)?$" }, "image": { "description": "path to the IOS image file", @@ -58,14 +62,15 @@ VM_CREATE_SCHEMA = { "minimum": 1, "maximum": 65535 }, - "mac_address": { + "mac_addr": { "description": "base MAC address", "type": "string", "minLength": 1, "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" } }, - "required": ["platform", "image", "ram"] + "additionalProperties": False, + "required": ["name", "platform", "image", "ram"] } VM_DELETE_SCHEMA = { @@ -78,6 +83,7 @@ VM_DELETE_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -91,6 +97,7 @@ VM_START_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -104,6 +111,7 @@ VM_STOP_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -117,6 +125,7 @@ VM_SUSPEND_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -130,10 +139,11 @@ VM_RELOAD_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } -#TODO: platform specific properties? +#TODO: improve platform specific properties (dependencies?) VM_UPDATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to update a VM instance", @@ -237,7 +247,7 @@ VM_UPDATE_SCHEMA = { "minimum": 1, "maximum": 65535 }, - "mac_address": { + "mac_addr": { "description": "base MAC address", "type": "string", "minLength": 1, @@ -326,10 +336,112 @@ VM_UPDATE_SCHEMA = { "description": "private configuration base64 encoded", "type": "string" }, + # C7200 properties + "npe": { + "description": "NPE model", + "enum": ["npe-100", + "npe-150", + "npe-175", + "npe-200", + "npe-225", + "npe-300", + "npe-400", + "npe-g2"] + }, + "midplane": { + "description": "Midplane model", + "enum": ["std", "vxr"] + }, + "sensors": { + "description": "Temperature sensors", + "type": "array" + }, + "power_supplies": { + "description": "Power supplies status", + "type": "array" + }, + # I/O memory property for all platforms but C7200 + "iomem": { + "description": "I/O memory percentage", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, }, + "additionalProperties": False, "required": ["id"] } +VM_START_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a packet capture on a VM instance port", + "type": "object", + "properties": { + "id": { + "description": "VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VM instance", + "type": "integer" + }, + "slot": { + "description": "Slot number", + "type": "integer", + "minimum": 0, + "maximum": 6 + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 + }, + "capture_file_name": { + "description": "Capture file name", + "type": "string", + "minLength": 1, + }, + "data_link_type": { + "description": "PCAP data link type", + "type": "string", + "minLength": 1, + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "slot", "port", "capture_file_name"] +} + +VM_STOP_CAPTURE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a packet capture on a VM instance port", + "type": "object", + "properties": { + "id": { + "description": "VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VM instance", + "type": "integer" + }, + "slot": { + "description": "Slot number", + "type": "integer", + "minimum": 0, + "maximum": 6 + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "slot", "port"] +} + VM_SAVE_CONFIG_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to save the configs for VM instance", @@ -340,6 +452,7 @@ VM_SAVE_CONFIG_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -357,6 +470,7 @@ VM_IDLEPCS_SCHEMA = { "type": "boolean" }, }, + "additionalProperties": False, "required": ["id"] } @@ -374,6 +488,7 @@ VM_ALLOCATE_UDP_PORT_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id", "port_id"] } @@ -383,129 +498,129 @@ VM_ADD_NIO_SCHEMA = { "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 + "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": { @@ -542,6 +657,7 @@ VM_ADD_NIO_SCHEMA = { ] }, }, + "additionalProperties": False, "required": ["id", "port_id", "slot", "port", "nio"] } @@ -567,5 +683,6 @@ VM_DELETE_NIO_SCHEMA = { "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 }, }, + "additionalProperties": False, "required": ["id", "slot", "port"] } diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index 8cf1036e4..b42c80306 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -20,23 +20,20 @@ IOU 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 .iou_device import IOUDevice from .iou_error import IOUError from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP from .nios.nio_generic_ethernet import NIO_GenericEthernet from ..attic import find_unused_port +from ..attic import has_privileged_access from .schemas import IOU_CREATE_SCHEMA from .schemas import IOU_DELETE_SCHEMA @@ -68,18 +65,15 @@ class IOU(IModule): iou_config = config.get_section_config(name.upper()) self._iouyap = iou_config.get("iouyap") if not self._iouyap or not os.path.isfile(self._iouyap): - iouyap_in_cwd = os.path.join(os.getcwd(), "iouyap") - if os.path.isfile(iouyap_in_cwd): - self._iouyap = iouyap_in_cwd - else: - # look for iouyap if none is defined or accessible - for path in os.environ["PATH"].split(":"): - try: - if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK): - self._iouyap = os.path.join(path, "iouyap") - break - except OSError: - continue + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for iouyap in the current working directory and $PATH + for path in paths: + try: + if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK): + self._iouyap = os.path.join(path, "iouyap") + break + except OSError: + continue if not self._iouyap: log.warning("iouyap binary couldn't be found!") @@ -206,6 +200,7 @@ class IOU(IModule): - iourc (base64 encoded iourc file) Optional request parameters: + - iouyap (path to iouyap) - working_dir (path to a working directory) - project_name - console_start_port_range @@ -216,12 +211,12 @@ class IOU(IModule): :param request: JSON request """ - if request == None: + if request is None: self.send_param_error() return if "iourc" in request: - iourc_content = base64.decodestring(request["iourc"].encode("utf-8")).decode("utf-8") + iourc_content = base64.decodebytes(request["iourc"].encode("utf-8")).decode("utf-8") iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix try: with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: @@ -229,7 +224,7 @@ class IOU(IModule): f.write(iourc_content) self._iourc = f.name except OSError as e: - raise IOUError("Could not save iourc file to {}: {}".format(f.name, e)) + raise IOUError("Could not create the iourc file: {}".format(e)) if "iouyap" in request and request["iouyap"]: self._iouyap = request["iouyap"] @@ -268,30 +263,6 @@ class IOU(IModule): log.debug("received request {}".format(request)) - def test_result(self, message, result="error"): - """ - """ - - return {"result": result, "message": message} - - @IModule.route("iou.test_settings") - def test_settings(self, request): - """ - """ - - response = [] - - # test iourc - if self._iourc == "": - response.append(self.test_result("No iourc file has been added")) - elif not os.path.isfile(self._iourc): - response.append(self.test_result("iourc file {} is not accessible".format(self._iourc))) - else: - #TODO: check hostname + license inside the file - pass - - self.send_response(response) - @IModule.route("iou.create") def iou_create(self, request): """ @@ -302,6 +273,7 @@ class IOU(IModule): Optional request parameters: - name (IOU name) + - console (IOU console port) Response parameters: - id (IOU instance identifier) @@ -315,23 +287,18 @@ class IOU(IModule): if not self.validate_request(request, IOU_CREATE_SCHEMA): return - name = None - if "name" in request: - name = request["name"] + name = request["name"] iou_path = request["path"] + console = request.get("console") + iou_id = request.get("iou_id") try: - try: - os.makedirs(self._working_dir) - except FileExistsError: - pass - except OSError as e: - raise IOUError("Could not create working directory {}".format(e)) - - iou_instance = IOUDevice(iou_path, + iou_instance = IOUDevice(name, + iou_path, self._working_dir, self._host, - name, + iou_id, + console, self._console_start_port_range, self._console_end_port_range) @@ -371,7 +338,7 @@ class IOU(IModule): return try: - iou_instance.delete() + iou_instance.clean_delete() del self._iou_instances[request["id"]] except IOUError as e: self.send_custom_error(str(e)) @@ -389,7 +356,7 @@ class IOU(IModule): Optional request parameters: - any setting to update - - startup_config_base64 (startup-config base64 encoded) + - initial_config_base64 (initial-config base64 encoded) Response parameters: - updated settings @@ -406,42 +373,42 @@ class IOU(IModule): if not iou_instance: return - response = {} - config_path = os.path.join(iou_instance.working_dir, "startup-config") + config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg") try: - if "startup_config_base64" in request: - # a new startup-config has been pushed - config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + if "initial_config_base64" in request: + # a new initial-config has been pushed + config = base64.decodebytes(request["initial_config_base64"].encode("utf-8")).decode("utf-8") config = "!\n" + config.replace("\r", "") config = config.replace('%h', iou_instance.name) try: with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(config_path)) + log.info("saving initial-config to {}".format(config_path)) f.write(config) except OSError as e: raise IOUError("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) - elif "startup_config" in request: - if os.path.isfile(request["startup_config"]) and request["startup_config"] != config_path: + # update the request with the new local initial-config path + request["initial_config"] = os.path.basename(config_path) + elif "initial_config" in request: + if os.path.isfile(request["initial_config"]) and request["initial_config"] != config_path: # this is a local file set in the GUI try: - with open(request["startup_config"], "r") as f: + with open(request["initial_config"], "r", errors="replace") as f: config = f.read() with open(config_path, "w") as f: config = "!\n" + config.replace("\r", "") config = config.replace('%h', iou_instance.name) f.write(config) - request["startup_config"] = os.path.basename(config_path) + request["initial_config"] = os.path.basename(config_path) except OSError as e: - raise IOUError("Could not save the configuration from {} to {}: {}".format(request["startup_config"], config_path, e)) - else: - raise IOUError("Startup-config {} could not be found on this server".format(request["startup_config"])) + raise IOUError("Could not save the configuration from {} to {}: {}".format(request["initial_config"], config_path, e)) + elif not os.path.isfile(config_path): + raise IOUError("Startup-config {} could not be found on this server".format(request["initial_config"])) except IOUError as e: self.send_custom_error(str(e)) return # update the IOU settings + response = {} for name, value in request.items(): if hasattr(iou_instance, name) and getattr(iou_instance, name) != value: try: @@ -477,7 +444,6 @@ class IOU(IModule): return try: - log.debug("starting IOU with command: {}".format(iou_instance.command())) iou_instance.iouyap = self._iouyap iou_instance.iourc = self._iourc iou_instance.start() @@ -581,40 +547,17 @@ class IOU(IModule): ignore_ports=self._allocated_udp_ports) except Exception as e: self.send_custom_error(str(e)) + return self._allocated_udp_ports.append(port) log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance.name, iou_instance.id, port, self._host)) - response = {"lport": port} - response["port_id"] = request["port_id"] + response = {"lport": port, + "port_id": request["port_id"]} self.send_response(response) - def _check_for_privileged_access(self, device): - """ - Check if iouyap can access Ethernet and TAP devices. - - :param device: device name - """ - - # we are root, so iouyap should have privileged access too - if os.geteuid() == 0: - return - - # test if iouyap has the CAP_NET_RAW capability - if "security.capability" in os.listxattr(self._iouyap): - try: - caps = os.getxattr(self._iouyap, "security.capability") - # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set - if struct.unpack(" args.telnet_limit: fd.send(b'\r\nToo many connections\r\n') @@ -334,7 +335,10 @@ class TelnetServer(Console): def _disconnect(self, fileno): fd = self.fd_dict.pop(fileno) log.info("Telnet client disconnected") - fd.shutdown(socket.SHUT_RDWR) + try: + fd.shutdown(socket.SHUT_RDWR) + except OSError as e: + log.warn("shutdown: {}".format(e)) fd.close() def __enter__(self): @@ -375,7 +379,10 @@ class IOU(Router): return buf def write(self, buf): - self.fd.send(buf) + try: + self.fd.send(buf) + except BlockingIOError: + return def _open(self): self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) @@ -387,14 +394,12 @@ class IOU(Router): except FileNotFoundError: pass except Exception as e: - raise NetioError("Couldn't unlink socket {}: {}" - .format(self.ttyC, 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)) + raise NetioError("Couldn't create socket {}: {}".format(self.ttyC, e)) def _connect(self): # Keep trying until we connect or die trying @@ -405,8 +410,7 @@ class IOU(Router): 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)) + raise NetioError("Couldn't connect to socket {}: {}".format(self.ttyS, e)) else: break @@ -459,8 +463,7 @@ def mkdir_netio(netio_dir): except FileExistsError: pass except Exception as e: - raise NetioError("Couldn't create directory {}: {}" - .format(netio_dir, e)) + raise NetioError("Couldn't create directory {}: {}".format(netio_dir, e)) def send_recv_loop(console, router, esc_char, stop_event): @@ -599,7 +602,7 @@ def start_ioucon(cmdline_args, stop_event): nport = int(port) except ValueError: pass - if (addr == '' or nport == 0): + if addr == '' or nport == 0: raise ConfigError('format for --telnet-server must be ' 'ADDR:PORT (like 127.0.0.1:20000)') @@ -625,7 +628,7 @@ def start_ioucon(cmdline_args, stop_event): if args.debug: traceback.print_exc(file=sys.stderr) else: - print(e, file=sys.stderr) + log.error("ioucon: {}".format(e)) sys.exit(EXIT_FAILURE) log.info("exiting...") diff --git a/gns3server/modules/iou/schemas.py b/gns3server/modules/iou/schemas.py index 5d37ffedf..b6a33dc7c 100644 --- a/gns3server/modules/iou/schemas.py +++ b/gns3server/modules/iou/schemas.py @@ -26,13 +26,24 @@ IOU_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, + "iou_id": { + "description": "IOU device instance ID", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, "path": { "description": "path to the IOU executable", "type": "string", "minLength": 1, } }, - "required": ["path"] + "additionalProperties": False, + "required": ["name", "path"], } IOU_DELETE_SCHEMA = { @@ -45,6 +56,7 @@ IOU_DELETE_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -67,8 +79,8 @@ IOU_UPDATE_SCHEMA = { "type": "string", "minLength": 1, }, - "startup_config": { - "description": "path to the IOU startup configuration file", + "initial_config": { + "description": "path to the IOU initial configuration file", "type": "string", "minLength": 1, }, @@ -102,11 +114,16 @@ IOU_UPDATE_SCHEMA = { "description": "use the default IOU RAM & NVRAM values", "type": "boolean" }, - "startup_config_base64": { - "description": "startup configuration base64 encoded", + "l1_keepalives": { + "description": "enable or disable layer 1 keepalive messages", + "type": "boolean" + }, + "initial_config_base64": { + "description": "initial configuration base64 encoded", "type": "string" }, }, + "additionalProperties": False, "required": ["id"] } @@ -120,6 +137,7 @@ IOU_START_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -133,6 +151,7 @@ IOU_STOP_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -146,6 +165,7 @@ IOU_RELOAD_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id"] } @@ -163,6 +183,7 @@ IOU_ALLOCATE_UDP_PORT_SCHEMA = { "type": "integer" }, }, + "additionalProperties": False, "required": ["id", "port_id"] } @@ -172,129 +193,129 @@ IOU_ADD_NIO_SCHEMA = { "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 + "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": { @@ -331,6 +352,7 @@ IOU_ADD_NIO_SCHEMA = { ] }, }, + "additionalProperties": False, "required": ["id", "port_id", "slot", "port", "nio"] } @@ -357,5 +379,6 @@ IOU_DELETE_NIO_SCHEMA = { "maximum": 3 }, }, + "additionalProperties": False, "required": ["id", "slot", "port"] } diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 1b4168899..c7493d4d8 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -20,16 +20,12 @@ VPCS server module. """ import os -import sys import base64 -import tempfile -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 @@ -66,18 +62,15 @@ class VPCS(IModule): 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 + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for VPCS in the current working directory and $PATH + for path in paths: + 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!") @@ -87,22 +80,16 @@ class VPCS(IModule): # 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._console_start_port_range = 4512 + self._console_end_port_range = 5000 + self._allocated_udp_ports = [] + self._udp_start_port_range = 40001 + self._udp_end_port_range = 40512 self._host = kwargs["host"] self._projects_dir = kwargs["projects_dir"] self._tempdir = kwargs["temp_dir"] self._working_dir = self._projects_dir - # 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. @@ -110,7 +97,6 @@ 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] @@ -118,27 +104,6 @@ class VPCS(IModule): IModule.stop(self, signum) # this will stop the I/O loop - def _check_vpcs_is_alive(self): - """ - Periodic callback to check if VPCS is 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) - vpcs_instance.stop() - def get_vpcs_instance(self, vpcs_id): """ Returns a VPCS device instance. @@ -171,9 +136,7 @@ class VPCS(IModule): 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 + self._allocated_udp_ports.clear() log.info("VPCS module has been reset") @@ -183,6 +146,7 @@ class VPCS(IModule): Set or update settings. Optional request parameters: + - path (path to vpcs) - working_dir (path to a working directory) - project_name - console_start_port_range @@ -193,19 +157,22 @@ class VPCS(IModule): :param request: JSON request """ - if request == None: + if request is None: self.send_param_error() return - if "vpcs" in request and request["vpcs"]: - self._vpcs = request["vpcs"] + if "path" in request and request["path"]: + self._vpcs = request["path"] log.info("VPCS path set to {}".format(self._vpcs)) + for vpcs_id in self._vpcs_instances: + vpcs_instance = self._vpcs_instances[vpcs_id] + vpcs_instance.path = 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") + new_working_dir = os.path.join(self._projects_dir, request["project_name"]) log.info("this server is remote with working directory path to {}".format(new_working_dir)) if self._projects_dir != self._working_dir != new_working_dir: if not os.path.isdir(new_working_dir): @@ -234,31 +201,16 @@ class VPCS(IModule): 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) + - name (VPCS name) Optional request parameters: - - name (VPCS name) + - console (VPCS console port) Response parameters: - id (VPCS instance identifier) @@ -272,28 +224,24 @@ class VPCS(IModule): if not self.validate_request(request, VPCS_CREATE_SCHEMA): return - name = None - if "name" in request: - name = request["name"] - vpcs_path = request["path"] + name = request["name"] + console = request.get("console") + vpcs_id = request.get("vpcs_id") 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 + if not self._vpcs: + raise VPCSError("No path to a VPCS executable has been set") + + vpcs_instance = VPCSDevice(name, + self._vpcs, + self._working_dir, + self._host, + vpcs_id, + console, + self._console_start_port_range, + self._console_end_port_range) + except VPCSError as e: self.send_custom_error(str(e)) return @@ -330,7 +278,7 @@ class VPCS(IModule): return try: - vpcs_instance.delete() + vpcs_instance.clean_delete() del self._vpcs_instances[request["id"]] except VPCSError as e: self.send_custom_error(str(e)) @@ -348,7 +296,7 @@ class VPCS(IModule): Optional request parameters: - any setting to update - - script_file_base64 (script-file base64 encoded) + - script_file_base64 (base64 encoded) Response parameters: - updated settings @@ -365,28 +313,42 @@ class VPCS(IModule): if not vpcs_instance: return - response = {} + config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc") try: - # 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", "") + # a new startup-config has been pushed + config = base64.decodebytes(request["script_file_base64"].encode("utf-8")).decode("utf-8") + config = config.replace("\r", "") 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)) + 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 script-file path + # update the request with the new local startup-config path request["script_file"] = os.path.basename(config_path) - + elif "script_file" in request: + if os.path.isfile(request["script_file"]) and request["script_file"] != config_path: + # this is a local file set in the GUI + try: + with open(request["script_file"], "r", errors="replace") as f: + config = f.read() + with open(config_path, "w") as f: + config = config.replace("\r", "") + config = config.replace('%h', vpcs_instance.name) + f.write(config) + request["script_file"] = os.path.basename(config_path) + except OSError as e: + raise VPCSError("Could not save the configuration from {} to {}: {}".format(request["script_file"], config_path, e)) + elif not os.path.isfile(config_path): + raise VPCSError("Startup-config {} could not be found on this server".format(request["script_file"])) except VPCSError as e: self.send_custom_error(str(e)) return # update the VPCS settings + response = {} for name, value in request.items(): if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value: try: @@ -422,8 +384,6 @@ class VPCS(IModule): return try: - 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)) @@ -518,53 +478,25 @@ class VPCS(IModule): 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: + port = find_unused_port(self._udp_start_port_range, + self._udp_end_port_range, + host=self._host, + socket_type="UDP", + ignore_ports=self._allocated_udp_ports) + except Exception as e: self.send_custom_error(str(e)) return - response["port_id"] = request["port_id"] + self._allocated_udp_ports.append(port) + log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name, + vpcs_instance.id, + port, + self._host)) + + response = {"lport": port, + "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("=3.1", - "pyzmq>=13.1.0", # this is the strict minimum, recommended is >= 14.0.0 + "pyzmq>=14.0.0", "jsonschema==2.3.0" - ], + ], entry_points={ "console_scripts": [ "gns3server = gns3server.main:main", - ] - }, + ] + }, packages=find_packages(), package_data={"gns3server": ["templates/upload.html"]}, include_package_data=True, @@ -71,5 +71,5 @@ setup( "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", - ], + ], ) diff --git a/tests/test_jsonrpc.py b/tests/test_jsonrpc.py index 67bea4a96..155502c56 100644 --- a/tests/test_jsonrpc.py +++ b/tests/test_jsonrpc.py @@ -40,7 +40,7 @@ class JSONRPC(AsyncTestCase): AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request)) response = self.wait() json_response = json_decode(response) - assert json_response["id"] == None + assert json_response["id"] is None assert json_response["error"].get("code") == -32600 def test_request_with_invalid_json(self): @@ -49,7 +49,7 @@ class JSONRPC(AsyncTestCase): AsyncWSRequest(self.URL, self.io_loop, self.stop, request) response = self.wait() json_response = json_decode(response) - assert json_response["id"] == None + assert json_response["id"] is None assert json_response["error"].get("code") == -32700 def test_request_with_invalid_jsonrpc_field(self): @@ -58,7 +58,7 @@ class JSONRPC(AsyncTestCase): AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request)) response = self.wait() json_response = json_decode(response) - assert json_response["id"] == None + assert json_response["id"] is None assert json_response["error"].get("code") == -32700 def test_request_with_no_params(self): diff --git a/tests/test_version_handler.py b/tests/test_version_handler.py index feeef3d37..0c8c75d96 100644 --- a/tests/test_version_handler.py +++ b/tests/test_version_handler.py @@ -34,7 +34,7 @@ class TestVersionHandler(AsyncHTTPTestCase): self.http_client.fetch(self.get_url(self.URL), self.stop) response = self.wait() - assert(response.headers['Content-Type'].startswith('application/json')) - assert(response.body) + assert response.headers['Content-Type'].startswith('application/json') + assert response.body body = json_decode(response.body) assert body['version'] == __version__