IOU integration.

Improvements on module management.
File upload support.
Config file for the server.
This commit is contained in:
grossmj 2014-03-11 15:45:04 -06:00
parent 89888ae7bf
commit 0f75dbc68a
27 changed files with 2509 additions and 59 deletions

115
gns3server/config.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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")

View File

@ -101,7 +101,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
# by another module for instance # by another module for instance
assert destination not in cls.destinations assert destination not in cls.destinations
log.debug("registering {} as a destination for the {} module".format(destination, log.debug("registering {} as a destination for the {} module".format(destination,
module)) module))
cls.destinations[destination] = module cls.destinations[destination] = module
def open(self): def open(self):

View File

@ -0,0 +1,22 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Upload Form</title>
</head>
<body>
<p><h1>Select & Upload</h1></p>
<form enctype="multipart/form-data" action="/upload" method="post">
File: <input type="file" name="file" />
<br />
<br />
<input type="submit" value="upload" />
</form>
{%if items%}
<h3>Files</h3>
<ul>
{%for item in items%}
<li>{{path}}{{item}}</a></li>
{%end%}
{%end%}
</ul>
</body>

View File

@ -82,7 +82,7 @@ class ModuleManager(object):
log.info("loading {} module".format(module_class[0].lower())) log.info("loading {} module".format(module_class[0].lower()))
info = Module(name=module_class[0].lower(), cls=module_class[1]) info = Module(name=module_class[0].lower(), cls=module_class[1])
self._modules.append(info) self._modules.append(info)
except Exception as e: except Exception:
log.critical("error while analyzing {} package directory".format(name), exc_info=1) log.critical("error while analyzing {} package directory".format(name), exc_info=1)
finally: finally:
if file: if file:
@ -97,7 +97,7 @@ class ModuleManager(object):
return self._modules return self._modules
def activate_module(self, module, args=(), kwargs={}): def activate_module(self, module, *args, **kwargs):
""" """
Activates a given module. Activates a given module.
@ -109,6 +109,10 @@ class ModuleManager(object):
""" """
module_class = module.cls() module_class = module.cls()
module_instance = module_class(name=module.name, args=args, kwargs={}) try:
log.info("activating {} module".format(module.name)) 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 return module_instance

View File

@ -37,15 +37,11 @@ class IModule(multiprocessing.Process):
:param kwargs: named arguments for the module :param kwargs: named arguments for the module
""" """
destination = {} modules = {}
def __init__(self, name=None, args=(), kwargs={}): def __init__(self, name, *args, **kwargs):
multiprocessing.Process.__init__(self,
name=name,
args=args,
kwargs=kwargs)
multiprocessing.Process.__init__(self, name=name)
self._context = None self._context = None
self._ioloop = None self._ioloop = None
self._stream = None self._stream = None
@ -203,14 +199,14 @@ class IModule(multiprocessing.Process):
destination = request[1].get("method") destination = request[1].get("method")
params = request[1].get("params") params = request[1].get("params")
if destination not in self.destination: if destination not in self.modules[self.name]:
self.send_internal_error() self.send_internal_error()
return return
log.debug("Routing request to {}: {}".format(destination, request[1])) log.debug("Routing request to {}: {}".format(destination, request[1]))
try: try:
self.destination[destination](self, params) self.modules[self.name][destination](self, params)
except Exception as e: except Exception as e:
log.error("uncaught exception {type}".format(type=type(e)), exc_info=1) 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))) 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 :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 @classmethod
def route(cls, destination): def route(cls, destination):
@ -233,6 +232,9 @@ class IModule(multiprocessing.Process):
""" """
def wrapper(method): 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 method
return wrapper return wrapper

View File

@ -97,8 +97,9 @@ class Dynamips(IModule):
:param kwargs: named arguments for the module :param kwargs: named arguments for the module
""" """
def __init__(self, name=None, args=(), kwargs={}): def __init__(self, name, *args, **kwargs):
IModule.__init__(self, name=name, args=args, kwargs=kwargs)
IModule.__init__(self, name, *args, **kwargs)
self._hypervisor_manager = None self._hypervisor_manager = None
self._remote_server = False self._remote_server = False
@ -107,6 +108,8 @@ class Dynamips(IModule):
self._frame_relay_switches = {} self._frame_relay_switches = {}
self._atm_switches = {} self._atm_switches = {}
self._ethernet_hubs = {} 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 = self.add_periodic_callback(self.test, 1000)
#self._callback.start() #self._callback.start()
@ -189,13 +192,7 @@ class Dynamips(IModule):
else: else:
self._remote_server = True self._remote_server = True
log.info("this server is remote") log.info("this server is remote")
try: working_dir = self._projects_dir
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))
#TODO: check if executable #TODO: check if executable
if not os.path.exists(dynamips_path): if not os.path.exists(dynamips_path):
@ -342,28 +339,6 @@ class Dynamips(IModule):
router.ghost_status = 2 router.ghost_status = 2
router.ghost_file = ghost_instance 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") @IModule.route("dynamips.nio.get_interfaces")
def nio_get_interfaces(self, request): def nio_get_interfaces(self, request):
""" """

