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
+
+{%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()