From 0f75dbc68ad9a6a3821309fd323238decf5bedc6 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 11 Mar 2014 15:45:04 -0600 Subject: [PATCH] IOU integration. Improvements on module management. File upload support. Config file for the server. --- gns3server/config.py | 115 +++ gns3server/handlers/file_upload_handler.py | 77 ++ gns3server/handlers/jsonrpc_websocket.py | 2 +- gns3server/handlers/upload.html | 22 + gns3server/module_manager.py | 12 +- gns3server/modules/base.py | 24 +- gns3server/modules/dynamips/__init__.py | 37 +- gns3server/modules/dynamips/backends/atmsw.py | 1 + .../modules/dynamips/backends/ethhub.py | 1 + gns3server/modules/dynamips/backends/ethsw.py | 1 + gns3server/modules/dynamips/backends/frsw.py | 1 + gns3server/modules/dynamips/backends/vm.py | 1 + gns3server/modules/dynamips/hypervisor.py | 15 +- .../modules/dynamips/hypervisor_manager.py | 1 - gns3server/modules/iou/__init__.py | 506 ++++++++++++ gns3server/modules/iou/adapters/__init__.py | 0 gns3server/modules/iou/adapters/adapter.py | 104 +++ .../modules/iou/adapters/ethernet_adapter.py | 31 + .../modules/iou/adapters/serial_adapter.py | 31 + gns3server/modules/iou/iou_device.py | 767 ++++++++++++++++++ gns3server/modules/iou/iou_error.py | 37 + gns3server/modules/iou/ioucon.py | 645 +++++++++++++++ gns3server/modules/iou/nios/__init__.py | 0 gns3server/modules/iou/nios/nio_udp.py | 75 ++ gns3server/server.py | 30 +- setup.py | 3 - tests/iou/test_iou_device.py | 29 + 27 files changed, 2509 insertions(+), 59 deletions(-) create mode 100644 gns3server/config.py create mode 100644 gns3server/handlers/file_upload_handler.py create mode 100644 gns3server/handlers/upload.html create mode 100644 gns3server/modules/iou/__init__.py create mode 100644 gns3server/modules/iou/adapters/__init__.py create mode 100644 gns3server/modules/iou/adapters/adapter.py create mode 100644 gns3server/modules/iou/adapters/ethernet_adapter.py create mode 100644 gns3server/modules/iou/adapters/serial_adapter.py create mode 100644 gns3server/modules/iou/iou_device.py create mode 100644 gns3server/modules/iou/iou_error.py create mode 100644 gns3server/modules/iou/ioucon.py create mode 100644 gns3server/modules/iou/nios/__init__.py create mode 100644 gns3server/modules/iou/nios/nio_udp.py create mode 100644 tests/iou/test_iou_device.py diff --git a/gns3server/config.py b/gns3server/config.py new file mode 100644 index 00000000..cd2d07a1 --- /dev/null +++ b/gns3server/config.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Reads the configuration file and store the settings for the server & modules. +""" + +import sys +import os +import configparser + +import logging +log = logging.getLogger(__name__) + + +class Config(object): + """ + Configuration file management using configparser. + """ + + def __init__(self): + + appname = "GNS3" + if sys.platform.startswith("win"): + + # On windows, the configuration file location can be one of the following: + # 1: %APPDATA%/GNS3/server.ini + # 2: %APPDATA%/GNS3.ini + # 3: %COMMON_APPDATA%/GNS3/server.ini + # 4: %COMMON_APPDATA%/GNS3.ini + # 5: server.ini in the current working directory + + appdata = os.path.expandvars("%APPDATA%") + common_appdata = os.path.expandvars("%COMMON_APPDATA%") + filename = "server.ini" + self._files = [os.path.join(appdata, appname, filename), + os.path.join(appdata, appname + ".ini"), + os.path.join(common_appdata, appname, filename), + os.path.join(common_appdata, appname + ".ini"), + filename] + else: + + # On UNIX-like platforms, the configuration file location can be one of the following: + # 1: $HOME/.config/GNS3/server.conf + # 2: $HOME/.config/GNS3.conf + # 3: /etc/xdg/GNS3/server.conf + # 4: /etc/xdg/GNS3.conf + # 5: server.conf in the current working directory + + home = os.path.expanduser("~") + filename = "server.conf" + self._files = [os.path.join(home, ".config", appname, filename), + os.path.join(home, ".config", appname + ".conf"), + os.path.join("/etc/xdg", appname, filename), + os.path.join("/etc/xdg", appname + ".conf"), + filename] + + self._config = configparser.ConfigParser() + self.read_config() + + def read_config(self): + """ + Read the configuration files. + """ + + parsed_files = self._config.read(self._files) + if not parsed_files: + log.warning("no configuration file could be found or read") + + def get_default_section(self): + """ + Get the default configuration section. + + :returns: configparser section + """ + + return self._config["DEFAULT"] + + def get_section_config(self, section): + """ + Get a specific configuration section. + Returns the default section if none can be found. + + :returns: configparser section + """ + + if not section in self._config: + return self._config["DEFAULT"] + return self._config[section] + + @staticmethod + def instance(): + """ + Singleton to return only on instance of Config. + + :returns: instance of Config + """ + + if not hasattr(Config, "_instance"): + Config._instance = Config() + return Config._instance diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py new file mode 100644 index 00000000..9ab65b97 --- /dev/null +++ b/gns3server/handlers/file_upload_handler.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Simple file upload & listing handler. +""" + + +import os +import tornado.web +from ..config import Config + +import logging +log = logging.getLogger(__name__) + + +class FileUploadHandler(tornado.web.RequestHandler): + """ + File upload handler. + + :param application: Tornado Application instance + :param request: Tornado Request instance + """ + + def __init__(self, application, request): + + # get the upload directory from the configuration file + 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"))) + + if not os.path.exists(self._upload_dir): + try: + os.makedirs(self._upload_dir) + log.info("upload directory '{}' created".format(self._upload_dir)) + except EnvironmentError 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. + """ + + items = [] + path = self._upload_dir + for filename in os.listdir(path): + items.append(filename) + + self.render("upload.html", path=path, items=items) + + def post(self): + """ + Invoked on POST request. + """ + + 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']) + self.redirect("/upload") diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py index 9bf0fbbc..acc56893 100644 --- a/gns3server/handlers/jsonrpc_websocket.py +++ b/gns3server/handlers/jsonrpc_websocket.py @@ -101,7 +101,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): # by another module for instance assert destination not in cls.destinations log.debug("registering {} as a destination for the {} module".format(destination, - module)) + module)) cls.destinations[destination] = module def open(self): diff --git a/gns3server/handlers/upload.html b/gns3server/handlers/upload.html new file mode 100644 index 00000000..d8e534ee --- /dev/null +++ b/gns3server/handlers/upload.html @@ -0,0 +1,22 @@ + + + +Upload Form + + +

Select & Upload

+
+File: +
+
+ +
+{%if items%} +

Files

+
    +{%for item in items%} +
  • {{path}}{{item}}
  • +{%end%} +{%end%} +