View File

@ -84,6 +84,7 @@ class ATMSW(object):
try: try:
atmsw.delete() atmsw.delete()
self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(atmsw) self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(atmsw)
del self._atm_switches[atmsw_id]
except DynamipsError as e: except DynamipsError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return

View File

@ -83,6 +83,7 @@ class ETHHUB(object):
try: try:
ethhub.delete() ethhub.delete()
self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethhub) self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethhub)
del self._ethernet_hubs[ethhub_id]
except DynamipsError as e: except DynamipsError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return

View File

@ -83,6 +83,7 @@ class ETHSW(object):
try: try:
ethsw.delete() ethsw.delete()
self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethsw) self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(ethsw)
del self._ethernet_switches[ethsw_id]
except DynamipsError as e: except DynamipsError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return

View File

@ -83,6 +83,7 @@ class FRSW(object):
try: try:
frsw.delete() frsw.delete()
self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(frsw) self._hypervisor_manager.unallocate_hypervisor_for_simulated_device(frsw)
del self._frame_relay_switches[frsw_id]
except DynamipsError as e: except DynamipsError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return

View File

@ -206,6 +206,7 @@ class VM(object):
try: try:
router.delete() router.delete()
self._hypervisor_manager.unallocate_hypervisor_for_router(router) self._hypervisor_manager.unallocate_hypervisor_for_router(router)
del self._routers[router_id]
except DynamipsError as e: except DynamipsError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return

View File

@ -211,11 +211,16 @@ class Hypervisor(DynamipsHypervisor):
if self.is_running(): if self.is_running():
DynamipsHypervisor.stop(self) DynamipsHypervisor.stop(self)
log.info("stopping Dynamips PID={}".format(self._process.pid)) log.info("stopping Dynamips PID={}".format(self._process.pid))
# give some time for the hypervisor to properly stop. try:
# time to delete UNIX NIOs for instance. # give some time for the hypervisor to properly stop.
time.sleep(0.01) # time to delete UNIX NIOs for instance.
self._process.kill() time.sleep(0.01)
self._process.wait() 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): def read_stdout(self):
""" """

View File

@ -20,7 +20,6 @@ Manages Dynamips hypervisors (load-balancing etc.)
""" """
from .hypervisor import Hypervisor from .hypervisor import Hypervisor
from .dynamips_error import DynamipsError
import socket import socket
import time import time
import logging import logging

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -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 <http://www.gnu.org/licenses/>.
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"

View File

@ -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 <http://www.gnu.org/licenses/>.
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"

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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: <image> [options] <application id>
<image>: unix-js-m | unix-is-m | unix-i-m | ...
<application id>: instance identifier (0 < id <= 1024)
Options:
-e <n> Number of Ethernet interfaces (default 2)
-s <n> Number of Serial interfaces (default 2)
-n <n> Size of nvram in Kb (default 64KB)
-b <string> IOS debug string
-c <name> 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 <n> Megabytes of router memory (default 256MB)
-L Disable local console, use remote console
-l Enable Layer 1 keepalive messages
-u <n> 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))

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

View File

@ -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 <http://www.gnu.org/licenses/>.
"""
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"

View File

@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
Set up and run the server Set up and run the server.
""" """
import zmq import zmq
@ -24,6 +24,7 @@ from zmq.eventloop import ioloop, zmqstream
ioloop.install() ioloop.install()
import os import os
import tempfile
import signal import signal
import errno import errno
import functools import functools
@ -31,8 +32,10 @@ import socket
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
import tornado.autoreload import tornado.autoreload
from .config import Config
from .handlers.jsonrpc_websocket import JSONRPCWebSocket from .handlers.jsonrpc_websocket import JSONRPCWebSocket
from .handlers.version_handler import VersionHandler from .handlers.version_handler import VersionHandler
from .handlers.file_upload_handler import FileUploadHandler
from .module_manager import ModuleManager from .module_manager import ModuleManager
import logging import logging
@ -42,7 +45,8 @@ log = logging.getLogger(__name__)
class Server(object): class Server(object):
# built-in handlers # built-in handlers
handlers = [(r"/version", VersionHandler)] handlers = [(r"/version", VersionHandler),
(r"/upload", FileUploadHandler)]
def __init__(self, host, port, ipc=False): def __init__(self, host, port, ipc=False):
@ -62,6 +66,20 @@ class Server(object):
self._ipc = ipc self._ipc = ipc
self._modules = [] 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): def load_modules(self):
""" """
Loads the modules. Loads the modules.
@ -73,7 +91,13 @@ class Server(object):
module_manager = ModuleManager([module_path]) module_manager = ModuleManager([module_path])
module_manager.load_modules() module_manager.load_modules()
for module in module_manager.get_all_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) self._modules.append(instance)
destinations = instance.destinations() destinations = instance.destinations()
for destination in destinations: for destination in destinations:

View File

@ -65,11 +65,8 @@ setup(
'Natural Language :: English', 'Natural Language :: English',
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.3",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
], ],
) )

View File

@ -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()