+ \ No newline at end of file diff --git a/gns3server/module_manager.py b/gns3server/module_manager.py index 876b09a9..cf3814be 100644 --- a/gns3server/module_manager.py +++ b/gns3server/module_manager.py @@ -82,7 +82,7 @@ class ModuleManager(object): log.info("loading {} module".format(module_class[0].lower())) info = Module(name=module_class[0].lower(), cls=module_class[1]) self._modules.append(info) - except Exception as e: + except Exception: log.critical("error while analyzing {} package directory".format(name), exc_info=1) finally: if file: @@ -97,7 +97,7 @@ class ModuleManager(object): return self._modules - def activate_module(self, module, args=(), kwargs={}): + def activate_module(self, module, *args, **kwargs): """ Activates a given module. @@ -109,6 +109,10 @@ class ModuleManager(object): """ module_class = module.cls() - module_instance = module_class(name=module.name, args=args, kwargs={}) - log.info("activating {} module".format(module.name)) + try: + module_instance = module_class(module.name, *args, **kwargs) + except Exception: + log.critical("error while activating the {} module".format(module.name), exc_info=1) + return None + log.info("activating the {} module".format(module.name)) return module_instance diff --git a/gns3server/modules/base.py b/gns3server/modules/base.py index 3f4d04f8..d1bca93c 100644 --- a/gns3server/modules/base.py +++ b/gns3server/modules/base.py @@ -37,15 +37,11 @@ class IModule(multiprocessing.Process): :param kwargs: named arguments for the module """ - destination = {} + modules = {} - def __init__(self, name=None, args=(), kwargs={}): - - multiprocessing.Process.__init__(self, - name=name, - args=args, - kwargs=kwargs) + def __init__(self, name, *args, **kwargs): + multiprocessing.Process.__init__(self, name=name) self._context = None self._ioloop = None self._stream = None @@ -203,14 +199,14 @@ class IModule(multiprocessing.Process): destination = request[1].get("method") params = request[1].get("params") - if destination not in self.destination: + if destination not in self.modules[self.name]: self.send_internal_error() return log.debug("Routing request to {}: {}".format(destination, request[1])) try: - self.destination[destination](self, params) + self.modules[self.name][destination](self, params) except Exception as e: log.error("uncaught exception {type}".format(type=type(e)), exc_info=1) self.send_custom_error("uncaught exception {type}: {string}".format(type=type(e), string=str(e))) @@ -222,7 +218,10 @@ class IModule(multiprocessing.Process): :returns: list of destinations """ - return self.destination.keys() + if not self.name in self.modules: + log.warn("no destinations found for module {}".format(self.name)) + return [] + return self.modules[self.name].keys() @classmethod def route(cls, destination): @@ -233,6 +232,9 @@ class IModule(multiprocessing.Process): """ def wrapper(method): - cls.destination[destination] = method + module = destination.split(".")[0] + if not module in cls.modules: + cls.modules[module] = {} + cls.modules[module][destination] = method return method return wrapper diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index b5599d03..160b5f3a 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -97,8 +97,9 @@ class Dynamips(IModule): :param kwargs: named arguments for the module """ - def __init__(self, name=None, args=(), kwargs={}): - IModule.__init__(self, name=name, args=args, kwargs=kwargs) + def __init__(self, name, *args, **kwargs): + + IModule.__init__(self, name, *args, **kwargs) self._hypervisor_manager = None self._remote_server = False @@ -107,6 +108,8 @@ class Dynamips(IModule): self._frame_relay_switches = {} self._atm_switches = {} self._ethernet_hubs = {} + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] #self._callback = self.add_periodic_callback(self.test, 1000) #self._callback.start() @@ -189,13 +192,7 @@ class Dynamips(IModule): else: self._remote_server = True log.info("this server is remote") - try: - working_dir = tempfile.mkdtemp(prefix="gns3-remote-server-") - working_dir = os.path.join(working_dir, "dynamips") - os.makedirs(working_dir) - log.info("temporary working directory created: {}".format(working_dir)) - except EnvironmentError as e: - raise DynamipsError("Could not create temporary working directory: {}".format(e)) + working_dir = self._projects_dir #TODO: check if executable if not os.path.exists(dynamips_path): @@ -342,28 +339,6 @@ class Dynamips(IModule): router.ghost_status = 2 router.ghost_file = ghost_instance -# def get_base64_config(self, config_path, router): -# """ -# Get the base64 encoded config from a file. -# Replaces %h by the router name. -# -# :param config_path: path to the configuration file. -# :param router: Router instance. -# -# :returns: base64 encoded string -# """ -# -# try: -# with open(config_path, "r") as f: -# log.info("opening configuration file: {}".format(config_path)) -# config = f.read() -# config = '!\n' + config.replace('\r', "") -# config = config.replace('%h', router.name) -# encoded = ("").join(base64.encodestring(config.encode("utf-8")).decode("utf-8").split()) -# return encoded -# except EnvironmentError as e: -# raise DynamipsError("Cannot parse {}: {}".format(config_path, e)) - @IModule.route("dynamips.nio.get_interfaces") def nio_get_interfaces(self, request): """ diff --git a/gns3server/modules/dynamips/backends/atmsw.py b/gns3server/modules/dynamips/backends/atmsw.py index df1b44ac..9bc4b037 100644 --- a/gns3server/modules/dynamips/backends/atmsw.py +++ b/gns3server/modules/dynamips/backends/atmsw.py @@ -84,6 +84,7 @@ class ATMSW(object): try: atmsw.delete() self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(atmsw) + del self._atm_switches[atmsw_id] except DynamipsError as e: self.send_custom_error(str(e)) return diff --git a/gns3server/modules/dynamips/backends/ethhub.py b/gns3server/modules/dynamips/backends/ethhub.py index bcb32158..f8419c1b 100644 --- a/gns3server/modules/dynamips/backends/ethhub.py +++ b/gns3server/modules/dynamips/backends/ethhub.py @@ -83,6 +83,7 @@ class ETHHUB(object): try: ethhub.delete() self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethhub) + del self._ethernet_hubs[ethhub_id] except DynamipsError as e: self.send_custom_error(str(e)) return diff --git a/gns3server/modules/dynamips/backends/ethsw.py b/gns3server/modules/dynamips/backends/ethsw.py index 387955e1..5d17b711 100644 --- a/gns3server/modules/dynamips/backends/ethsw.py +++ b/gns3server/modules/dynamips/backends/ethsw.py @@ -83,6 +83,7 @@ class ETHSW(object): try: ethsw.delete() self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethsw) + del self._ethernet_switches[ethsw_id] except DynamipsError as e: self.send_custom_error(str(e)) return diff --git a/gns3server/modules/dynamips/backends/frsw.py b/gns3server/modules/dynamips/backends/frsw.py index ebe912ab..a45ddc85 100644 --- a/gns3server/modules/dynamips/backends/frsw.py +++ b/gns3server/modules/dynamips/backends/frsw.py @@ -83,6 +83,7 @@ class FRSW(object): try: frsw.delete() self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(frsw) + del self._frame_relay_switches[frsw_id] except DynamipsError as e: self.send_custom_error(str(e)) return diff --git a/gns3server/modules/dynamips/backends/vm.py b/gns3server/modules/dynamips/backends/vm.py index 8486ea34..2f8592d7 100644 --- a/gns3server/modules/dynamips/backends/vm.py +++ b/gns3server/modules/dynamips/backends/vm.py @@ -206,6 +206,7 @@ class VM(object): try: router.delete() self._hypervisor_manager.unallocate_hypervisor_for_router(router) + del self._routers[router_id] except DynamipsError as e: self.send_custom_error(str(e)) return diff --git a/gns3server/modules/dynamips/hypervisor.py b/gns3server/modules/dynamips/hypervisor.py index 98082aa6..b2c76a3f 100644 --- a/gns3server/modules/dynamips/hypervisor.py +++ b/gns3server/modules/dynamips/hypervisor.py @@ -211,11 +211,16 @@ class Hypervisor(DynamipsHypervisor): if self.is_running(): DynamipsHypervisor.stop(self) log.info("stopping Dynamips PID={}".format(self._process.pid)) - # give some time for the hypervisor to properly stop. - # time to delete UNIX NIOs for instance. - time.sleep(0.01) - self._process.kill() - self._process.wait() + try: + # give some time for the hypervisor to properly stop. + # time to delete UNIX NIOs for instance. + time.sleep(0.01) + self._process.terminate() + self._process.wait(1) + except subprocess.TimeoutExpired: + self._process.kill() + if self._process.poll() == None: + log.warn("Dynamips process {} is still running".format(self._process.pid)) def read_stdout(self): """ diff --git a/gns3server/modules/dynamips/hypervisor_manager.py b/gns3server/modules/dynamips/hypervisor_manager.py index 1a5a7c01..cc6c4533 100644 --- a/gns3server/modules/dynamips/hypervisor_manager.py +++ b/gns3server/modules/dynamips/hypervisor_manager.py @@ -20,7 +20,6 @@ Manages Dynamips hypervisors (load-balancing etc.) """ from .hypervisor import Hypervisor -from .dynamips_error import DynamipsError import socket import time import logging diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py new file mode 100644 index 00000000..388b211d --- /dev/null +++ b/gns3server/modules/iou/__init__.py @@ -0,0 +1,506 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +IOU server module. +""" + +import os +import sys +import base64 +import tempfile +from gns3server.modules import IModule +from gns3server.config import Config +from .iou_device import IOUDevice +from .iou_error import IOUError +from .nios.nio_udp import NIO_UDP +import gns3server.jsonrpc as jsonrpc + +import logging +log = logging.getLogger(__name__) + + +class IOU(IModule): + """ + IOU module. + + :param name: module name + :param args: arguments for the module + :param kwargs: named arguments for the module + """ + + def __init__(self, name, *args, **kwargs): + + if not sys.platform.startswith("linux"): + raise IOUError("Sorry the IOU module only works on Linux") + + # get the iouyap location + config = Config.instance() + iou_config = config.get_section_config(name.upper()) + self._iouyap = iou_config.get("iouyap") + if not self._iouyap: + for path in os.environ["PATH"].split(":"): + if "iouyap" in os.listdir(path) and os.access("iouyap", os.X_OK): + self._iouyap = os.path.join(path, "iouyap") + break + + if not self._iouyap or not os.path.exists(self._iouyap): + raise IOUError("iouyap binary couldn't be found!") + + if not os.access(self._iouyap, os.X_OK): + raise IOUError("iouyap is not executable") + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._remote_server = False + self._iou_instances = {} + self._console_start_port_range = 4001 + self._console_end_port_range = 4512 + self._current_console_port = self._console_start_port_range + self._udp_start_port_range = 30001 + self._udp_end_port_range = 40001 + self._current_udp_port = self._udp_start_port_range + self._host = "127.0.0.1" + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + + #self._callback = self.add_periodic_callback(self.test, 1000) + #self._callback.start() + + def stop(self): + """ + Properly stops the module. + """ + + # delete all IOU instances + for iou_id in self._iou_instances: + iou_instance = self._iou_instances[iou_id] + iou_instance.delete() + + IModule.stop(self) # this will stop the I/O loop + + @IModule.route("iou.reset") + def reset(self, request): + """ + Resets the module. + + :param request: JSON request + """ + + # delete all IOU instances + for iou_id in self._iou_instances: + iou_instance = self._iou_instances[iou_id] + iou_instance.delete() + + # resets the instance IDs + IOUDevice.reset() + + self._iou_instances.clear() + self._remote_server = False + self._current_console_port = self._console_start_port_range + self._current_udp_port = self._udp_start_port_range + log.info("IOU module has been reset") + + @IModule.route("iou.settings") + def settings(self, request): + """ + Set or update settings. + + Mandatory request parameters: + - path (path to the IOU executable) + + Optional request parameters: + - working_dir (path to a working directory) + - console_start_port_range + - console_end_port_range + - udp_start_port_range + - udp_end_port_range + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + print(request) + + if "working_dir" in request and self._working_dir != request["working_dir"]: + self._working_dir = request["working_dir"] + log.info("this server is local with working directory path to {}".format(self._working_dir)) + for iou_id in self._iou_instances: + iou_instance = self._iou_instances[iou_id] + iou_instance.working_dir = self._working_dir + else: + self._remote_server = True + log.info("this server is remote") + self._working_dir = self._projects_dir + + if "console_start_port_range" in request and "console_end_port_range" in request: + self._console_start_port_range = request["console_start_port_range"] + self._console_end_port_range = request["console_end_port_range"] + + if "udp_start_port_range" in request and "udp_end_port_range" in request: + self._udp_start_port_range = request["udp_start_port_range"] + self._udp_end_port_range = request["udp_end_port_range"] + + log.debug("received request {}".format(request)) + + @IModule.route("iou.create") + def iou_create(self, request): + """ + Creates a new IOU instance. + + Optional request parameters: + - name (IOU name) + - path (path to IOU) + + Response parameters: + - id (IOU instance identifier) + - name (IOU name) + + :param request: JSON request + """ + + #TODO: JSON schema validation for the request + name = None + if request and "name" in request: + name = request["name"] + + iou_path = request["path"] + + try: + iou_instance = IOUDevice(iou_path, self._working_dir, 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 + iou_instance.console = IOUDevice.find_unused_port(self._current_console_port, self._console_end_port_range, self._host) + self._current_console_port += 1 + except IOUError as e: + self.send_custom_error(str(e)) + return + + response = {"name": iou_instance.name, + "id": iou_instance.id} + + defaults = iou_instance.defaults() + response.update(defaults) + self._iou_instances[iou_instance.id] = iou_instance + self.send_response(response) + + @IModule.route("iou.delete") + def iou_delete(self, request): + """ + Deletes an IOU instance. + + Mandatory request parameters: + - id (IOU instance identifier) + + Response parameters: + - same as original request + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + try: + iou_instance.delete() + del self._iou_instances[iou_id] + except IOUError as e: + self.send_custom_error(str(e)) + return + self.send_response(request) + + @IModule.route("iou.update") + def iou_update(self, request): + """ + Updates an IOU instance + + Mandatory request parameters: + - id (IOU instance identifier) + + Optional request parameters: + - any setting to update + - startup_config_base64 (startup-config base64 encoded) + + Response parameters: + - same as original request + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + + try: + # a new startup-config has been pushed + if "startup_config_base64" in request: + config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + config = "!\n" + config.replace("\r", "") + config = config.replace('%h', iou_instance.name) + config_path = os.path.join(iou_instance.working_dir, "startup-config") + try: + with open(config_path, "w") as f: + log.info("saving startup-config to {}".format(config_path)) + f.write(config) + except EnvironmentError as e: + raise IOUError("Could not save the configuration {}: {}".format(config_path, e)) + request["startup_config"] = os.path.basename(config_path) + if "startup_config" in request: + iou_instance.startup_config = request["startup_config"] + except IOUError as e: + self.send_custom_error(str(e)) + return + + for name, value in request.items(): + if hasattr(iou_instance, name) and getattr(iou_instance, name) != value: + try: + setattr(iou_instance, name, value) + except IOUError as e: + self.send_custom_error(str(e)) + return + + self.send_response(request) + + @IModule.route("iou.start") + def vm_start(self, request): + """ + Starts an IOU instance. + + Mandatory request parameters: + - id (IOU instance identifier) + + Response parameters: + - same as original request + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + try: + log.debug("starting IOU with command: {}".format(iou_instance.command())) + iou_instance.iouyap = self._iouyap + iou_instance.start() + except IOUError as e: + self.send_custom_error(str(e)) + return + self.send_response(request) + + @IModule.route("iou.stop") + def vm_stop(self, request): + """ + Stops an IOU instance. + + Mandatory request parameters: + - id (IOU instance identifier) + + Response parameters: + - same as original request + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + try: + iou_instance.stop() + except IOUError as e: + self.send_custom_error(str(e)) + return + self.send_response(request) + + @IModule.route("iou.allocate_udp_port") + def allocate_udp_port(self, request): + """ + Allocates a UDP port in order to create an UDP NIO. + + Mandatory request parameters: + - id (IOU identifier) + - port_id (unique port identifier) + + Response parameters: + - port_id (unique port identifier) + - lhost (local host address) + - lport (allocated local port) + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + + try: + + # find a UDP port + if self._current_udp_port >= self._udp_end_port_range: + self._current_udp_port = self._udp_start_port_range + port = IOUDevice.find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") + self._current_udp_port += 1 + + log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance .name, + iou_instance .id, + port, + self._host)) + response = {"lport": port, + "lhost": self._host} + + except IOUError as e: + self.send_custom_error(str(e)) + return + + response["port_id"] = request["port_id"] + self.send_response(response) + + @IModule.route("iou.add_nio") + def add_nio(self, request): + """ + Adds an NIO (Network Input/Output) for an IOU instance. + + Mandatory request parameters: + - id (IOU instance identifier) + - slot (slot number) + - port (port number) + - port_id (unique port identifier) + - nio (nio type, one of the following) + - "NIO_UDP" + - lport (local port) + - rhost (remote host) + - rport (remote port) + + Response parameters: + - same as original request + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + + slot = request["slot"] + port = request["port"] + + try: + nio = None + #TODO: support for TAP and Ethernet NIOs + if request["nio"] == "NIO_UDP": + lport = request["lport"] + rhost = request["rhost"] + rport = request["rport"] + nio = NIO_UDP(lport, rhost, rport) + if not nio: + raise IOUError("Requested NIO doesn't exist or is not supported: {}".format(request["nio"])) + except IOUError as e: + self.send_custom_error(str(e)) + return + + try: + iou_instance.slot_add_nio_binding(slot, port, nio) + except IOUError as e: + self.send_custom_error(str(e)) + return + + # for now send back the original request + self.send_response(request) + + @IModule.route("iou.delete_nio") + def delete_nio(self, request): + """ + Deletes an NIO (Network Input/Output). + + Mandatory request parameters: + - id (IOU instance identifier) + - slot (slot identifier) + - port (port identifier) + + Response parameters: + - same as original request + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + return + + #TODO: JSON schema validation for the request + log.debug("received request {}".format(request)) + iou_id = request["id"] + iou_instance = self._iou_instances[iou_id] + slot = request["slot"] + port = request["port"] + + try: + iou_instance.slot_remove_nio_binding(slot, port) + except IOUError as e: + self.send_custom_error(str(e)) + return + + # for now send back the original request + self.send_response(request) + + @IModule.route("iou.echo") + def echo(self, request): + """ + Echo end point for testing purposes. + + :param request: JSON request + """ + + if request == None: + self.send_param_error() + else: + log.debug("received request {}".format(request)) + self.send_response(request) diff --git a/gns3server/modules/iou/adapters/__init__.py b/gns3server/modules/iou/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/iou/adapters/adapter.py b/gns3server/modules/iou/adapters/adapter.py new file mode 100644 index 00000000..4d2f4053 --- /dev/null +++ b/gns3server/modules/iou/adapters/adapter.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +class Adapter(object): + """ + Base class for adapters. + + :param interfaces: number of interfaces supported by this adapter. + """ + + def __init__(self, interfaces=4): + + self._interfaces = interfaces + + self._ports = {} + for port_id in range(0, interfaces): + self._ports[port_id] = None + + def removable(self): + """ + Returns True if the adapter can be removed from a slot + and False if not. + + :returns: boolean + """ + + return True + + def port_exists(self, port_id): + """ + Checks if a port exists on this adapter. + + :returns: True is the port exists, + False otherwise. + """ + + if port_id in self._ports: + return True + return False + + def add_nio(self, port_id, nio): + """ + Adds a NIO to a port on this adapter. + + :param port_id: port ID (integer) + :param nio: NIO instance + """ + + self._ports[port_id] = nio + + def remove_nio(self, port_id): + """ + Removes a NIO from a port on this adapter. + + :param port_id: port ID (integer) + """ + + self._ports[port_id] = None + + def get_nio(self, port_id): + """ + Returns the NIO assigned to a port. + + :params port_id: port ID (integer) + + :returns: NIO instance + """ + + return self._ports[port_id] + + @property + def ports(self): + """ + Returns port to NIO mapping + + :returns: dictionary port -> NIO + """ + + return self._ports + + @property + def interfaces(self): + """ + Returns the number of interfaces supported by this adapter. + + :returns: number of interfaces + """ + + return self._interfaces diff --git a/gns3server/modules/iou/adapters/ethernet_adapter.py b/gns3server/modules/iou/adapters/ethernet_adapter.py new file mode 100644 index 00000000..312ef848 --- /dev/null +++ b/gns3server/modules/iou/adapters/ethernet_adapter.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .adapter import Adapter + + +class EthernetAdapter(Adapter): + """ + IOU Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=4) + + def __str__(self): + + return "IOU Ethernet adapter" diff --git a/gns3server/modules/iou/adapters/serial_adapter.py b/gns3server/modules/iou/adapters/serial_adapter.py new file mode 100644 index 00000000..9f2851a5 --- /dev/null +++ b/gns3server/modules/iou/adapters/serial_adapter.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .adapter import Adapter + + +class SerialAdapter(Adapter): + """ + IOU Serial adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=4) + + def __str__(self): + + return "IOU Serial adapter" diff --git a/gns3server/modules/iou/iou_device.py b/gns3server/modules/iou/iou_device.py new file mode 100644 index 00000000..ec8c2f88 --- /dev/null +++ b/gns3server/modules/iou/iou_device.py @@ -0,0 +1,767 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +IOU device management (creates command line, processes, files etc.) in +order to run an IOU instance. +""" + +import os +import socket +import errno +import signal +import subprocess +import argparse +import threading +import configparser +from .ioucon import start_ioucon +from .iou_error import IOUError +from .adapters.ethernet_adapter import EthernetAdapter +from .adapters.serial_adapter import SerialAdapter + + +import logging +log = logging.getLogger(__name__) + + +class IOUDevice(object): + """ + IOU device implementation. + + :param path: path to IOU executable + :param working_dir: path to a working directory + :param host: host/address to bind for console and UDP connections + :param name: name of this IOU device + """ + + _instances = [] + + def __init__(self, path, working_dir, host="127.0.0.1", name=None): + + # find an instance identifier (0 < id <= 512) + self._id = 0 + for identifier in range(1, 513): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break + + if self._id == 0: + raise IOUError("Maximum number of IOU instances reached") + + if name: + self._name = name + else: + self._name = "IOU{}".format(self._id) + self._path = path + self._iourc = "" + self._iouyap = "" + self._console = None + self._working_dir = None + self._command = [] + self._process = None + self._iouyap_process = None + self._stdout_file = "" + self._ioucon_thead = None + self._ioucon_thread_stop_event = None + self._host = host + + # IOU settings + self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces + self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces + self._slots = self._ethernet_adapters + self._serial_adapters + self._nvram = 128 # Kilobytes + self._startup_config = "" + self._ram = 256 # Megabytes + + # update the working directory + self.working_dir = working_dir + + log.info("IOU device {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for IOU. + + :returns: default values (dictionary) + """ + + iou_defaults = {"name": self._name, + "path": self._path, + "iourc": self._iourc, + "startup_config": self._startup_config, + "ram": self._ram, + "nvram": self._nvram, + "ethernet_adapters": len(self._ethernet_adapters), + "serial_adapters": len(self._serial_adapters), + "console": self._console} + + return iou_defaults + + @property + def id(self): + """ + Returns the unique ID for this IOU device. + + :returns: id (integer) + """ + + return(self._id) + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + + @property + def name(self): + """ + Returns the name of this IOU device. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this IOU device. + + :param new_name: name + """ + + self._name = new_name + log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + @property + def path(self): + """ + Returns the path to the IOU executable. + + :returns: path to IOU + """ + + return(self._path) + + @path.setter + def path(self, path): + """ + Sets the path to the IOU executable. + + :param path: path to IOU + """ + + self._path = path + log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name, + id=self._id, + path=path)) + + @property + def iourc(self): + """ + Returns the path to the iourc file. + + :returns: path to the iourc file + """ + + return(self._iourc) + + @iourc.setter + def iourc(self, iourc): + """ + Sets the path to the iourc file. + + :param path: path to the iourc file. + """ + + self._iourc = iourc + log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, + id=self._id, + path=self._iourc)) + + @property + def iouyap(self): + """ + Returns the path to iouyap + + :returns: path to iouyap + """ + + return(self._iouyap) + + @iouyap.setter + def iouyap(self, iouyap): + """ + Sets the path to iouyap. + + :param path: path to iouyap + """ + + self._iouyap = iouyap + log.info("IOU {name} [id={id}]: iouyap path set to {path}".format(name=self._name, + id=self._id, + path=self._iouyap)) + + @property + def working_dir(self): + """ + Returns current working directory + + :returns: path to the working directory + """ + + return self._working_dir + + @working_dir.setter + def working_dir(self, working_dir): + """ + Sets the working directory for IOU. + + :param working_dir: path to the working directory + """ + + # create our own working directory + working_dir = os.path.join(working_dir, "device-{}".format(self._id)) + if not os.path.exists(working_dir): + try: + os.makedirs(working_dir) + except EnvironmentError as e: + raise IOUError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("IOU {name} [id={id}]: working directory changed to {wd}".format(name=self._name, + id=self._id, + wd=self._working_dir)) + + @property + def console(self): + """ + Returns the TCP console port. + + :returns: console port (integer) + """ + + return self._console + + @console.setter + def console(self, console): + """ + Sets the TCP console port. + + :param console: console port (integer) + """ + + self._console = console + log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + def command(self): + """ + Returns the IOU command line. + + :returns: IOU command line (string) + """ + + return " ".join(self._build_command()) + + def delete(self): + """ + Deletes this IOU device. + """ + + self.stop() + self._instances.remove(self._id) + log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + def _update_iouyap_config(self): + """ + Updates the iouyap.ini file. + """ + + iouyap_ini = os.path.join(self._working_dir, "iouyap.ini") + + config = configparser.ConfigParser() + config["default"] = {"netmap": "NETMAP", + "base_port": "49000"} + + bay_id = 0 + for adapter in self._slots: + unit_id = 0 + for unit in adapter.ports.keys(): + nio = adapter.get_nio(unit) + if nio: + #TODO: handle TAP and Ethernet NIOs + tunnel = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport)} + + config["{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)] = tunnel + unit_id += 1 + bay_id += 1 + + try: + with open(iouyap_ini, "w") as config_file: + config.write(config_file) + log.info("IOU {name} [id={id}]: iouyap.ini updated".format(name=self._name, + id=self._id)) + except EnvironmentError as e: + raise IOUError("Could not create {}: {}".format(iouyap_ini, e)) + + def _create_netmap_config(self): + """ + Creates the NETMAP file. + """ + + netmap_path = os.path.join(self._working_dir, "NETMAP") + try: + with open(netmap_path, "w") as f: + for bay in range(0, 16): + for unit in range(0, 4): + f.write("{iouyap_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(iouyap_id=str(self._id + 512), + bay=bay, + unit=unit, + iou_id=self._id)) + log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, + id=self._id)) + except EnvironmentError as e: + raise IOUError("Could not create {}: {}".format(netmap_path, e)) + + def _start_ioucon(self): + """ + Starts ioucon thread (for console connections). + """ + + if not self._ioucon_thead: + telnet_server = "{}:{}".format(self._host, self._console) + log.info("starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) + args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) + self._ioucon_thread_stop_event = threading.Event() + self._ioucon_thead = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) + self._ioucon_thead.start() + + def _start_iouyap(self): + """ + Starts iouyap (handles connections to and from this IOU device). + """ + + try: + self._update_iouyap_config() + command = [self._iouyap, str(self._id + 512)] # iouyap has always IOU ID + 512 + log.info("starting iouyap: {}".format(command)) + self._stdout_file = os.path.join(self._working_dir, "iouyap.log") + log.info("logging to {}".format(self._stdout_file)) + with open(self._stdout_file, "w") as fd: + self._iouyap_process = subprocess.Popen(command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + + log.info("iouyap started PID={}".format(self._iouyap_process.pid)) + except EnvironmentError as e: + log.error("could not start iouyap: {}".format(e)) + raise IOUError("Could not start iouyap: {}".format(e)) + + def start(self): + """ + Starts the IOU process. + """ + + if not self.is_running(): + if not self._iourc or not os.path.exists(self._iourc): + raise IOUError("A iourc file is necessary to start IOU") + + if not self._iouyap or not os.path.exists(self._iouyap): + raise IOUError("iouyap is necessary to start IOU") + + self._create_netmap_config() + # created a environment variable pointing to the iourc file. + env = os.environ.copy() + env["IOURC"] = self._iourc + self._command = self._build_command() + try: + log.info("starting IOU: {}".format(self._command)) + self._stdout_file = os.path.join(self._working_dir, "iou.log") + log.info("logging to {}".format(self._stdout_file)) + with open(self._stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir, + env=env) + log.info("IOU instance {} started PID={}".format(self._id, self._process.pid)) + except EnvironmentError as e: + log.error("could not start IOU: {}".format(e)) + raise IOUError("could not start IOU: {}".format(e)) + + # start console support + self._start_ioucon() + # connections support + self._start_iouyap() + + def stop(self): + """ + Stops the IOU process. + """ + + # stop the IOU process + if self.is_running(): + log.info("stopping IOU instance {} PID={}".format(self._id, self._process.pid)) + try: + self._process.terminate() + self._process.wait(1) + except subprocess.TimeoutExpired: + self._process.kill() + if self._process.poll() == None: + log.warn("IOU instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + + # stop console support + if self._ioucon_thead: + self._ioucon_thread_stop_event.set() + if self._ioucon_thead.is_alive(): + self._ioucon_thead.join(timeout=0.10) + self._ioucon_thead = None + + # stop iouyap + if self.is_iouyap_running(): + log.info("stopping iouyap PID={} for IOU instance {}".format(self._iouyap_process.pid, self._id)) + try: + self._iouyap_process.terminate() + self._iouyap_process.wait(1) + except subprocess.TimeoutExpired: + self._iouyap_process.kill() + if self._iouyap_process.poll() == None: + log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid, + self._id)) + self._iouyap_process = None + + def read_stdout(self): + """ + Reads the standard output of the IOU process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._stdout_file: + try: + with open(self._stdout_file) as file: + output = file.read() + except EnvironmentError as e: + log.warn("could not read {}: {}".format(self._stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the IOU process is running + + :returns: True or False + """ + + if self._process and self._process.poll() == None: + return True + return False + + def is_iouyap_running(self): + """ + Checks if the iouyap process is running + + :returns: True or False + """ + + if self._iouyap_process and self._iouyap_process.poll() == None: + return True + return False + + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + adapter.add_nio(port_id, nio) + log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + + :param slot_id: slot ID + :param port_id: port ID + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + def _build_command(self): + """ + Command to start the IOU process. + (to be passed to subprocess.Popen()) + + IOU command line: + Usage: [options] + : unix-js-m | unix-is-m | unix-i-m | ... + : instance identifier (0 < id <= 1024) + Options: + -e Number of Ethernet interfaces (default 2) + -s Number of Serial interfaces (default 2) + -n Size of nvram in Kb (default 64KB) + -b IOS debug string + -c Configuration file name + -d Generate debug information + -t Netio message trace + -q Suppress informational messages + -h Display this help + -C Turn off use of host clock + -m Megabytes of router memory (default 256MB) + -L Disable local console, use remote console + -l Enable Layer 1 keepalive messages + -u UDP port base for distributed networks + -R Ignore options from the IOURC file + -U Disable unix: file system location + -W Disable watchdog timer + -N Ignore the NETMAP file + """ + + #TODO: add support for keepalive and watchdog + command = [self._path] + if len(self._ethernet_adapters) != 2: + command.extend(["-e", str(len(self._ethernet_adapters))]) + if len(self._serial_adapters) != 2: + command.extend(["-s", str(len(self._serial_adapters))]) + command.extend(["-n", str(self._nvram)]) + command.extend(["-m", str(self._ram)]) + command.extend(["-L"]) # disable local console, use remote console + if self._startup_config: + command.extend(["-c", self._startup_config]) + command.extend([str(self._id)]) + return command + + @property + def ram(self): + """ + Returns the amount of RAM allocated to this IOU instance. + + :returns: amount of RAM in Mbytes (integer) + """ + + return self._ram + + @ram.setter + def ram(self, ram): + """ + Sets amount of RAM allocated to this IOU instance. + + :param ram: amount of RAM in Mbytes (integer) + """ + + if self._ram == ram: + return + + log.info("IOU {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, + id=self._id, + old_ram=self._ram, + new_ram=ram)) + + self._ram = ram + + @property + def nvram(self): + """ + Returns the mount of NVRAM allocated to this IOU instance. + + :returns: amount of NVRAM in Kbytes (integer) + """ + + return self._nvram + + @nvram.setter + def nvram(self, nvram): + """ + Sets amount of NVRAM allocated to this IOU instance. + + :param nvram: amount of NVRAM in Kbytes (integer) + """ + + if self._nvram == nvram: + return + + log.info("IOU {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, + id=self._id, + old_nvram=self._nvram, + new_nvram=nvram)) + self._nvram = nvram + + @property + def startup_config(self): + """ + Returns the startup-config for this IOU instance. + + :returns: path to startup-config file + """ + + return self._startup_config + + @startup_config.setter + def startup_config(self, startup_config): + """ + Sets the startup-config for this IOU instance. + + :param startup_config: path to startup-config file + """ + + self._startup_config = startup_config + log.info("IOU {name} [id={id}]: startup_config set to {config}".format(name=self._name, + id=self._id, + config=self._startup_config)) + + @property + def ethernet_adapters(self): + """ + Returns the number of Ethernet adapters for this IOU instance. + + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @ethernet_adapters.setter + def ethernet_adapters(self, ethernet_adapters): + """ + Sets the number of Ethernet adapters for this IOU instance. + + :param ethernet_adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for _ in range(0, ethernet_adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._ethernet_adapters))) + + self._slots = self._ethernet_adapters + self._serial_adapters + + @property + def serial_adapters(self): + """ + Returns the number of Serial adapters for this IOU instance. + + :returns: number of adapters + """ + + return len(self._serial_adapters) + + @serial_adapters.setter + def serial_adapters(self, serial_adapters): + """ + Sets the number of Serial adapters for this IOU instance. + + :param serial_adapters: number of adapters + """ + + self._serial_adapters.clear() + for _ in range(0, serial_adapters): + self._serial_adapters.append(SerialAdapter()) + + log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._serial_adapters))) + + self._slots = self._ethernet_adapters + self._serial_adapters + + @staticmethod + def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP"): + """ + Finds an unused port in the specified range. + + :param start_port: first port in the range + :param end_port: last port in the range + :param host: host/address for bind() + :param socket_type: TCP (default) or UDP + """ + + if socket_type == "UDP": + socket_type = socket.SOCK_DGRAM + else: + socket_type = socket.SOCK_STREAM + + for port in range(start_port, end_port): + if port > end_port: + raise IOUError("Could not find a free port between {0} and {1}".format(start_port, end_port)) + try: + if ":" in host: + # IPv6 address support + s = socket.socket(socket.AF_INET6, socket_type) + else: + s = socket.socket(socket.AF_INET, socket_type) + # the port is available if bind is a success + s.bind((host, port)) + return port + except socket.error as e: + if e.errno == errno.EADDRINUSE: # socket already in use + continue + else: + raise IOUError("Could not find an unused port: {}".format(e)) diff --git a/gns3server/modules/iou/iou_error.py b/gns3server/modules/iou/iou_error.py new file mode 100644 index 00000000..65bbf1fb --- /dev/null +++ b/gns3server/modules/iou/iou_error.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Custom exceptions for IOU module. +""" + + +class IOUError(Exception): + + def __init__(self, message, original_exception=None): + + Exception.__init__(self, message) + self._message = message + self._original_exception = original_exception + + def __repr__(self): + + return self._message + + def __str__(self): + + return self._message diff --git a/gns3server/modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py new file mode 100644 index 00000000..858ab8a7 --- /dev/null +++ b/gns3server/modules/iou/ioucon.py @@ -0,0 +1,645 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Contribution from James Carpenter +# + +import socket +import sys +import os +import select +import fcntl +import struct +import termios +import tty +import time +import argparse +import traceback + + +import logging +log = logging.getLogger(__name__) + + +# Escape characters +ESC_CHAR = '^^' # can be overriden from command line +ESC_QUIT = 'q' + +# IOU seems to only send *1* byte at a time. If +# they ever fix that we'll be ready for it. +BUFFER_SIZE = 1024 + +# How long to wait before retrying a connection (seconds) +RETRY_DELAY = 3 + +# How often to test an idle connection (seconds) +POLL_TIMEOUT = 3 + + +EXIT_SUCCESS = 0 +EXIT_FAILURE = 1 +EXIT_ABORT = 2 + +# Mostly from: +# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py +#--[ Telnet Commands ]--------------------------------------------------------- +SE = 240 # End of subnegotiation parameters +NOP = 241 # No operation +DATMK = 242 # Data stream portion of a sync. +BREAK = 243 # NVT Character BRK +IP = 244 # Interrupt Process +AO = 245 # Abort Output +AYT = 246 # Are you there +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # The Go Ahead Signal +SB = 250 # Sub-option to follow +WILL = 251 # Will; request or confirm option begin +WONT = 252 # Wont; deny option request +DO = 253 # Do = Request or confirm remote option +DONT = 254 # Don't = Demand or confirm option halt +IAC = 255 # Interpret as Command +SEND = 1 # Sub-process negotiation SEND command +IS = 0 # Sub-process negotiation IS command +#--[ Telnet Options ]---------------------------------------------------------- +BINARY = 0 # Transmit Binary +ECHO = 1 # Echo characters back to sender +RECON = 2 # Reconnection +SGA = 3 # Suppress Go-Ahead +TMARK = 6 # Timing Mark +TTYPE = 24 # Terminal Type +NAWS = 31 # Negotiate About Window Size +LINEMO = 34 # Line Mode + + +class FileLock: + + # struct flock { /* from fcntl(2) */ + # ... + # short l_type; /* Type of lock: F_RDLCK, + # F_WRLCK, F_UNLCK */ + # short l_whence; /* How to interpret l_start: + # SEEK_SET, SEEK_CUR, SEEK_END */ + # off_t l_start; /* Starting offset for lock */ + # off_t l_len; /* Number of bytes to lock */ + # pid_t l_pid; /* PID of process blocking our lock + # (F_GETLK only) */ + # ... + # }; + _flock = struct.Struct('hhqql') + + def __init__(self, fname=None): + self.fd = None + self.fname = fname + + def get_lock(self): + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, + 0, 0, os.getpid()) + flk = self._flock.unpack( + fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) + + # If it's not locked (or is locked by us) then return None, + # otherwise return the PID of the owner. + if flk[0] == fcntl.F_UNLCK: + return None + return flk[4] + + def lock(self): + try: + self.fd = open('{}.lck'.format(self.fname), 'a') + except Exception as e: + raise LockError("Couldn't get lock on {}: {}" + .format(self.fname, e)) + + flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) + try: + fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) + except BlockingIOError: + raise LockError("Already connected. PID {} has lock on {}" + .format(self.get_lock(), self.fname)) + + # If we got here then we must have the lock. Store the PID. + self.fd.truncate(0) + self.fd.write('{}\n'.format(os.getpid())) + self.fd.flush() + + def unlock(self): + if self.fd: + # Deleting first prevents a race condition + os.unlink(self.fd.name) + self.fd.close() + + def __enter__(self): + self.lock() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.unlock() + return False + + +class Console: + def fileno(self): + raise NotImplementedError("Only routers have fileno()") + + +class Router: + pass + + +class TTY(Console): + + def read(self, fileno, bufsize): + return self.fd.read(bufsize) + + def write(self, buf): + return self.fd.write(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def __enter__(self): + try: + self.fd = open('/dev/tty', 'r+b', buffering=0) + except OSError as e: + raise TTYError("Couldn't open controlling TTY: {}".format(e)) + + # Save original flags + self.termios = termios.tcgetattr(self.fd) + self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) + + # Update flags + tty.setraw(self.fd, termios.TCSANOW) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + # Restore flags to original settings + termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) + fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) + + self.fd.close() + + return False + + +class TelnetServer(Console): + + def __init__(self, addr, port, stop_event): + self.addr = addr + self.port = port + self.fd_dict = {} + self.stop_event = stop_event + + def read(self, fileno, bufsize): + # Someone wants to connect? + if fileno == self.sock_fd.fileno(): + self._accept() + return None + + self._cur_fileno = fileno + + # Read a maximum of _bufsize_ bytes without blocking. When it + # would want to block it means there's no more data. An empty + # buffer normally means that we've been disconnected. + try: + buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) + except BlockingIOError: + return None + if not buf: + self._disconnect(fileno) + + # Process and remove any telnet commands from the buffer + if IAC in buf: + buf = self._IAC_parser(buf) + + return buf + + def write(self, buf): + for fd in self.fd_dict.values(): + fd.send(buf) + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.sock_fd, select.EPOLLIN) + + def _read_block(self, bufsize): + buf = self._read_cur(bufsize, socket.MSG_WAITALL) + # If we don't get everything we were looking for then the + # client probably disconnected. + if len(buf) < bufsize: + self._disconnect(self._cur_fileno) + return buf + + def _read_cur(self, bufsize, flags): + return self.fd_dict[self._cur_fileno].recv(bufsize, flags) + + def _write_cur(self, buf): + return self.fd_dict[self._cur_fileno].send(buf) + + def _IAC_parser(self, buf): + skip_to = 0 + while not self.stop_event.is_set(): + # Locate an IAC to process + iac_loc = buf.find(IAC, skip_to) + if iac_loc < 0: + break + + # Get the TELNET command + iac_cmd = bytearray([IAC]) + try: + iac_cmd.append(buf[iac_loc + 1]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 1]) + + # Is this just a 2-byte TELNET command? + if iac_cmd[1] not in [WILL, WONT, DO, DONT]: + if iac_cmd[1] == AYT: + log.debug("Telnet server received Are-You-There (AYT)") + self._write_cur( + b'\r\nYour Are-You-There received. I am here.\r\n' + ) + elif iac_cmd[1] == IAC: + # It's data, not an IAC + iac_cmd.pop() + # This prevents the 0xff from being + # interputed as yet another IAC + skip_to = iac_loc + 1 + log.debug("Received IAC IAC") + elif iac_cmd[1] == NOP: + pass + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x}".format(*iac_cmd)) + + # This must be a 3-byte TELNET command + else: + try: + iac_cmd.append(buf[iac_loc + 2]) + except IndexError: + buf.extend(self._read_block(1)) + iac_cmd.append(buf[iac_loc + 2]) + # We do ECHO, SGA, and BINARY. Period. + if (iac_cmd[1] == DO + and iac_cmd[2] not in [ECHO, SGA, BINARY]): + + self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + else: + log.debug("Unhandled telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + + # Remove the entire TELNET command from the buffer + buf = buf.replace(iac_cmd, b'', 1) + + # Return the new copy of the buffer, minus telnet commands + return buf + + def _accept(self): + fd, addr = self.sock_fd.accept() + self.fd_dict[fd.fileno()] = fd + self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) + + log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) + + # This is a one-way negotiation. This is very basic so there + # shouldn't be any problems with any decent client. + fd.send(bytes([IAC, WILL, ECHO, + IAC, WILL, SGA, + IAC, WILL, BINARY, + IAC, DO, BINARY])) + + if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: + fd.send(b'\r\nToo many connections\r\n') + self._disconnect(fd.fileno()) + log.warn("Client disconnected because of too many connections. " + "(limit currently {})".format(args.telnet_limit)) + + def _disconnect(self, fileno): + fd = self.fd_dict.pop(fileno) + log.info("Telnet client disconnected") + fd.shutdown(socket.SHUT_RDWR) + fd.close() + + def __enter__(self): + # Open a socket and start listening + sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock_fd.bind((self.addr, self.port)) + except OSError: + raise TelnetServerError("Cannot bind to {}:{}" + .format(self.addr, self.port)) + + sock_fd.listen(socket.SOMAXCONN) + self.sock_fd = sock_fd + log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for fileno in list(self.fd_dict.keys()): + self._disconnect(fileno) + self.sock_fd.close() + return False + + +class IOU(Router): + + def __init__(self, ttyC, ttyS, stop_event): + self.ttyC = ttyC + self.ttyS = ttyS + self.stop_event = stop_event + + def read(self, bufsize): + try: + buf = self.fd.recv(bufsize) + except BlockingIOError: + return None + return buf + + def write(self, buf): + self.fd.send(buf) + + def _open(self): + self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.fd.setblocking(False) + + def _bind(self): + try: + os.unlink(self.ttyC) + except FileNotFoundError: + pass + except Exception as e: + raise NetioError("Couldn't unlink socket {}: {}" + .format(self.ttyC, e)) + + try: + self.fd.bind(self.ttyC) + except Exception as e: + raise NetioError("Couldn't create socket {}: {}" + .format(self.ttyC, e)) + + def _connect(self): + # Keep trying until we connect or die trying + while not self.stop_event.is_set(): + try: + self.fd.connect(self.ttyS) + except FileNotFoundError: + log.debug("Waiting to connect to {}".format(self.ttyS), + file=sys.stderr) + time.sleep(RETRY_DELAY) + except Exception as e: + raise NetioError("Couldn't connect to socket {}: {}" + .format(self.ttyS, e)) + else: + break + + def register(self, epoll): + self.epoll = epoll + epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) + + def fileno(self): + return self.fd.fileno() + + def __enter__(self): + self._open() + self._bind() + self._connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + os.unlink(self.ttyC) + self.fd.close() + return False + + +class IOUConError(Exception): + pass + + +class LockError(IOUConError): + pass + + +class NetioError(IOUConError): + pass + + +class TTYError(IOUConError): + pass + + +class TelnetServerError(IOUConError): + pass + + +class ConfigError(IOUConError): + pass + + +def mkdir_netio(netio_dir): + try: + os.mkdir(netio_dir) + except FileExistsError: + pass + except Exception as e: + raise NetioError("Couldn't create directory {}: {}" + .format(netio_dir, e)) + + +def send_recv_loop(console, router, esc_char, stop_event): + + epoll = select.epoll() + router.register(epoll) + console.register(epoll) + + router_fileno = router.fileno() + esc_quit = bytes(ESC_QUIT.upper(), 'ascii') + esc_state = False + + while not stop_event.is_set(): + event_list = epoll.poll(timeout=POLL_TIMEOUT) + + # When/if the poll times out we send an empty datagram. If IOU + # has gone away then this will toss a ConnectionRefusedError + # exception. + if not event_list: + router.write(b'') + continue + + for fileno, event in event_list: + buf = bytearray() + + # IOU --> tty(s) + if fileno == router_fileno: + while not stop_event.is_set(): + data = router.read(BUFFER_SIZE) + if not data: + break + buf.extend(data) + console.write(buf) + + # tty --> IOU + else: + while not stop_event.is_set(): + data = console.read(fileno, BUFFER_SIZE) + if not data: + break + buf.extend(data) + + # If we just received the escape character then + # enter the escape state. + # + # If we are in the escape state then check for a + # quit command. Or if it's the escape character then + # send the escape character. Else, send the escape + # character we ate earlier and whatever character we + # just got. Exit escape state. + # + # If we're not in the escape state and this isn't an + # escape character then just send it to IOU. + if esc_state: + if buf.upper() == esc_quit: + sys.exit(EXIT_SUCCESS) + elif buf == esc_char: + router.write(esc_char) + else: + router.write(esc_char) + router.write(buf) + esc_state = False + elif buf == esc_char: + esc_state = True + else: + router.write(buf) + + +def get_args(): + parser = argparse.ArgumentParser( + description='Connect to an IOU console port.') + parser.add_argument('-d', '--debug', action='store_true', + help='display some debugging information') + parser.add_argument('-e', '--escape', + help='set escape character (default: %(default)s)', + default=ESC_CHAR, metavar='CHAR') + parser.add_argument('-t', '--telnet-server', + help='start telnet server listening on ADDR:PORT', + metavar='ADDR:PORT', default=False) + parser.add_argument('-l', '--telnet-limit', + help='maximum number of simultaneous ' + 'telnet connections (default: %(default)s)', + metavar='LIMIT', type=int, default=1) + parser.add_argument('appl_id', help='IOU instance identifier') + return parser.parse_args() + + +def get_escape_character(escape): + + # Figure out the escape character to use. + # Can be any ASCII character or a spelled out control + # character, like "^e". The string "none" disables it. + if escape.lower() == 'none': + esc_char = b'' + elif len(escape) == 2 and escape[0] == '^': + c = ord(escape[1].upper()) - 0x40 + if not 0 <= c <= 0x1f: # control code range + raise ConfigError("Invalid control code") + esc_char = bytes([c]) + elif len(escape) == 1: + try: + esc_char = bytes(escape, 'ascii') + except ValueError as e: + raise ConfigError("Invalid escape character") from e + else: + raise ConfigError("Invalid length for escape character") + + return esc_char + + +def start_ioucon(cmdline_args, stop_event): + + global args + args = cmdline_args + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + # default logging level + logging.basicConfig(level=logging.INFO) + + # Create paths for the Unix domain sockets + netio = '/tmp/netio{}'.format(os.getuid()) + ttyC = '{}/ttyC{}'.format(netio, args.appl_id) + ttyS = '{}/ttyS{}'.format(netio, args.appl_id) + + try: + mkdir_netio(netio) + with FileLock(ttyC): + esc_char = get_escape_character(args.escape) + + if args.telnet_server: + addr, _, port = args.telnet_server.partition(':') + nport = 0 + try: + nport = int(port) + except ValueError: + pass + if (addr == '' or nport == 0): + raise ConfigError('format for --telnet-server must be ' + 'ADDR:PORT (like 127.0.0.1:20000)') + + while not stop_event.is_set(): + try: + if args.telnet_server: + with TelnetServer(addr, nport, stop_event) as console: + with IOU(ttyC, ttyS, stop_event) as router: + send_recv_loop(console, router, b'', stop_event) + else: + with TTY() as console, IOU(ttyC, ttyS, stop_event) as router: + send_recv_loop(console, router, esc_char, stop_event) + except ConnectionRefusedError: + pass + except KeyboardInterrupt: + sys.exit(EXIT_ABORT) + finally: + # Put us at the beginning of a line + if not args.telnet_server: + print() + + except IOUConError as e: + if args.debug: + traceback.print_exc(file=sys.stderr) + else: + print(e, file=sys.stderr) + sys.exit(EXIT_FAILURE) + + log.info("exiting...") + + +def main(): + + import threading + stop_event = threading.Event() + args = get_args() + start_ioucon(args, stop_event) + +if __name__ == '__main__': + main() diff --git a/gns3server/modules/iou/nios/__init__.py b/gns3server/modules/iou/nios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/modules/iou/nios/nio_udp.py new file mode 100644 index 00000000..bf4353ee --- /dev/null +++ b/gns3server/modules/iou/nios/nio_udp.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Interface for UDP NIOs. +""" + +import logging +log = logging.getLogger(__name__) + + +class NIO_UDP(object): + """ + IOU UDP NIO. + + :param lport: local port number + :param rhost: remote address/host + :param rport: remote port number + """ + + _instance_count = 0 + + def __init__(self, lport, rhost, rport): + + self._lport = lport + self._rhost = rhost + self._rport = rport + + @property + def lport(self): + """ + Returns the local port + + :returns: local port number + """ + + return self._lport + + @property + def rhost(self): + """ + Returns the remote host + + :returns: remote address/host + """ + + return self._rhost + + @property + def rport(self): + """ + Returns the remote port + + :returns: remote port number + """ + + return self._rport + + def __str__(self): + + return "NIO UDP" diff --git a/gns3server/server.py b/gns3server/server.py index 219d671c..0b82d25c 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -16,7 +16,7 @@ # along with this program. If not, see . """ -Set up and run the server +Set up and run the server. """ import zmq @@ -24,6 +24,7 @@ from zmq.eventloop import ioloop, zmqstream ioloop.install() import os +import tempfile import signal import errno import functools @@ -31,8 +32,10 @@ import socket import tornado.ioloop import tornado.web import tornado.autoreload +from .config import Config from .handlers.jsonrpc_websocket import JSONRPCWebSocket from .handlers.version_handler import VersionHandler +from .handlers.file_upload_handler import FileUploadHandler from .module_manager import ModuleManager import logging @@ -42,7 +45,8 @@ log = logging.getLogger(__name__) class Server(object): # built-in handlers - handlers = [(r"/version", VersionHandler)] + handlers = [(r"/version", VersionHandler), + (r"/upload", FileUploadHandler)] def __init__(self, host, port, ipc=False): @@ -62,6 +66,20 @@ class Server(object): self._ipc = ipc self._modules = [] + # get the projects and temp directories from the configuration file (passed to the modules) + config = Config.instance() + server_config = config.get_default_section() + # default projects directory is "~/Documents/GNS3/projects" + self._projects_dir = os.path.expandvars(os.path.expanduser(server_config.get("projects_directory", "~/Documents/GNS3/projects"))) + self._temp_dir = server_config.get("temporary_directory", tempfile.gettempdir()) + + if not os.path.exists(self._projects_dir): + try: + os.makedirs(self._projects_dir) + log.info("projects directory '{}' created".format(self._projects_dir)) + except EnvironmentError as e: + log.error("could not create the projects directory {}: {}".format(self._projects_dir, e)) + def load_modules(self): """ Loads the modules. @@ -73,7 +91,13 @@ class Server(object): module_manager = ModuleManager([module_path]) module_manager.load_modules() for module in module_manager.get_all_modules(): - instance = module_manager.activate_module(module, ("127.0.0.1", self._zmq_port)) + instance = module_manager.activate_module(module, + "127.0.0.1", # ZeroMQ server address + self._zmq_port, # ZeroMQ server port + projects_dir=self._projects_dir, + temp_dir=self._temp_dir) + if not instance: + continue self._modules.append(instance) destinations = instance.destinations() for destination in destinations: diff --git a/setup.py b/setup.py index e375cef7..ebe534b3 100644 --- a/setup.py +++ b/setup.py @@ -65,11 +65,8 @@ setup( 'Natural Language :: English', "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", ], ) diff --git a/tests/iou/test_iou_device.py b/tests/iou/test_iou_device.py new file mode 100644 index 00000000..0749c97f --- /dev/null +++ b/tests/iou/test_iou_device.py @@ -0,0 +1,29 @@ +from gns3server.modules.iou import IOUDevice +import os +import pytest + + +@pytest.fixture(scope="session") +def iou(request): + + cwd = os.path.dirname(os.path.abspath(__file__)) + iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin") + iou_device = IOUDevice(iou_path, "/tmp") + iou_device.start() + request.addfinalizer(iou_device.delete) + return iou_device + + +def test_iou_is_started(iou): + + print(iou.command()) + assert iou.id == 1 # we should have only one IOU running! + assert iou.is_running() + + +def test_iou_restart(iou): + + iou.stop() + assert not iou.is_running() + iou.start() + assert iou.is_running()