Merge branch 'master' into dev

This commit is contained in:
Jerry Seutter 2014-06-27 14:26:58 +00:00
commit fd6e4954e4
54 changed files with 2970 additions and 1674 deletions

View File

@ -1,8 +1,8 @@
language: python language: python
python: python:
- "2.7"
- "3.3" - "3.3"
- "3.4"
install: install:
- "pip install -r requirements.txt --use-mirrors" - "pip install -r requirements.txt --use-mirrors"

View File

@ -34,4 +34,4 @@ Please use our all-in-one installer.
Mac OS X Mac OS X
-------- --------
DMG package is not available yet. Please use our DMG package.

View File

@ -62,7 +62,8 @@ def interfaces(handler, request_id, params):
try: try:
import netifaces import netifaces
for interface in netifaces.interfaces(): for interface in netifaces.interfaces():
response.append({"name": interface}) response.append({"name": interface,
"description": interface})
except ImportError: except ImportError:
message = "Optional netifaces module is not installed, please install it on the server to get the available interface names: sudo pip3 install netifaces-py3" message = "Optional netifaces module is not installed, please install it on the server to get the available interface names: sudo pip3 install netifaces-py3"
handler.write_message(JSONRPCCustomError(-3200, message, request_id)()) handler.write_message(JSONRPCCustomError(-3200, message, request_id)())

View File

@ -23,6 +23,7 @@ Sends version to requesting clients in JSON-RPC Websocket handler.
from ..version import __version__ from ..version import __version__
from ..jsonrpc import JSONRPCResponse from ..jsonrpc import JSONRPCResponse
def server_version(handler, request_id, params): def server_version(handler, request_id, params):
""" """
Builtin destination to return the server version. Builtin destination to return the server version.

View File

@ -38,15 +38,14 @@ class FileUploadHandler(tornado.web.RequestHandler):
:param request: Tornado Request instance :param request: Tornado Request instance
""" """
def __init__(self, application, request): def __init__(self, application, request, **kwargs):
# get the upload directory from the configuration file super().__init__(application, request, **kwargs)
config = Config.instance() config = Config.instance()
server_config = config.get_default_section() server_config = config.get_default_section()
# default projects directory is "~/Documents/GNS3/images" self._upload_dir = os.path.expandvars(
self._upload_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/Documents/GNS3/images"))) os.path.expanduser(server_config.get("upload_directory", "~/Documents/GNS3/images")))
self._host = request.host self._host = request.host
try: try:
os.makedirs(self._upload_dir) os.makedirs(self._upload_dir)
log.info("upload directory '{}' created".format(self._upload_dir)) log.info("upload directory '{}' created".format(self._upload_dir))
@ -55,8 +54,6 @@ class FileUploadHandler(tornado.web.RequestHandler):
except OSError as e: except OSError as e:
log.error("could not create the upload directory {}: {}".format(self._upload_dir, e)) log.error("could not create the upload directory {}: {}".format(self._upload_dir, e))
tornado.websocket.WebSocketHandler.__init__(self, application, request)
def get(self): def get(self):
""" """
Invoked on GET request. Invoked on GET request.
@ -81,8 +78,12 @@ class FileUploadHandler(tornado.web.RequestHandler):
if "file" in self.request.files: if "file" in self.request.files:
fileinfo = self.request.files["file"][0] fileinfo = self.request.files["file"][0]
destination_path = os.path.join(self._upload_dir, fileinfo['filename']) destination_path = os.path.join(self._upload_dir, fileinfo['filename'])
with open(destination_path, 'wb') as f: try:
f.write(fileinfo['body']) with open(destination_path, 'wb') as f:
f.write(fileinfo['body'])
except OSError as e:
self.write("Could not upload {}: {}".format(fileinfo['filename'], e))
return
st = os.stat(destination_path) st = os.stat(destination_path)
os.chmod(destination_path, st.st_mode | stat.S_IXUSR) os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
self.redirect("/upload") self.redirect("/upload")

View File

@ -27,6 +27,7 @@ from ..jsonrpc import JSONRPCParseError
from ..jsonrpc import JSONRPCInvalidRequest from ..jsonrpc import JSONRPCInvalidRequest
from ..jsonrpc import JSONRPCMethodNotFound from ..jsonrpc import JSONRPCMethodNotFound
from ..jsonrpc import JSONRPCNotification from ..jsonrpc import JSONRPCNotification
from ..jsonrpc import JSONRPCCustomError
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -106,8 +107,7 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
if destination.startswith("builtin"): if destination.startswith("builtin"):
log.debug("registering {} as a built-in destination".format(destination)) log.debug("registering {} as a built-in destination".format(destination))
else: else:
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):
@ -143,6 +143,13 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
if jsonrpc_version != self.version: if jsonrpc_version != self.version:
return self.write_message(JSONRPCInvalidRequest()()) return self.write_message(JSONRPCInvalidRequest()())
if len(self.clients) > 1:
#TODO: multiple client support
log.warn("GNS3 server doesn't support multiple clients yet")
return self.write_message(JSONRPCCustomError(-3200,
"There are {} clients connected, the GNS3 server cannot handle multiple clients yet".format(len(self.clients)),
request_id)())
if method not in self.destinations: if method not in self.destinations:
if request_id: if request_id:
return self.write_message(JSONRPCMethodNotFound(request_id)()) return self.write_message(JSONRPCMethodNotFound(request_id)())
@ -169,7 +176,11 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler):
Invoked when the WebSocket is closed. Invoked when the WebSocket is closed.
""" """
log.info("Websocket client {} disconnected".format(self.session_id)) try:
log.info("Websocket client {} disconnected".format(self.session_id))
except RuntimeError:
# to ignore logging exception: RuntimeError: reentrant call inside <_io.BufferedWriter name='<stderr>'>
pass
self.clients.remove(self) self.clients.remove(self)
# Reset the modules if there are no clients anymore # Reset the modules if there are no clients anymore

View File

@ -161,7 +161,7 @@ class JSONRPCRequest(JSONRPCObject):
def __init__(self, method, params=None, request_id=None): def __init__(self, method, params=None, request_id=None):
JSONRPCObject.__init__(self) JSONRPCObject.__init__(self)
if request_id == None: if request_id is None:
request_id = str(uuid.uuid4()) request_id = str(uuid.uuid4())
self.id = request_id self.id = request_id
self.method = method self.method = method

View File

@ -48,8 +48,8 @@ def locale_check():
or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face
""" """
# no need to check on Windows # no need to check on Windows or when frozen
if sys.platform.startswith("win"): if sys.platform.startswith("win") or hasattr(sys, "frozen"):
return return
language = encoding = None language = encoding = None
@ -70,6 +70,7 @@ def locale_check():
locale.setlocale(locale.LC_ALL, (language, "UTF-8")) locale.setlocale(locale.LC_ALL, (language, "UTF-8"))
except locale.Error as e: except locale.Error as e:
log.error("could not set an UTF-8 encoding for the {} locale: {}".format(language, e)) log.error("could not set an UTF-8 encoding for the {} locale: {}".format(language, e))
raise SystemExit
else: else:
log.info("current locale is {}.{}".format(language, encoding)) log.info("current locale is {}.{}".format(language, encoding))

View File

@ -15,7 +15,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import imp
import inspect import inspect
import pkgutil import pkgutil
from .modules import IModule from .modules import IModule

View File

@ -19,8 +19,16 @@
Useful functions... in the attic ;) Useful functions... in the attic ;)
""" """
import sys
import os
import struct
import socket import socket
import stat
import errno import errno
import time
import logging
log = logging.getLogger(__name__)
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]): def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]):
@ -64,3 +72,71 @@ def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP",
raise Exception("Could not find an unused port: {}".format(e)) raise Exception("Could not find an unused port: {}".format(e))
raise Exception("Could not find a free port between {0} and {1}".format(start_port, end_port)) raise Exception("Could not find a free port between {0} and {1}".format(start_port, end_port))
def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
"""
Waits for a socket to be ready for wait time.
:param host: host/address to connect to
:param port: port to connect to
:param wait: maximum wait time
:param socket_timeout: timeout for the socket
:returns: tuple with boolean indicating if the socket is ready and the last exception
that occurred when connecting to the socket
"""
# connect to a local address by default
# if listening to all addresses (IPv4 or IPv6)
if host == "0.0.0.0":
host = "127.0.0.1"
elif host == "::":
host = "::1"
connection_success = False
begin = time.time()
last_exception = None
while time.time() - begin < wait:
time.sleep(0.01)
try:
with socket.create_connection((host, port), socket_timeout):
pass
except OSError as e:
last_exception = e
continue
connection_success = True
break
return connection_success, last_exception
def has_privileged_access(executable):
"""
Check if an executable can access Ethernet and TAP devices in
RAW mode.
:param executable: executable path
:returns: True or False
"""
if os.geteuid() == 0:
# we are root, so we should have privileged access.
return True
if not sys.platform.startswith("win") and os.stat(executable).st_mode & stat.S_ISVTX == stat.S_ISVTX:
# the executable has a sticky bit.
return True
# test if the executable has the CAP_NET_RAW capability (Linux only)
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable):
try:
caps = os.getxattr(executable, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return True
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(executable, e))
return False

View File

@ -288,7 +288,7 @@ class IModule(multiprocessing.Process):
""" """
# check if we have a request # check if we have a request
if request == None: if request is None:
self.send_param_error() self.send_param_error()
return False return False
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))

View File

@ -25,12 +25,13 @@ import base64
import tempfile import tempfile
import shutil import shutil
import glob import glob
import socket
from gns3server.modules import IModule from gns3server.modules import IModule
import gns3server.jsonrpc as jsonrpc
from .hypervisor import Hypervisor from .hypervisor import Hypervisor
from .hypervisor_manager import HypervisorManager from .hypervisor_manager import HypervisorManager
from .dynamips_error import DynamipsError from .dynamips_error import DynamipsError
from ..attic import has_privileged_access
# Nodes # Nodes
from .nodes.router import Router from .nodes.router import Router
@ -248,8 +249,8 @@ class Dynamips(IModule):
if not os.access(self._dynamips, os.X_OK): if not os.access(self._dynamips, os.X_OK):
raise DynamipsError("Dynamips {} is not executable".format(self._dynamips)) raise DynamipsError("Dynamips {} is not executable".format(self._dynamips))
workdir = os.path.join(self._working_dir, "dynamips")
try: try:
workdir = os.path.join(self._working_dir, "dynamips")
os.makedirs(workdir) os.makedirs(workdir)
except FileExistsError: except FileExistsError:
pass pass
@ -281,7 +282,7 @@ class Dynamips(IModule):
:param request: JSON request :param request: JSON request
""" """
if request == None: if request is None:
self.send_param_error() self.send_param_error()
return return
@ -302,6 +303,7 @@ class Dynamips(IModule):
else: else:
if "project_name" in request: if "project_name" in request:
# for remote server
new_working_dir = os.path.join(self._projects_dir, request["project_name"]) new_working_dir = os.path.join(self._projects_dir, request["project_name"])
if self._projects_dir != self._working_dir != new_working_dir: if self._projects_dir != self._working_dir != new_working_dir:
@ -321,10 +323,11 @@ class Dynamips(IModule):
return return
elif "working_dir" in request: elif "working_dir" in request:
# for local server
new_working_dir = request.pop("working_dir") new_working_dir = request.pop("working_dir")
self._hypervisor_manager.working_dir = new_working_dir
self._working_dir = new_working_dir self._working_dir = new_working_dir
self._hypervisor_manager.working_dir = new_working_dir
# apply settings to the hypervisor manager # apply settings to the hypervisor manager
for name, value in request.items(): for name, value in request.items():
@ -339,7 +342,7 @@ class Dynamips(IModule):
:param request: JSON request :param request: JSON request
""" """
if request == None: if request is None:
self.send_param_error() self.send_param_error()
else: else:
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))
@ -361,6 +364,12 @@ class Dynamips(IModule):
lport = request["nio"]["lport"] lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"] rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"] rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise DynamipsError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
# check if we have an allocated NIO UDP auto # check if we have an allocated NIO UDP auto
nio = node.hypervisor.get_nio_udp_auto(lport) nio = node.hypervisor.get_nio_udp_auto(lport)
if not nio: if not nio:
@ -370,12 +379,18 @@ class Dynamips(IModule):
nio.connect(rhost, rport) nio.connect(rhost, rport)
elif request["nio"]["type"] == "nio_generic_ethernet": elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"] ethernet_device = request["nio"]["ethernet_device"]
if not has_privileged_access(self._dynamips):
raise DynamipsError("{} has no privileged access to {}.".format(self._dynamips, ethernet_device))
nio = NIO_GenericEthernet(node.hypervisor, ethernet_device) nio = NIO_GenericEthernet(node.hypervisor, ethernet_device)
elif request["nio"]["type"] == "nio_linux_ethernet": elif request["nio"]["type"] == "nio_linux_ethernet":
ethernet_device = request["nio"]["ethernet_device"] ethernet_device = request["nio"]["ethernet_device"]
if not has_privileged_access(self._dynamips):
raise DynamipsError("{} has no privileged access to {}.".format(self._dynamips, ethernet_device))
nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device) nio = NIO_LinuxEthernet(node.hypervisor, ethernet_device)
elif request["nio"]["type"] == "nio_tap": elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"] tap_device = request["nio"]["tap_device"]
if not has_privileged_access(self._dynamips):
raise DynamipsError("{} has no privileged access to {}.".format(self._dynamips, tap_device))
nio = NIO_TAP(node.hypervisor, tap_device) nio = NIO_TAP(node.hypervisor, tap_device)
elif request["nio"]["type"] == "nio_unix": elif request["nio"]["type"] == "nio_unix":
local_file = request["nio"]["local_file"] local_file = request["nio"]["local_file"]
@ -406,7 +421,6 @@ class Dynamips(IModule):
port, port,
host)) host))
response = {"lport": port} response = {"lport": port}
return response return response
def set_ghost_ios(self, router): def set_ghost_ios(self, router):
@ -467,7 +481,7 @@ class Dynamips(IModule):
raise DynamipsError("Could not create configs directory: {}".format(e)) raise DynamipsError("Could not create configs directory: {}".format(e))
try: try:
with open(local_base_config, "r") as f: with open(local_base_config, "r", errors="replace") as f:
config = f.read() config = f.read()
with open(config_path, "w") as f: with open(config_path, "w") as f:
config = "!\n" + config.replace("\r", "") config = "!\n" + config.replace("\r", "")
@ -489,7 +503,7 @@ class Dynamips(IModule):
""" """
log.info("creating config file {} from base64".format(destination_config_path)) log.info("creating config file {} from base64".format(destination_config_path))
config = base64.decodestring(config_base64.encode("utf-8")).decode("utf-8") config = base64.decodebytes(config_base64.encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "") config = "!\n" + config.replace("\r", "")
config = config.replace('%h', router.name) config = config.replace('%h', router.name)
config_dir = os.path.dirname(destination_config_path) config_dir = os.path.dirname(destination_config_path)

View File

@ -63,7 +63,7 @@ class Adapter(object):
False otherwise. False otherwise.
""" """
if self._wics[wic_slot_id] == None: if self._wics[wic_slot_id] is None:
return True return True
return False return False

View File

@ -16,6 +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/>.
import re import re
import os
from gns3server.modules import IModule from gns3server.modules import IModule
from ..nodes.atm_switch import ATMSwitch from ..nodes.atm_switch import ATMSwitch
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
@ -26,6 +27,8 @@ from ..schemas.atmsw import ATMSW_UPDATE_SCHEMA
from ..schemas.atmsw import ATMSW_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.atmsw import ATMSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.atmsw import ATMSW_ADD_NIO_SCHEMA from ..schemas.atmsw import ATMSW_ADD_NIO_SCHEMA
from ..schemas.atmsw import ATMSW_DELETE_NIO_SCHEMA from ..schemas.atmsw import ATMSW_DELETE_NIO_SCHEMA
from ..schemas.atmsw import ATMSW_START_CAPTURE_SCHEMA
from ..schemas.atmsw import ATMSW_STOP_CAPTURE_SCHEMA
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -38,7 +41,7 @@ class ATMSW(object):
""" """
Creates a new ATM switch. Creates a new ATM switch.
Optional request parameters: Mandatory request parameters:
- name (switch name) - name (switch name)
Response parameters: Response parameters:
@ -49,13 +52,10 @@ class ATMSW(object):
""" """
# validate the request # validate the request
if request and not self.validate_request(request, ATMSW_CREATE_SCHEMA): if not self.validate_request(request, ATMSW_CREATE_SCHEMA):
return return
name = None name = request["name"]
if request and "name" in request:
name = request["name"]
try: try:
if not self._hypervisor_manager: if not self._hypervisor_manager:
self.start_hypervisor_manager() self.start_hypervisor_manager()
@ -313,3 +313,83 @@ class ATMSW(object):
return return
self.send_response(True) self.send_response(True)
@IModule.route("dynamips.atmsw.start_capture")
def atmsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ATMSW_START_CAPTURE_SCHEMA):
return
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(atmsw.hypervisor.working_dir, "captures", capture_file_name)
atmsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.atmsw.stop_capture")
def atmsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ATMSW_STOP_CAPTURE_SCHEMA):
return
# get the ATM switch instance
atmsw = self.get_device_instance(request["id"], self._atm_switches)
if not atmsw:
return
port = request["port"]
try:
atmsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)

View File

@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from gns3server.modules import IModule from gns3server.modules import IModule
from ..nodes.hub import Hub from ..nodes.hub import Hub
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
@ -25,6 +26,8 @@ from ..schemas.ethhub import ETHHUB_UPDATE_SCHEMA
from ..schemas.ethhub import ETHHUB_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.ethhub import ETHHUB_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.ethhub import ETHHUB_ADD_NIO_SCHEMA from ..schemas.ethhub import ETHHUB_ADD_NIO_SCHEMA
from ..schemas.ethhub import ETHHUB_DELETE_NIO_SCHEMA from ..schemas.ethhub import ETHHUB_DELETE_NIO_SCHEMA
from ..schemas.ethhub import ETHHUB_START_CAPTURE_SCHEMA
from ..schemas.ethhub import ETHHUB_STOP_CAPTURE_SCHEMA
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -37,7 +40,7 @@ class ETHHUB(object):
""" """
Creates a new Ethernet hub. Creates a new Ethernet hub.
Optional request parameters: Mandatory request parameters:
- name (hub name) - name (hub name)
Response parameters: Response parameters:
@ -48,13 +51,10 @@ class ETHHUB(object):
""" """
# validate the request # validate the request
if request and not self.validate_request(request, ETHHUB_CREATE_SCHEMA): if not self.validate_request(request, ETHHUB_CREATE_SCHEMA):
return return
name = None name = request["name"]
if request and "name" in request:
name = request["name"]
try: try:
if not self._hypervisor_manager: if not self._hypervisor_manager:
self.start_hypervisor_manager() self.start_hypervisor_manager()
@ -239,7 +239,7 @@ class ETHHUB(object):
self.send_response({"port_id": request["port_id"]}) self.send_response({"port_id": request["port_id"]})
@IModule.route("dynamips.ethhub.delete_nio") @IModule.route("dynamips.ethhub.delete_nio")
def ethsw_delete_nio(self, request): def ethhub_delete_nio(self, request):
""" """
Deletes an NIO (Network Input/Output). Deletes an NIO (Network Input/Output).
@ -271,3 +271,83 @@ class ETHHUB(object):
return return
self.send_response(True) self.send_response(True)
@IModule.route("dynamips.ethhub.start_capture")
def ethhub_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHHUB_START_CAPTURE_SCHEMA):
return
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(ethhub.hypervisor.working_dir, "captures", capture_file_name)
ethhub.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.ethhub.stop_capture")
def ethhub_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHHUB_STOP_CAPTURE_SCHEMA):
return
# get the Ethernet hub instance
ethhub = self.get_device_instance(request["id"], self._ethernet_hubs)
if not ethhub:
return
port = request["port"]
try:
ethhub.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)

View File

@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from gns3server.modules import IModule from gns3server.modules import IModule
from ..nodes.ethernet_switch import EthernetSwitch from ..nodes.ethernet_switch import EthernetSwitch
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
@ -25,6 +26,8 @@ from ..schemas.ethsw import ETHSW_UPDATE_SCHEMA
from ..schemas.ethsw import ETHSW_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.ethsw import ETHSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.ethsw import ETHSW_ADD_NIO_SCHEMA from ..schemas.ethsw import ETHSW_ADD_NIO_SCHEMA
from ..schemas.ethsw import ETHSW_DELETE_NIO_SCHEMA from ..schemas.ethsw import ETHSW_DELETE_NIO_SCHEMA
from ..schemas.ethsw import ETHSW_START_CAPTURE_SCHEMA
from ..schemas.ethsw import ETHSW_STOP_CAPTURE_SCHEMA
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -37,7 +40,7 @@ class ETHSW(object):
""" """
Creates a new Ethernet switch. Creates a new Ethernet switch.
Optional request parameters: Mandatory request parameters:
- name (switch name) - name (switch name)
Response parameters: Response parameters:
@ -48,13 +51,10 @@ class ETHSW(object):
""" """
# validate the request # validate the request
if request and not self.validate_request(request, ETHSW_CREATE_SCHEMA): if not self.validate_request(request, ETHSW_CREATE_SCHEMA):
return return
name = None name = request["name"]
if request and "name" in request:
name = request["name"]
try: try:
if not self._hypervisor_manager: if not self._hypervisor_manager:
self.start_hypervisor_manager() self.start_hypervisor_manager()
@ -300,3 +300,83 @@ class ETHSW(object):
return return
self.send_response(True) self.send_response(True)
@IModule.route("dynamips.ethsw.start_capture")
def ethsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHSW_START_CAPTURE_SCHEMA):
return
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(ethsw.hypervisor.working_dir, "captures", capture_file_name)
ethsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.ethsw.stop_capture")
def ethsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, ETHSW_STOP_CAPTURE_SCHEMA):
return
# get the Ethernet switch instance
ethsw = self.get_device_instance(request["id"], self._ethernet_switches)
if not ethsw:
return
port = request["port"]
try:
ethsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)

View File

@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from gns3server.modules import IModule from gns3server.modules import IModule
from ..nodes.frame_relay_switch import FrameRelaySwitch from ..nodes.frame_relay_switch import FrameRelaySwitch
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
@ -25,6 +26,8 @@ from ..schemas.frsw import FRSW_UPDATE_SCHEMA
from ..schemas.frsw import FRSW_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.frsw import FRSW_ALLOCATE_UDP_PORT_SCHEMA
from ..schemas.frsw import FRSW_ADD_NIO_SCHEMA from ..schemas.frsw import FRSW_ADD_NIO_SCHEMA
from ..schemas.frsw import FRSW_DELETE_NIO_SCHEMA from ..schemas.frsw import FRSW_DELETE_NIO_SCHEMA
from ..schemas.frsw import FRSW_START_CAPTURE_SCHEMA
from ..schemas.frsw import FRSW_STOP_CAPTURE_SCHEMA
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -37,7 +40,7 @@ class FRSW(object):
""" """
Creates a new Frame-Relay switch. Creates a new Frame-Relay switch.
Optional request parameters: Mandatory request parameters:
- name (switch name) - name (switch name)
Response parameters: Response parameters:
@ -48,13 +51,10 @@ class FRSW(object):
""" """
# validate the request # validate the request
if request and not self.validate_request(request, FRSW_CREATE_SCHEMA): if not self.validate_request(request, FRSW_CREATE_SCHEMA):
return return
name = None name = request["name"]
if request and "name" in request:
name = request["name"]
try: try:
if not self._hypervisor_manager: if not self._hypervisor_manager:
self.start_hypervisor_manager() self.start_hypervisor_manager()
@ -292,3 +292,83 @@ class FRSW(object):
return return
self.send_response(True) self.send_response(True)
@IModule.route("dynamips.frsw.start_capture")
def frsw_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port (port identifier)
- port_id (port identifier)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, FRSW_START_CAPTURE_SCHEMA):
return
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(frsw.hypervisor.working_dir, "captures", capture_file_name)
frsw.start_capture(port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.frsw.stop_capture")
def frsw_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, FRSW_STOP_CAPTURE_SCHEMA):
return
# get the Frame relay switch instance
frsw = self.get_device_instance(request["id"], self._frame_relay_switches)
if not frsw:
return
port = request["port"]
try:
frsw.stop_capture(port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)

View File

@ -56,6 +56,8 @@ from ..schemas.vm import VM_STOP_SCHEMA
from ..schemas.vm import VM_SUSPEND_SCHEMA from ..schemas.vm import VM_SUSPEND_SCHEMA
from ..schemas.vm import VM_RELOAD_SCHEMA from ..schemas.vm import VM_RELOAD_SCHEMA
from ..schemas.vm import VM_UPDATE_SCHEMA from ..schemas.vm import VM_UPDATE_SCHEMA
from ..schemas.vm import VM_START_CAPTURE_SCHEMA
from ..schemas.vm import VM_STOP_CAPTURE_SCHEMA
from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA from ..schemas.vm import VM_SAVE_CONFIG_SCHEMA
from ..schemas.vm import VM_IDLEPCS_SCHEMA from ..schemas.vm import VM_IDLEPCS_SCHEMA
from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA from ..schemas.vm import VM_ALLOCATE_UDP_PORT_SCHEMA
@ -105,12 +107,12 @@ class VM(object):
Creates a new VM (router). Creates a new VM (router).
Mandatory request parameters: Mandatory request parameters:
- name (vm name)
- platform (platform name e.g. c7200) - platform (platform name e.g. c7200)
- image (path to IOS image) - image (path to IOS image)
- ram (amount of RAM in MB) - ram (amount of RAM in MB)
Optional request parameters: Optional request parameters:
- name (vm name)
- console (console port number) - console (console port number)
- aux (auxiliary console port number) - aux (auxiliary console port number)
- mac_addr (MAC address) - mac_addr (MAC address)
@ -127,16 +129,13 @@ class VM(object):
if not self.validate_request(request, VM_CREATE_SCHEMA): if not self.validate_request(request, VM_CREATE_SCHEMA):
return return
name = None name = request["name"]
if "name" in request:
name = request["name"]
platform = request["platform"] platform = request["platform"]
image = request["image"] image = request["image"]
ram = request["ram"] ram = request["ram"]
hypervisor = None hypervisor = None
chassis = None chassis = request.get("chassis")
if "chassis" in request: router_id = request.get("router_id")
chassis = request["chassis"]
try: try:
@ -149,9 +148,9 @@ class VM(object):
hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram) hypervisor = self._hypervisor_manager.allocate_hypervisor_for_router(image, ram)
if chassis: if chassis:
router = PLATFORMS[platform](hypervisor, name, chassis=chassis) router = PLATFORMS[platform](hypervisor, name, router_id, chassis=chassis)
else: else:
router = PLATFORMS[platform](hypervisor, name) router = PLATFORMS[platform](hypervisor, name, router_id)
router.ram = ram router.ram = ram
router.image = image router.image = image
router.sparsemem = self._hypervisor_manager.sparse_memory_support router.sparsemem = self._hypervisor_manager.sparse_memory_support
@ -390,8 +389,8 @@ class VM(object):
response = {} response = {}
try: try:
startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "{}.cfg".format(router.name)) startup_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(router.id))
private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "{}-private.cfg".format(router.name)) private_config_path = os.path.join(router.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(router.id))
# a new startup-config has been pushed # a new startup-config has been pushed
if "startup_config_base64" in request: if "startup_config_base64" in request:
@ -479,10 +478,98 @@ class VM(object):
# Update the ghost IOS file in case the RAM size has changed # Update the ghost IOS file in case the RAM size has changed
if self._hypervisor_manager.ghost_ios_support: if self._hypervisor_manager.ghost_ios_support:
self.set_ghost_ios(router) try:
self.set_ghost_ios(router)
except DynamipsError as e:
self.send_custom_error(str(e))
return
self.send_response(response) self.send_response(response)
@IModule.route("dynamips.vm.start_capture")
def vm_start_capture(self, request):
"""
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- slot (slot number)
- port (port number)
- capture_file_name
Optional request parameters:
- data_link_type (PCAP DLT_* value)
Response parameters:
- port_id (port identifier)
- capture_file_path (path to the capture file)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_START_CAPTURE_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
capture_file_name = request["capture_file_name"]
data_link_type = request.get("data_link_type")
try:
capture_file_path = os.path.join(router.hypervisor.working_dir, "captures", capture_file_name)
router.start_capture(slot, port, capture_file_path, data_link_type)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"],
"capture_file_path": capture_file_path}
self.send_response(response)
@IModule.route("dynamips.vm.stop_capture")
def vm_stop_capture(self, request):
"""
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- port_id (port identifier)
- slot (slot number)
- port (port number)
Response parameters:
- port_id (port identifier)
:param request: JSON request
"""
# validate the request
if not self.validate_request(request, VM_STOP_CAPTURE_SCHEMA):
return
# get the router instance
router = self.get_device_instance(request["id"], self._routers)
if not router:
return
slot = request["slot"]
port = request["port"]
try:
router.stop_capture(slot, port)
except DynamipsError as e:
self.send_custom_error(str(e))
return
response = {"port_id": request["port_id"]}
self.send_response(response)
@IModule.route("dynamips.vm.save_config") @IModule.route("dynamips.vm.save_config")
def vm_save_config(self, request): def vm_save_config(self, request):
""" """

View File

@ -61,7 +61,7 @@ class DynamipsHypervisor(object):
self._udp_end_port_range = 20000 self._udp_end_port_range = 20000
self._nio_udp_auto_instances = {} self._nio_udp_auto_instances = {}
self._version = "N/A" self._version = "N/A"
self._timeout = 30 self._timeout = timeout
self._socket = None self._socket = None
self._uuid = None self._uuid = None
@ -80,9 +80,7 @@ class DynamipsHypervisor(object):
host = self._host host = self._host
try: try:
self._socket = socket.create_connection((host, self._socket = socket.create_connection((host, self._port), self._timeout)
self._port),
self._timeout)
except OSError as e: except OSError as e:
raise DynamipsError("Could not connect to server: {}".format(e)) raise DynamipsError("Could not connect to server: {}".format(e))
@ -477,7 +475,7 @@ class DynamipsHypervisor(object):
self.socket.sendall(command.encode('utf-8')) self.socket.sendall(command.encode('utf-8'))
except OSError as e: except OSError as e:
raise DynamipsError("Lost communication with {host}:{port} :{error}" raise DynamipsError("Lost communication with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e)) .format(host=self._host, port=self._port, error=e))
# Now retrieve the result # Now retrieve the result
data = [] data = []
@ -488,7 +486,7 @@ class DynamipsHypervisor(object):
buf += chunk.decode("utf-8") buf += chunk.decode("utf-8")
except OSError as e: except OSError as e:
raise DynamipsError("Communication timed out with {host}:{port} :{error}" raise DynamipsError("Communication timed out with {host}:{port} :{error}"
.format(host=self._host, port=self._port, error=e)) .format(host=self._host, port=self._port, error=e))
# If the buffer doesn't end in '\n' then we can't be done # If the buffer doesn't end in '\n' then we can't be done
try: try:

View File

@ -70,7 +70,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: id (integer) :returns: id (integer)
""" """
return(self._id) return self._id
@property @property
def started(self): def started(self):
@ -90,7 +90,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: path to Dynamips :returns: path to Dynamips
""" """
return(self._path) return self._path
@path.setter @path.setter
def path(self, path): def path(self, path):
@ -110,7 +110,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: port number (integer) :returns: port number (integer)
""" """
return(self._port) return self._port
@port.setter @port.setter
def port(self, port): def port(self, port):
@ -130,7 +130,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: host/address (string) :returns: host/address (string)
""" """
return(self._host) return self._host
@host.setter @host.setter
def host(self, host): def host(self, host):
@ -232,7 +232,7 @@ class Hypervisor(DynamipsHypervisor):
self._process.wait(1) self._process.wait(1)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self._process.kill() self._process.kill()
if self._process.poll() == None: if self._process.poll() is None:
log.warn("Dynamips process {} is still running".format(self._process.pid)) log.warn("Dynamips process {} is still running".format(self._process.pid))
if self._stdout_file and os.access(self._stdout_file, os.W_OK): if self._stdout_file and os.access(self._stdout_file, os.W_OK):
@ -251,7 +251,7 @@ class Hypervisor(DynamipsHypervisor):
output = "" output = ""
if self._stdout_file and os.access(self._stdout_file, os.R_OK): if self._stdout_file and os.access(self._stdout_file, os.R_OK):
try: try:
with open(self._stdout_file) as file: with open(self._stdout_file, errors="replace") as file:
output = file.read() output = file.read()
except OSError as e: except OSError as e:
log.warn("could not read {}: {}".format(self._stdout_file, e)) log.warn("could not read {}: {}".format(self._stdout_file, e))
@ -264,7 +264,7 @@ class Hypervisor(DynamipsHypervisor):
:returns: True or False :returns: True or False
""" """
if self._process and self._process.poll() == None: if self._process and self._process.poll() is None:
return True return True
return False return False

View File

@ -22,10 +22,10 @@ Manages Dynamips hypervisors (load-balancing etc.)
from .hypervisor import Hypervisor from .hypervisor import Hypervisor
from .dynamips_error import DynamipsError from .dynamips_error import DynamipsError
from ..attic import find_unused_port from ..attic import find_unused_port
from ..attic import wait_socket_is_ready
from pkg_resources import parse_version from pkg_resources import parse_version
import os import os
import socket
import time import time
import logging import logging
@ -39,10 +39,6 @@ class HypervisorManager(object):
:param path: path to the Dynamips executable :param path: path to the Dynamips executable
:param working_dir: path to a working directory :param working_dir: path to a working directory
:param host: host/address for hypervisors to listen to :param host: host/address for hypervisors to listen to
:param base_port: base TCP port for hypervisors
:param base_console: base TCP port for consoles
:param base_aux: base TCP port for auxiliary consoles
:param base_udp: base UDP port for UDP tunnels
""" """
def __init__(self, path, working_dir, host='127.0.0.1'): def __init__(self, path, working_dir, host='127.0.0.1'):
@ -504,35 +500,17 @@ class HypervisorManager(object):
else: else:
log.info("allocating an hypervisor per IOS image disabled") log.info("allocating an hypervisor per IOS image disabled")
def wait_for_hypervisor(self, host, port, timeout=10): def wait_for_hypervisor(self, host, port):
""" """
Waits for an hypervisor to be started (accepting a socket connection) Waits for an hypervisor to be started (accepting a socket connection)
:param host: host/address to connect to the hypervisor :param host: host/address to connect to the hypervisor
:param port: port to connect to the hypervisor :param port: port to connect to the hypervisor
:param timeout: timeout value (default is 10 seconds)
""" """
# connect to a local address by default
# if listening to all addresses (IPv4 or IPv6)
if host == "0.0.0.0":
host = "127.0.0.1"
elif host == "::":
host = "::1"
connection_success = False
begin = time.time() begin = time.time()
# try to connect for 10 seconds # wait for the socket for a maximum of 10 seconds.
while(time.time() - begin < 10.0): connection_success, last_exception = wait_socket_is_ready(host, port, wait=10.0)
time.sleep(0.01)
try:
with socket.create_connection((host, port), timeout):
pass
except OSError as e:
last_exception = e
continue
connection_success = True
break
if not connection_success: if not connection_success:
# FIXME: throw exception here # FIXME: throw exception here

View File

@ -57,6 +57,8 @@ class NIO(object):
Deletes this NIO. Deletes this NIO.
""" """
if self._input_filter or self._output_filter:
self.unbind_filter("both")
self._hypervisor.send("nio delete {}".format(self._name)) self._hypervisor.send("nio delete {}".format(self._name))
log.info("NIO {name} has been deleted".format(name=self._name)) log.info("NIO {name} has been deleted".format(name=self._name))
@ -134,7 +136,7 @@ class NIO(object):
def setup_filter(self, direction, options): def setup_filter(self, direction, options):
""" """
Setups a packet filter binded with this NIO. Setups a packet filter bound with this NIO.
Filter "freq_drop" has 1 argument "<frequency>". It will drop Filter "freq_drop" has 1 argument "<frequency>". It will drop
everything with a -1 frequency, drop every Nth packet with a everything with a -1 frequency, drop every Nth packet with a
@ -174,7 +176,7 @@ class NIO(object):
:returns: tuple (filter name, filter options) :returns: tuple (filter name, filter options)
""" """
return (self._input_filter, self._input_filter_options) return self._input_filter, self._input_filter_options
@property @property
def output_filter(self): def output_filter(self):
@ -184,7 +186,7 @@ class NIO(object):
:returns: tuple (filter name, filter options) :returns: tuple (filter name, filter options)
""" """
return (self._output_filter, self._output_filter_options) return self._output_filter, self._output_filter_options
def get_stats(self): def get_stats(self):
""" """

View File

@ -33,6 +33,7 @@ class ATMBridge(object):
def __init__(self, hypervisor, name): def __init__(self, hypervisor, name):
#FIXME: instance tracking
self._hypervisor = hypervisor self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("atm_bridge create {}".format(self._name)) self._hypervisor.send("atm_bridge create {}".format(self._name))

View File

@ -20,6 +20,7 @@ Interface for Dynamips virtual ATM switch module ("atmsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593 http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593
""" """
import os
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
import logging import logging
@ -34,26 +35,21 @@ class ATMSwitch(object):
:param name: name for this switch :param name: name for this switch
""" """
_allocated_names = [] _instances = []
_instance_count = 1
def __init__(self, hypervisor, name=None): def __init__(self, hypervisor, name):
# create an unique ID # find an instance identifier (0 < id <= 4096)
self._id = ATMSwitch._instance_count self._id = 0
ATMSwitch._instance_count += 1 for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen if self._id == 0:
if not name: raise DynamipsError("Maximum number of instances reached")
name_id = self._id
while True:
name = "ATM" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
self._allocated_names.append(name)
self._hypervisor = hypervisor self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("atmsw create {}".format(self._name)) self._hypervisor.send("atmsw create {}".format(self._name))
@ -68,11 +64,10 @@ class ATMSwitch(object):
@classmethod @classmethod
def reset(cls): def reset(cls):
""" """
Resets the instance count and the allocated names list. Resets the instance count and the allocated instances list.
""" """
cls._instance_count = 1 cls._instances.clear()
cls._allocated_names.clear()
@property @property
def id(self): def id(self):
@ -102,7 +97,6 @@ class ATMSwitch(object):
:param new_name: New name for this switch :param new_name: New name for this switch
""" """
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("atmsw rename {name} {new_name}".format(name=self._name, self._hypervisor.send("atmsw rename {name} {new_name}".format(name=self._name,
new_name=new_name)) new_name=new_name))
@ -111,9 +105,7 @@ class ATMSwitch(object):
id=self._id, id=self._id,
new_name=new_name)) new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property @property
def hypervisor(self): def hypervisor(self):
@ -164,7 +156,7 @@ class ATMSwitch(object):
log.info("ATM switch {name} [id={id}] has been deleted".format(name=self._name, log.info("ATM switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
self._hypervisor.devices.remove(self) self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name) self._instances.remove(self._id)
def has_port(self, port): def has_port(self, port):
""" """
@ -360,3 +352,54 @@ class ATMSwitch(object):
vpi2=vpi2, vpi2=vpi2,
vci2=vci2)) vci2=vci2))
del self._mapping[(port1, vpi1, vci1)] del self._mapping[(port1, vpi1, vci1)]
def start_capture(self, port, output_file, data_link_type="DLT_ATM_RFC1483"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("ATM switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("ATM switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))

View File

@ -29,12 +29,9 @@ class Bridge(object):
:param name: name for this bridge :param name: name for this bridge
""" """
_allocated_names = []
def __init__(self, hypervisor, name): def __init__(self, hypervisor, name):
self._hypervisor = hypervisor self._hypervisor = hypervisor
self._allocated_names.append(name)
self._name = '"' + name + '"' # put name into quotes to protect spaces self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("nio_bridge create {}".format(self._name)) self._hypervisor.send("nio_bridge create {}".format(self._name))
self._hypervisor.devices.append(self) self._hypervisor.devices.append(self)
@ -58,14 +55,11 @@ class Bridge(object):
:param new_name: New name for this bridge :param new_name: New name for this bridge
""" """
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("nio_bridge rename {name} {new_name}".format(name=self._name, self._hypervisor.send("nio_bridge rename {name} {new_name}".format(name=self._name,
new_name=new_name)) new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property @property
def hypervisor(self): def hypervisor(self):
@ -103,7 +97,6 @@ class Bridge(object):
self._hypervisor.send("nio_bridge delete {}".format(self._name)) self._hypervisor.send("nio_bridge delete {}".format(self._name))
self._hypervisor.devices.remove(self) self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name)
def add_nio(self, nio): def add_nio(self, nio):
""" """

View File

@ -34,13 +34,14 @@ class C1700(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router: :param chassis: chassis for this router:
1720, 1721, 1750, 1751 or 1760 (default = 1720). 1720, 1721, 1750, 1751 or 1760 (default = 1720).
1710 is not supported. 1710 is not supported.
""" """
def __init__(self, hypervisor, name=None, chassis="1720"): def __init__(self, hypervisor, name, router_id=None, chassis="1720"):
Router.__init__(self, hypervisor, name, platform="c1700") Router.__init__(self, hypervisor, name, router_id, platform="c1700")
# Set default values for this platform # Set default values for this platform
self._ram = 64 self._ram = 64

View File

@ -36,6 +36,7 @@ class C2600(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router: :param chassis: chassis for this router:
2610, 2611, 2620, 2621, 2610XM, 2611XM 2610, 2611, 2620, 2621, 2610XM, 2611XM
2620XM, 2621XM, 2650XM or 2651XM (default = 2610). 2620XM, 2621XM, 2650XM or 2651XM (default = 2610).
@ -54,8 +55,8 @@ class C2600(Router):
"2650XM": C2600_MB_1FE, "2650XM": C2600_MB_1FE,
"2651XM": C2600_MB_2FE} "2651XM": C2600_MB_2FE}
def __init__(self, hypervisor, name=None, chassis="2610"): def __init__(self, hypervisor, name, router_id=None, chassis="2610"):
Router.__init__(self, hypervisor, name, platform="c2600") Router.__init__(self, hypervisor, name, router_id, platform="c2600")
# Set default values for this platform # Set default values for this platform
self._ram = 64 self._ram = 64

View File

@ -33,10 +33,11 @@ class C2691(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
""" """
def __init__(self, hypervisor, name=None): def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, platform="c2691") Router.__init__(self, hypervisor, name, router_id, platform="c2691")
# Set default values for this platform # Set default values for this platform
self._ram = 128 self._ram = 128

View File

@ -33,12 +33,13 @@ class C3600(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
:param chassis: chassis for this router: :param chassis: chassis for this router:
3620, 3640 or 3660 (default = 3640). 3620, 3640 or 3660 (default = 3640).
""" """
def __init__(self, hypervisor, name=None, chassis="3640"): def __init__(self, hypervisor, name, router_id=None, chassis="3640"):
Router.__init__(self, hypervisor, name, platform="c3600") Router.__init__(self, hypervisor, name, router_id, platform="c3600")
# Set default values for this platform # Set default values for this platform
self._ram = 128 self._ram = 128

View File

@ -33,10 +33,11 @@ class C3725(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
""" """
def __init__(self, hypervisor, name=None): def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, platform="c3725") Router.__init__(self, hypervisor, name, router_id, platform="c3725")
# Set default values for this platform # Set default values for this platform
self._ram = 128 self._ram = 128

View File

@ -33,10 +33,11 @@ class C3745(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
""" """
def __init__(self, hypervisor, name=None): def __init__(self, hypervisor, name, router_id=None):
Router.__init__(self, hypervisor, name, platform="c3745") Router.__init__(self, hypervisor, name, router_id, platform="c3745")
# Set default values for this platform # Set default values for this platform
self._ram = 128 self._ram = 128

View File

@ -35,11 +35,12 @@ class C7200(Router):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
:param npe: default NPE :param npe: default NPE
""" """
def __init__(self, hypervisor, name=None, npe="npe-400"): def __init__(self, hypervisor, name, router_id=None, npe="npe-400"):
Router.__init__(self, hypervisor, name, platform="c7200") Router.__init__(self, hypervisor, name, router_id, platform="c7200")
# Set default values for this platform # Set default values for this platform
self._ram = 256 self._ram = 256

View File

@ -20,6 +20,7 @@ Interface for Dynamips virtual Ethernet switch module ("ethsw").
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558 http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558
""" """
import os
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
import logging import logging
@ -34,26 +35,21 @@ class EthernetSwitch(object):
:param name: name for this switch :param name: name for this switch
""" """
_allocated_names = [] _instances = []
_instance_count = 1
def __init__(self, hypervisor, name=None): def __init__(self, hypervisor, name):
# create an unique ID # find an instance identifier (0 < id <= 4096)
self._id = EthernetSwitch._instance_count self._id = 0
EthernetSwitch._instance_count += 1 for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen if self._id == 0:
if not name: raise DynamipsError("Maximum number of instances reached")
name_id = self._id
while True:
name = "SW" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
self._allocated_names.append(name)
self._hypervisor = hypervisor self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("ethsw create {}".format(self._name)) self._hypervisor.send("ethsw create {}".format(self._name))
@ -68,11 +64,10 @@ class EthernetSwitch(object):
@classmethod @classmethod
def reset(cls): def reset(cls):
""" """
Resets the instance count and the allocated names list. Resets the instance count and the allocated instances list.
""" """
cls._instance_count = 1 cls._instances.clear()
cls._allocated_names.clear()
@property @property
def id(self): def id(self):
@ -102,7 +97,6 @@ class EthernetSwitch(object):
:param new_name: New name for this switch :param new_name: New name for this switch
""" """
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("ethsw rename {name} {new_name}".format(name=self._name, self._hypervisor.send("ethsw rename {name} {new_name}".format(name=self._name,
new_name=new_name)) new_name=new_name))
@ -111,9 +105,7 @@ class EthernetSwitch(object):
id=self._id, id=self._id,
new_name=new_name)) new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property @property
def hypervisor(self): def hypervisor(self):
@ -164,7 +156,7 @@ class EthernetSwitch(object):
log.info("Ethernet switch {name} [id={id}] has been deleted".format(name=self._name, log.info("Ethernet switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
self._hypervisor.devices.remove(self) self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name) self._instances.remove(self._id)
def add_nio(self, nio, port): def add_nio(self, nio, port):
""" """
@ -296,3 +288,54 @@ class EthernetSwitch(object):
""" """
self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name)) self._hypervisor.send("ethsw clear_mac_addr_table {}".format(self._name))
def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Ethernet switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("Ethernet switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))

View File

@ -20,6 +20,7 @@ Interface for Dynamips virtual Frame-Relay switch module.
http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642 http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642
""" """
import os
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
import logging import logging
@ -34,26 +35,21 @@ class FrameRelaySwitch(object):
:param name: name for this switch :param name: name for this switch
""" """
_allocated_names = [] _instances = []
_instance_count = 1
def __init__(self, hypervisor, name=None): def __init__(self, hypervisor, name):
# create an unique ID # find an instance identifier (0 < id <= 4096)
self._id = FrameRelaySwitch._instance_count self._id = 0
FrameRelaySwitch._instance_count += 1 for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen if self._id == 0:
if not name: raise DynamipsError("Maximum number of instances reached")
name_id = self._id
while True:
name = "FR" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
self._allocated_names.append(name)
self._hypervisor = hypervisor self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces self._name = '"' + name + '"' # put name into quotes to protect spaces
self._hypervisor.send("frsw create {}".format(self._name)) self._hypervisor.send("frsw create {}".format(self._name))
@ -68,11 +64,10 @@ class FrameRelaySwitch(object):
@classmethod @classmethod
def reset(cls): def reset(cls):
""" """
Resets the instance count and the allocated names list. Resets the instance count and the allocated instances list.
""" """
cls._instance_count = 1 cls._instances.clear()
cls._allocated_names.clear()
@property @property
def id(self): def id(self):
@ -102,7 +97,6 @@ class FrameRelaySwitch(object):
:param new_name: New name for this switch :param new_name: New name for this switch
""" """
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("frsw rename {name} {new_name}".format(name=self._name, self._hypervisor.send("frsw rename {name} {new_name}".format(name=self._name,
new_name=new_name)) new_name=new_name))
@ -111,9 +105,7 @@ class FrameRelaySwitch(object):
id=self._id, id=self._id,
new_name=new_name)) new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property @property
def hypervisor(self): def hypervisor(self):
@ -164,7 +156,7 @@ class FrameRelaySwitch(object):
log.info("Frame Relay switch {name} [id={id}] has been deleted".format(name=self._name, log.info("Frame Relay switch {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
self._hypervisor.devices.remove(self) self._hypervisor.devices.remove(self)
self._allocated_names.remove(self.name) self._instances.remove(self._id)
def has_port(self, port): def has_port(self, port):
""" """
@ -282,3 +274,54 @@ class FrameRelaySwitch(object):
port2=port2, port2=port2,
dlci2=dlci2)) dlci2=dlci2))
del self._mapping[(port1, dlci1)] del self._mapping[(port1, dlci1)]
def start_capture(self, port, output_file, data_link_type="DLT_FRELAY"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Frame relay switch {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._nios:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._nios[port]
nio.unbind_filter("both")
log.info("Frame relay switch {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))

View File

@ -19,6 +19,7 @@
Hub object that uses the Bridge interface to create a hub with ports. Hub object that uses the Bridge interface to create a hub with ports.
""" """
import os
from .bridge import Bridge from .bridge import Bridge
from ..dynamips_error import DynamipsError from ..dynamips_error import DynamipsError
@ -34,23 +35,20 @@ class Hub(Bridge):
:param name: name for this hub :param name: name for this hub
""" """
_instance_count = 1 _instances = []
def __init__(self, hypervisor, name): def __init__(self, hypervisor, name):
# create an unique ID # find an instance identifier (0 < id <= 4096)
self._id = Hub._instance_count self._id = 0
Hub._instance_count += 1 for identifier in range(1, 4097):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
# let's create a unique name if none has been chosen if self._id == 0:
if not name: raise DynamipsError("Maximum number of instances reached")
name_id = self._id
while True:
name = "Hub" + str(name_id)
# check if the name has already been allocated to another switch
if name not in self._allocated_names:
break
name_id += 1
self._mapping = {} self._mapping = {}
Bridge.__init__(self, hypervisor, name) Bridge.__init__(self, hypervisor, name)
@ -61,11 +59,10 @@ class Hub(Bridge):
@classmethod @classmethod
def reset(cls): def reset(cls):
""" """
Resets the instance count and the allocated names list. Resets the instance count and the allocated instances list.
""" """
cls._instance_count = 1 cls._instances.clear()
cls._allocated_names.clear()
@property @property
def id(self): def id(self):
@ -95,6 +92,7 @@ class Hub(Bridge):
Bridge.delete(self) Bridge.delete(self)
log.info("Ethernet hub {name} [id={id}] has been deleted".format(name=self._name, log.info("Ethernet hub {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
self._instances.remove(self._id)
def add_nio(self, nio, port): def add_nio(self, nio, port):
""" """
@ -137,3 +135,54 @@ class Hub(Bridge):
del self._mapping[port] del self._mapping[port]
return nio return nio
def start_capture(self, port, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param port: allocated port
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
if port not in self._mapping:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._mapping[port]
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {} has already a filter applied".format(port))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("Ethernet hub {name} [id={id}]: starting packet capture on {port}".format(name=self._name,
id=self._id,
port=port))
def stop_capture(self, port):
"""
Stops a packet capture.
:param port: allocated port
"""
if port not in self._mapping:
raise DynamipsError("Port {} is not allocated".format(port))
nio = self._mapping[port]
nio.unbind_filter("both")
log.info("Ethernet hub {name} [id={id}]: stopping packet capture on {port}".format(name=self._name,
id=self._id,
port=port))

View File

@ -37,41 +37,45 @@ class Router(object):
:param hypervisor: Dynamips hypervisor instance :param hypervisor: Dynamips hypervisor instance
:param name: name for this router :param name: name for this router
:param router_id: router instance ID
:param platform: c7200, c3745, c3725, c3600, c2691, c2600 or c1700 :param platform: c7200, c3745, c3725, c3600, c2691, c2600 or c1700
:param ghost_flag: used when creating a ghost IOS. :param ghost_flag: used when creating a ghost IOS.
""" """
_allocated_names = [] _instances = []
_allocated_console_ports = [] _allocated_console_ports = []
_allocated_aux_ports = [] _allocated_aux_ports = []
_instance_count = 1
_status = {0: "inactive", _status = {0: "inactive",
1: "shutting down", 1: "shutting down",
2: "running", 2: "running",
3: "suspended"} 3: "suspended"}
def __init__(self, hypervisor, name=None, platform="c7200", ghost_flag=False): def __init__(self, hypervisor, name, router_id=None, platform="c7200", ghost_flag=False):
if not ghost_flag: if not ghost_flag:
# create an unique ID
self._id = Router._instance_count
Router._instance_count += 1
# let's create a unique name if none has been chosen if not router_id:
if not name: # find an instance identifier if none is provided (0 < id <= 4096)
name_id = self._id self._id = 0
while True: for identifier in range(1, 4097):
name = "R" + str(name_id) if identifier not in self._instances:
# check if the name has already been allocated to another router self._id = identifier
if name not in self._allocated_names: self._instances.append(self._id)
break break
name_id += 1
if self._id == 0:
raise DynamipsError("Maximum number of instances reached")
else:
if router_id in self._instances:
raise DynamipsError("Router identifier {} is already used by another router".format(router_id))
self._id = router_id
self._instances.append(self._id)
else: else:
log.info("creating a new ghost IOS file") log.info("creating a new ghost IOS file")
self._id = 0 self._id = 0
name = "Ghost" name = "Ghost"
self._allocated_names.append(name)
self._hypervisor = hypervisor self._hypervisor = hypervisor
self._name = '"' + name + '"' # put name into quotes to protect spaces self._name = '"' + name + '"' # put name into quotes to protect spaces
self._platform = platform self._platform = platform
@ -137,18 +141,17 @@ class Router(object):
# get the default base MAC address # get the default base MAC address
self._mac_addr = self._hypervisor.send("{platform} get_mac_addr {name}".format(platform=self._platform, self._mac_addr = self._hypervisor.send("{platform} get_mac_addr {name}".format(platform=self._platform,
name=self._name))[0] name=self._name))[0]
self._hypervisor.devices.append(self) self._hypervisor.devices.append(self)
@classmethod @classmethod
def reset(cls): def reset(cls):
""" """
Resets the instance count and the allocated names list. Resets the instance count and the allocated instances list.
""" """
cls._instance_count = 1 cls._instances.clear()
cls._allocated_names.clear()
cls._allocated_console_ports.clear() cls._allocated_console_ports.clear()
cls._allocated_aux_ports.clear() cls._allocated_aux_ports.clear()
@ -222,10 +225,32 @@ class Router(object):
:param new_name: new name string :param new_name: new name string
""" """
if new_name in self._allocated_names: if self._startup_config:
raise DynamipsError('Name "{}" is already used by another router'.format(new_name)) # change the hostname in the startup-config
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_startup-config.cfg".format(self.id))
if os.path.isfile(startup_config_path):
try:
with open(startup_config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(startup_config_path, e))
if self._private_config:
# change the hostname in the private-config
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "i{}_private-config.cfg".format(self.id))
if os.path.isfile(private_config_path):
try:
with open(private_config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self.name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise DynamipsError("Could not amend the configuration {}: {}".format(private_config_path, e))
new_name_no_quotes = new_name
new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces new_name = '"' + new_name + '"' # put the new name into quotes to protect spaces
self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name, self._hypervisor.send("vm rename {name} {new_name}".format(name=self._name,
new_name=new_name)) new_name=new_name))
@ -233,10 +258,7 @@ class Router(object):
log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name, log.info("router {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id, id=self._id,
new_name=new_name)) new_name=new_name))
self._allocated_names.remove(self.name)
self._name = new_name self._name = new_name
self._allocated_names.append(new_name_no_quotes)
@property @property
def platform(self): def platform(self):
@ -284,9 +306,9 @@ class Router(object):
self._hypervisor.send("vm delete {}".format(self._name)) self._hypervisor.send("vm delete {}".format(self._name))
self._hypervisor.devices.remove(self) self._hypervisor.devices.remove(self)
log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id)) log.info("router {name} [id={id}] has been deleted".format(name=self._name, id=self._id))
self._allocated_names.remove(self.name) if self._id in self._instances:
self._instances.remove(self._id)
if self.console: if self.console:
self._allocated_console_ports.remove(self.console) self._allocated_console_ports.remove(self.console)
if self.aux: if self.aux:
@ -300,8 +322,21 @@ class Router(object):
self._hypervisor.send("vm clean_delete {}".format(self._name)) self._hypervisor.send("vm clean_delete {}".format(self._name))
self._hypervisor.devices.remove(self) self._hypervisor.devices.remove(self)
if self._startup_config:
# delete the startup-config
startup_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}.cfg".format(self.name))
if os.path.isfile(startup_config_path):
os.remove(startup_config_path)
if self._private_config:
# delete the private-config
private_config_path = os.path.join(self.hypervisor.working_dir, "configs", "{}-private.cfg".format(self.name))
if os.path.isfile(private_config_path):
os.remove(private_config_path)
log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id)) log.info("router {name} [id={id}] has been deleted (including associated files)".format(name=self._name, id=self._id))
self._allocated_names.remove(self.name) if self._id in self._instances:
self._instances.remove(self._id)
if self.console: if self.console:
self._allocated_console_ports.remove(self.console) self._allocated_console_ports.remove(self.console)
if self.aux: if self.aux:
@ -313,9 +348,10 @@ class Router(object):
At least the IOS image must be set before starting it. At least the IOS image must be set before starting it.
""" """
if self.get_status() == "suspended": status = self.get_status()
if status == "suspended":
self.resume() self.resume()
else: elif status == "inactive":
if not os.path.isfile(self._image): if not os.path.isfile(self._image):
raise DynamipsError("IOS image '{}' is not accessible".format(self._image)) raise DynamipsError("IOS image '{}' is not accessible".format(self._image))
@ -329,7 +365,7 @@ class Router(object):
# IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1
if elf_header_start != b'\x7fELF\x01\x02\x01': if elf_header_start != b'\x7fELF\x01\x02\x01':
raise DynamipsError("'{}' is not a valid IOU image".format(self._image)) raise DynamipsError("'{}' is not a valid IOS image".format(self._image))
self._hypervisor.send("vm start {}".format(self._name)) self._hypervisor.send("vm start {}".format(self._name))
log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id)) log.info("router {name} [id={id}] has been started".format(name=self._name, id=self._id))
@ -340,8 +376,9 @@ class Router(object):
The settings are kept. The settings are kept.
""" """
self._hypervisor.send("vm stop {}".format(self._name)) if self.get_status() != "inactive":
log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id)) self._hypervisor.send("vm stop {}".format(self._name))
log.info("router {name} [id={id}] has been stopped".format(name=self._name, id=self._id))
def suspend(self): def suspend(self):
""" """
@ -535,10 +572,10 @@ class Router(object):
reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:] reply = self._hypervisor.send("vm extract_config {}".format(self._name))[0].rsplit(' ', 2)[-2:]
except IOError: except IOError:
#for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file. #for some reason Dynamips gets frozen when it does not find the magic number in the NVRAM file.
return (None, None) return None, None
startup_config = reply[0][1:-1] # get statup-config and remove single quotes startup_config = reply[0][1:-1] # get statup-config and remove single quotes
private_config = reply[1][1:-1] # get private-config and remove single quotes private_config = reply[1][1:-1] # get private-config and remove single quotes
return (startup_config, private_config) return startup_config, private_config
def push_config(self, startup_config, private_config='(keep)'): def push_config(self, startup_config, private_config='(keep)'):
""" """
@ -681,7 +718,7 @@ class Router(object):
else: else:
flag = 0 flag = 0
self._hypervisor.send("vm set_sparse_mem {name} {sparsemem}".format(name=self._name, self._hypervisor.send("vm set_sparse_mem {name} {sparsemem}".format(name=self._name,
sparsemem=flag)) sparsemem=flag))
if sparsemem: if sparsemem:
log.info("router {name} [id={id}]: sparse memory enabled".format(name=self._name, log.info("router {name} [id={id}]: sparse memory enabled".format(name=self._name,
@ -941,7 +978,7 @@ class Router(object):
translated by the JIT (they contain the native code translated by the JIT (they contain the native code
corresponding to MIPS code pages). corresponding to MIPS code pages).
:param excec_area: exec area value (integer) :param exec_area: exec area value (integer)
""" """
self._hypervisor.send("vm set_exec_area {name} {exec_area}".format(name=self._name, self._hypervisor.send("vm set_exec_area {name} {exec_area}".format(name=self._name,
@ -1222,7 +1259,7 @@ class Router(object):
:returns: slot bindings (adapter names) list :returns: slot bindings (adapter names) list
""" """
return (self._hypervisor.send("vm slot_bindings {}".format(self._name))) return self._hypervisor.send("vm slot_bindings {}".format(self._name))
def slot_add_binding(self, slot_id, adapter): def slot_add_binding(self, slot_id, adapter):
""" """
@ -1238,16 +1275,16 @@ class Router(object):
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id)) slot_id=slot_id))
if slot != None: if slot is not None:
current_adapter = slot current_adapter = slot
raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router {name}".format(name=self._name, raise DynamipsError("Slot {slot_id} is already occupied by adapter {adapter} on router {name}".format(name=self._name,
slot_id=slot_id, slot_id=slot_id,
adapter=current_adapter)) adapter=current_adapter))
# Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running # Only c7200, c3600 and c3745 (NM-4T only) support new adapter while running
if self.is_running() and not (self._platform == 'c7200' \ if self.is_running() and not (self._platform == 'c7200'
and not (self._platform == 'c3600' and self.chassis == '3660') \ and not (self._platform == 'c3600' and self.chassis == '3660')
and not (self._platform == 'c3745' and adapter == 'NM-4T')): and not (self._platform == 'c3745' and adapter == 'NM-4T')):
raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter, raise DynamipsError("Adapter {adapter} cannot be added while router {name} is running".format(adapter=adapter,
name=self._name)) name=self._name))
@ -1285,14 +1322,14 @@ class Router(object):
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name, raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id)) slot_id=slot_id))
if adapter == None: if adapter is None:
raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name, raise DynamipsError("No adapter in slot {slot_id} on router {name}".format(name=self._name,
slot_id=slot_id)) slot_id=slot_id))
# Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running # Only c7200, c3600 and c3745 (NM-4T only) support to remove adapter while running
if self.is_running() and not (self._platform == 'c7200' \ if self.is_running() and not (self._platform == 'c7200'
and not (self._platform == 'c3600' and self.chassis == '3660') \ and not (self._platform == 'c3600' and self.chassis == '3660')
and not (self._platform == 'c3745' and adapter == 'NM-4T')): and not (self._platform == 'c3745' and adapter == 'NM-4T')):
raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter, raise DynamipsError("Adapter {adapter} cannot be removed while router {name} is running".format(adapter=adapter,
name=self._name)) name=self._name))
@ -1378,8 +1415,8 @@ class Router(object):
# WIC1 = 16, WIC2 = 32 and WIC3 = 48 # WIC1 = 16, WIC2 = 32 and WIC3 = 48
internal_wic_slot_id = 16 * (wic_slot_id + 1) internal_wic_slot_id = 16 * (wic_slot_id + 1)
self._hypervisor.send("vm slot_remove_binding {name} {slot_id} {wic_slot_id}".format(name=self._name, self._hypervisor.send("vm slot_remove_binding {name} {slot_id} {wic_slot_id}".format(name=self._name,
slot_id=slot_id, slot_id=slot_id,
wic_slot_id=internal_wic_slot_id)) wic_slot_id=internal_wic_slot_id))
log.info("router {name} [id={id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name, log.info("router {name} [id={id}]: {wic} removed from WIC slot {wic_slot_id}".format(name=self._name,
id=self._id, id=self._id,
@ -1502,6 +1539,77 @@ class Router(object):
slot_id=slot_id, slot_id=slot_id,
port_id=port_id)) port_id=port_id))
def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"):
"""
Starts a packet capture.
:param slot_id: slot ID
:param port_id: port ID
:param output_file: PCAP destination file for the capture
:param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
data_link_type = data_link_type.lower()
if data_link_type.startswith("dlt_"):
data_link_type = data_link_type[4:]
nio = adapter.get_nio(port_id)
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
raise DynamipsError("Port {port_id} has already a filter applied on {adapter}".format(adapter=adapter,
port_id=port_id))
try:
os.makedirs(os.path.dirname(output_file))
except FileExistsError:
pass
except OSError as e:
raise DynamipsError("Could not create captures directory {}".format(e))
nio.bind_filter("both", "capture")
nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
log.info("router {name} [id={id}]: starting packet capture on port {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio_name=nio.name,
slot_id=slot_id,
port_id=port_id))
def stop_capture(self, slot_id, port_id):
"""
Stops a packet capture.
:param slot_id: slot ID
:param port_id: port ID
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise DynamipsError("Slot {slot_id} doesn't exist on router {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise DynamipsError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
nio.unbind_filter("both")
log.info("router {name} [id={id}]: stopping packet capture on port {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio_name=nio.name,
slot_id=slot_id,
port_id=port_id))
def _create_slots(self, numslots): def _create_slots(self, numslots):
""" """
Creates the appropriate number of slots for this router. Creates the appropriate number of slots for this router.

View File

@ -24,8 +24,10 @@ ATMSW_CREATE_SCHEMA = {
"description": "ATM switch name", "description": "ATM switch name",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
} },
} },
"additionalProperties": False,
"required": ["name"]
} }
ATMSW_DELETE_SCHEMA = { ATMSW_DELETE_SCHEMA = {
@ -38,6 +40,7 @@ ATMSW_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -56,6 +59,7 @@ ATMSW_UPDATE_SCHEMA = {
"minLength": 1, "minLength": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -73,6 +77,7 @@ ATMSW_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
@ -82,129 +87,129 @@ ATMSW_ADD_NIO_SCHEMA = {
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -237,6 +242,7 @@ ATMSW_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"additionalProperties": False,
"required": ["id", "port", "port_id", "mappings", "nio"], "required": ["id", "port", "port_id", "mappings", "nio"],
} }
@ -255,5 +261,62 @@ ATMSW_DELETE_NIO_SCHEMA = {
"minimum": 1, "minimum": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id", "port"] "required": ["id", "port"]
} }
ATMSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an ATM switch instance port",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ATMSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an ATM switch instance port",
"type": "object",
"properties": {
"id": {
"description": "ATM switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the ATM switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}

View File

@ -24,8 +24,10 @@ ETHHUB_CREATE_SCHEMA = {
"description": "Ethernet hub name", "description": "Ethernet hub name",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
} },
} },
"additionalProperties": False,
"required": ["name"]
} }
ETHHUB_DELETE_SCHEMA = { ETHHUB_DELETE_SCHEMA = {
@ -38,6 +40,7 @@ ETHHUB_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -56,6 +59,7 @@ ETHHUB_UPDATE_SCHEMA = {
"minLength": 1, "minLength": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -73,6 +77,7 @@ ETHHUB_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
@ -82,129 +87,129 @@ ETHHUB_ADD_NIO_SCHEMA = {
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -234,6 +239,7 @@ ETHHUB_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id", "port", "nio"] "required": ["id", "port_id", "port", "nio"]
} }
@ -252,5 +258,62 @@ ETHHUB_DELETE_NIO_SCHEMA = {
"minimum": 1, "minimum": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id", "port"] "required": ["id", "port"]
} }
ETHHUB_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an Ethernet hub instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ETHHUB_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an Ethernet hub instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet hub instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet hub instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}

View File

@ -24,8 +24,10 @@ ETHSW_CREATE_SCHEMA = {
"description": "Ethernet switch name", "description": "Ethernet switch name",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
} },
} },
"additionalProperties": False,
"required": ["name"]
} }
ETHSW_DELETE_SCHEMA = { ETHSW_DELETE_SCHEMA = {
@ -38,6 +40,7 @@ ETHSW_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -71,6 +74,7 @@ ETHSW_UPDATE_SCHEMA = {
# }, # },
# }, # },
}, },
#"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -88,6 +92,7 @@ ETHSW_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
@ -97,129 +102,129 @@ ETHSW_ADD_NIO_SCHEMA = {
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -258,12 +263,13 @@ ETHSW_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id", "port", "port_type", "vlan", "nio"], "required": ["id", "port_id", "port", "port_type", "vlan", "nio"],
"dependencies": { "dependencies": {
"port_type": ["vlan"], "port_type": ["vlan"],
"vlan": ["port_type"] "vlan": ["port_type"]
} }
} }
ETHSW_DELETE_NIO_SCHEMA = { ETHSW_DELETE_NIO_SCHEMA = {
@ -281,5 +287,62 @@ ETHSW_DELETE_NIO_SCHEMA = {
"minimum": 1, "minimum": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id", "port"] "required": ["id", "port"]
} }
ETHSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on an Ethernet switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
ETHSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on an Ethernet switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Ethernet switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Ethernet switch instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}

View File

@ -24,8 +24,10 @@ FRSW_CREATE_SCHEMA = {
"description": "Frame relay switch name", "description": "Frame relay switch name",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
} },
} },
"additionalProperties": False,
"required": ["name"]
} }
FRSW_DELETE_SCHEMA = { FRSW_DELETE_SCHEMA = {
@ -38,6 +40,7 @@ FRSW_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -56,6 +59,7 @@ FRSW_UPDATE_SCHEMA = {
"minLength": 1, "minLength": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -73,6 +77,7 @@ FRSW_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
@ -82,129 +87,129 @@ FRSW_ADD_NIO_SCHEMA = {
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -237,6 +242,7 @@ FRSW_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"additionalProperties": False,
"required": ["id", "port", "port_id", "mappings", "nio"], "required": ["id", "port", "port_id", "mappings", "nio"],
} }
@ -255,5 +261,62 @@ FRSW_DELETE_NIO_SCHEMA = {
"minimum": 1, "minimum": 1,
}, },
}, },
"additionalProperties": False,
"required": ["id", "port"] "required": ["id", "port"]
} }
FRSW_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a Frame relay switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port", "capture_file_name"]
}
FRSW_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a Frame relay switch instance port",
"type": "object",
"properties": {
"id": {
"description": "Frame relay switch instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the Frame relay instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "port"]
}

View File

@ -25,6 +25,10 @@ VM_CREATE_SCHEMA = {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
"router_id": {
"description": "VM/router instance ID",
"type": "integer"
},
"platform": { "platform": {
"description": "router platform", "description": "router platform",
"type": "string", "type": "string",
@ -35,7 +39,7 @@ VM_CREATE_SCHEMA = {
"description": "router chassis model", "description": "router chassis model",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"pattern": "^[0-9]{4}$" "pattern": "^[0-9]{4}(XM)?$"
}, },
"image": { "image": {
"description": "path to the IOS image file", "description": "path to the IOS image file",
@ -58,14 +62,15 @@ VM_CREATE_SCHEMA = {
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"mac_address": { "mac_addr": {
"description": "base MAC address", "description": "base MAC address",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$" "pattern": "^([0-9a-fA-F]{4}\\.){2}[0-9a-fA-F]{4}$"
} }
}, },
"required": ["platform", "image", "ram"] "additionalProperties": False,
"required": ["name", "platform", "image", "ram"]
} }
VM_DELETE_SCHEMA = { VM_DELETE_SCHEMA = {
@ -78,6 +83,7 @@ VM_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -91,6 +97,7 @@ VM_START_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -104,6 +111,7 @@ VM_STOP_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -117,6 +125,7 @@ VM_SUSPEND_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -130,10 +139,11 @@ VM_RELOAD_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
#TODO: platform specific properties? #TODO: improve platform specific properties (dependencies?)
VM_UPDATE_SCHEMA = { VM_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update a VM instance", "description": "Request validation to update a VM instance",
@ -237,7 +247,7 @@ VM_UPDATE_SCHEMA = {
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"mac_address": { "mac_addr": {
"description": "base MAC address", "description": "base MAC address",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
@ -326,10 +336,112 @@ VM_UPDATE_SCHEMA = {
"description": "private configuration base64 encoded", "description": "private configuration base64 encoded",
"type": "string" "type": "string"
}, },
# C7200 properties
"npe": {
"description": "NPE model",
"enum": ["npe-100",
"npe-150",
"npe-175",
"npe-200",
"npe-225",
"npe-300",
"npe-400",
"npe-g2"]
},
"midplane": {
"description": "Midplane model",
"enum": ["std", "vxr"]
},
"sensors": {
"description": "Temperature sensors",
"type": "array"
},
"power_supplies": {
"description": "Power supplies status",
"type": "array"
},
# I/O memory property for all platforms but C7200
"iomem": {
"description": "I/O memory percentage",
"type": "integer",
"minimum": 0,
"maximum": 100
},
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
VM_START_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start a packet capture on a VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
"capture_file_name": {
"description": "Capture file name",
"type": "string",
"minLength": 1,
},
"data_link_type": {
"description": "PCAP data link type",
"type": "string",
"minLength": 1,
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "capture_file_name"]
}
VM_STOP_CAPTURE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop a packet capture on a VM instance port",
"type": "object",
"properties": {
"id": {
"description": "VM instance ID",
"type": "integer"
},
"port_id": {
"description": "Unique port identifier for the VM instance",
"type": "integer"
},
"slot": {
"description": "Slot number",
"type": "integer",
"minimum": 0,
"maximum": 6
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
},
},
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port"]
}
VM_SAVE_CONFIG_SCHEMA = { VM_SAVE_CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to save the configs for VM instance", "description": "Request validation to save the configs for VM instance",
@ -340,6 +452,7 @@ VM_SAVE_CONFIG_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -357,6 +470,7 @@ VM_IDLEPCS_SCHEMA = {
"type": "boolean" "type": "boolean"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -374,6 +488,7 @@ VM_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
@ -383,129 +498,129 @@ VM_ADD_NIO_SCHEMA = {
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -542,6 +657,7 @@ VM_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "nio"] "required": ["id", "port_id", "slot", "port", "nio"]
} }
@ -567,5 +683,6 @@ VM_DELETE_NIO_SCHEMA = {
"maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48 "maximum": 49 # maximum is 16 for regular port numbers, WICs port numbers start at 16, 32 or 48
}, },
}, },
"additionalProperties": False,
"required": ["id", "slot", "port"] "required": ["id", "slot", "port"]
} }

View File

@ -20,23 +20,20 @@ IOU server module.
""" """
import os import os
import sys
import base64 import base64
import tempfile import tempfile
import fcntl
import struct
import socket import socket
import shutil import shutil
from gns3server.modules import IModule from gns3server.modules import IModule
from gns3server.config import Config from gns3server.config import Config
import gns3server.jsonrpc as jsonrpc
from .iou_device import IOUDevice from .iou_device import IOUDevice
from .iou_error import IOUError from .iou_error import IOUError
from .nios.nio_udp import NIO_UDP from .nios.nio_udp import NIO_UDP
from .nios.nio_tap import NIO_TAP from .nios.nio_tap import NIO_TAP
from .nios.nio_generic_ethernet import NIO_GenericEthernet from .nios.nio_generic_ethernet import NIO_GenericEthernet
from ..attic import find_unused_port from ..attic import find_unused_port
from ..attic import has_privileged_access
from .schemas import IOU_CREATE_SCHEMA from .schemas import IOU_CREATE_SCHEMA
from .schemas import IOU_DELETE_SCHEMA from .schemas import IOU_DELETE_SCHEMA
@ -68,18 +65,15 @@ class IOU(IModule):
iou_config = config.get_section_config(name.upper()) iou_config = config.get_section_config(name.upper())
self._iouyap = iou_config.get("iouyap") self._iouyap = iou_config.get("iouyap")
if not self._iouyap or not os.path.isfile(self._iouyap): if not self._iouyap or not os.path.isfile(self._iouyap):
iouyap_in_cwd = os.path.join(os.getcwd(), "iouyap") paths = [os.getcwd()] + os.environ["PATH"].split(":")
if os.path.isfile(iouyap_in_cwd): # look for iouyap in the current working directory and $PATH
self._iouyap = iouyap_in_cwd for path in paths:
else: try:
# look for iouyap if none is defined or accessible if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK):
for path in os.environ["PATH"].split(":"): self._iouyap = os.path.join(path, "iouyap")
try: break
if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK): except OSError:
self._iouyap = os.path.join(path, "iouyap") continue
break
except OSError:
continue
if not self._iouyap: if not self._iouyap:
log.warning("iouyap binary couldn't be found!") log.warning("iouyap binary couldn't be found!")
@ -206,6 +200,7 @@ class IOU(IModule):
- iourc (base64 encoded iourc file) - iourc (base64 encoded iourc file)
Optional request parameters: Optional request parameters:
- iouyap (path to iouyap)
- working_dir (path to a working directory) - working_dir (path to a working directory)
- project_name - project_name
- console_start_port_range - console_start_port_range
@ -216,12 +211,12 @@ class IOU(IModule):
:param request: JSON request :param request: JSON request
""" """
if request == None: if request is None:
self.send_param_error() self.send_param_error()
return return
if "iourc" in request: if "iourc" in request:
iourc_content = base64.decodestring(request["iourc"].encode("utf-8")).decode("utf-8") iourc_content = base64.decodebytes(request["iourc"].encode("utf-8")).decode("utf-8")
iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix
try: try:
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
@ -229,7 +224,7 @@ class IOU(IModule):
f.write(iourc_content) f.write(iourc_content)
self._iourc = f.name self._iourc = f.name
except OSError as e: except OSError as e:
raise IOUError("Could not save iourc file to {}: {}".format(f.name, e)) raise IOUError("Could not create the iourc file: {}".format(e))
if "iouyap" in request and request["iouyap"]: if "iouyap" in request and request["iouyap"]:
self._iouyap = request["iouyap"] self._iouyap = request["iouyap"]
@ -268,30 +263,6 @@ class IOU(IModule):
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))
def test_result(self, message, result="error"):
"""
"""
return {"result": result, "message": message}
@IModule.route("iou.test_settings")
def test_settings(self, request):
"""
"""
response = []
# test iourc
if self._iourc == "":
response.append(self.test_result("No iourc file has been added"))
elif not os.path.isfile(self._iourc):
response.append(self.test_result("iourc file {} is not accessible".format(self._iourc)))
else:
#TODO: check hostname + license inside the file
pass
self.send_response(response)
@IModule.route("iou.create") @IModule.route("iou.create")
def iou_create(self, request): def iou_create(self, request):
""" """
@ -302,6 +273,7 @@ class IOU(IModule):
Optional request parameters: Optional request parameters:
- name (IOU name) - name (IOU name)
- console (IOU console port)
Response parameters: Response parameters:
- id (IOU instance identifier) - id (IOU instance identifier)
@ -315,23 +287,18 @@ class IOU(IModule):
if not self.validate_request(request, IOU_CREATE_SCHEMA): if not self.validate_request(request, IOU_CREATE_SCHEMA):
return return
name = None name = request["name"]
if "name" in request:
name = request["name"]
iou_path = request["path"] iou_path = request["path"]
console = request.get("console")
iou_id = request.get("iou_id")
try: try:
try: iou_instance = IOUDevice(name,
os.makedirs(self._working_dir) iou_path,
except FileExistsError:
pass
except OSError as e:
raise IOUError("Could not create working directory {}".format(e))
iou_instance = IOUDevice(iou_path,
self._working_dir, self._working_dir,
self._host, self._host,
name, iou_id,
console,
self._console_start_port_range, self._console_start_port_range,
self._console_end_port_range) self._console_end_port_range)
@ -371,7 +338,7 @@ class IOU(IModule):
return return
try: try:
iou_instance.delete() iou_instance.clean_delete()
del self._iou_instances[request["id"]] del self._iou_instances[request["id"]]
except IOUError as e: except IOUError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
@ -389,7 +356,7 @@ class IOU(IModule):
Optional request parameters: Optional request parameters:
- any setting to update - any setting to update
- startup_config_base64 (startup-config base64 encoded) - initial_config_base64 (initial-config base64 encoded)
Response parameters: Response parameters:
- updated settings - updated settings
@ -406,42 +373,42 @@ class IOU(IModule):
if not iou_instance: if not iou_instance:
return return
response = {} config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg")
config_path = os.path.join(iou_instance.working_dir, "startup-config")
try: try:
if "startup_config_base64" in request: if "initial_config_base64" in request:
# a new startup-config has been pushed # a new initial-config has been pushed
config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") config = base64.decodebytes(request["initial_config_base64"].encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "") config = "!\n" + config.replace("\r", "")
config = config.replace('%h', iou_instance.name) config = config.replace('%h', iou_instance.name)
try: try:
with open(config_path, "w") as f: with open(config_path, "w") as f:
log.info("saving startup-config to {}".format(config_path)) log.info("saving initial-config to {}".format(config_path))
f.write(config) f.write(config)
except OSError as e: except OSError as e:
raise IOUError("Could not save the configuration {}: {}".format(config_path, e)) raise IOUError("Could not save the configuration {}: {}".format(config_path, e))
# update the request with the new local startup-config path # update the request with the new local initial-config path
request["startup_config"] = os.path.basename(config_path) request["initial_config"] = os.path.basename(config_path)
elif "startup_config" in request: elif "initial_config" in request:
if os.path.isfile(request["startup_config"]) and request["startup_config"] != config_path: if os.path.isfile(request["initial_config"]) and request["initial_config"] != config_path:
# this is a local file set in the GUI # this is a local file set in the GUI
try: try:
with open(request["startup_config"], "r") as f: with open(request["initial_config"], "r", errors="replace") as f:
config = f.read() config = f.read()
with open(config_path, "w") as f: with open(config_path, "w") as f:
config = "!\n" + config.replace("\r", "") config = "!\n" + config.replace("\r", "")
config = config.replace('%h', iou_instance.name) config = config.replace('%h', iou_instance.name)
f.write(config) f.write(config)
request["startup_config"] = os.path.basename(config_path) request["initial_config"] = os.path.basename(config_path)
except OSError as e: except OSError as e:
raise IOUError("Could not save the configuration from {} to {}: {}".format(request["startup_config"], config_path, e)) raise IOUError("Could not save the configuration from {} to {}: {}".format(request["initial_config"], config_path, e))
else: elif not os.path.isfile(config_path):
raise IOUError("Startup-config {} could not be found on this server".format(request["startup_config"])) raise IOUError("Startup-config {} could not be found on this server".format(request["initial_config"]))
except IOUError as e: except IOUError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
# update the IOU settings # update the IOU settings
response = {}
for name, value in request.items(): for name, value in request.items():
if hasattr(iou_instance, name) and getattr(iou_instance, name) != value: if hasattr(iou_instance, name) and getattr(iou_instance, name) != value:
try: try:
@ -477,7 +444,6 @@ class IOU(IModule):
return return
try: try:
log.debug("starting IOU with command: {}".format(iou_instance.command()))
iou_instance.iouyap = self._iouyap iou_instance.iouyap = self._iouyap
iou_instance.iourc = self._iourc iou_instance.iourc = self._iourc
iou_instance.start() iou_instance.start()
@ -581,40 +547,17 @@ class IOU(IModule):
ignore_ports=self._allocated_udp_ports) ignore_ports=self._allocated_udp_ports)
except Exception as e: except Exception as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port) self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance.name, log.info("{} [id={}] has allocated UDP port {} with host {}".format(iou_instance.name,
iou_instance.id, iou_instance.id,
port, port,
self._host)) self._host))
response = {"lport": port} response = {"lport": port,
response["port_id"] = request["port_id"] "port_id": request["port_id"]}
self.send_response(response) self.send_response(response)
def _check_for_privileged_access(self, device):
"""
Check if iouyap can access Ethernet and TAP devices.
:param device: device name
"""
# we are root, so iouyap should have privileged access too
if os.geteuid() == 0:
return
# test if iouyap has the CAP_NET_RAW capability
if "security.capability" in os.listxattr(self._iouyap):
try:
caps = os.getxattr(self._iouyap, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(self._iouyap, e))
return
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, device))
@IModule.route("iou.add_nio") @IModule.route("iou.add_nio")
def add_nio(self, request): def add_nio(self, request):
""" """
@ -658,14 +601,22 @@ class IOU(IModule):
lport = request["nio"]["lport"] lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"] rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"] rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise IOUError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport) nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap": elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"] tap_device = request["nio"]["tap_device"]
self._check_for_privileged_access(tap_device) if not has_privileged_access(self._iouyap):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, tap_device))
nio = NIO_TAP(tap_device) nio = NIO_TAP(tap_device)
elif request["nio"]["type"] == "nio_generic_ethernet": elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"] ethernet_device = request["nio"]["ethernet_device"]
self._check_for_privileged_access(ethernet_device) if not has_privileged_access(self._iouyap):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device))
nio = NIO_GenericEthernet(ethernet_device) nio = NIO_GenericEthernet(ethernet_device)
if not nio: if not nio:
raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
@ -726,7 +677,7 @@ class IOU(IModule):
:param request: JSON request :param request: JSON request
""" """
if request == None: if request is None:
self.send_param_error() self.send_param_error()
else: else:
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))

View File

@ -27,6 +27,8 @@ import subprocess
import argparse import argparse
import threading import threading
import configparser import configparser
import shutil
from .ioucon import start_ioucon from .ioucon import start_ioucon
from .iou_error import IOUError from .iou_error import IOUError
from .adapters.ethernet_adapter import EthernetAdapter from .adapters.ethernet_adapter import EthernetAdapter
@ -44,10 +46,12 @@ class IOUDevice(object):
""" """
IOU device implementation. IOU device implementation.
:param name: name of this IOU device
:param path: path to IOU executable :param path: path to IOU executable
:param working_dir: path to a working directory :param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections :param host: host/address to bind for console and UDP connections
:param name: name of this IOU device :param iou_id: IOU instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start :param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end :param console_end_port_range: TCP console port range end
""" """
@ -55,32 +59,38 @@ class IOUDevice(object):
_instances = [] _instances = []
_allocated_console_ports = [] _allocated_console_ports = []
def __init__(self, path, def __init__(self,
name,
path,
working_dir, working_dir,
host="127.0.0.1", host="127.0.0.1",
name=None, iou_id = None,
console=None,
console_start_port_range=4001, console_start_port_range=4001,
console_end_port_range=4512): console_end_port_range=4512):
# find an instance identifier (0 < id <= 512) if not iou_id:
self._id = 0 # find an instance identifier if none is provided (0 < id <= 512)
for identifier in range(1, 513): self._id = 0
if identifier not in self._instances: for identifier in range(1, 513):
self._id = identifier if identifier not in self._instances:
self._instances.append(self._id) self._id = identifier
break self._instances.append(self._id)
break
if self._id == 0: if self._id == 0:
raise IOUError("Maximum number of IOU instances reached") raise IOUError("Maximum number of IOU instances reached")
if name:
self._name = name
else: else:
self._name = "IOU{}".format(self._id) if iou_id in self._instances:
raise IOUError("IOU identifier {} is already used by another IOU device".format(iou_id))
self._id = iou_id
self._instances.append(self._id)
self._name = name
self._path = path self._path = path
self._iourc = "" self._iourc = ""
self._iouyap = "" self._iouyap = ""
self._console = None self._console = console
self._working_dir = None self._working_dir = None
self._command = [] self._command = []
self._process = None self._process = None
@ -100,22 +110,32 @@ class IOUDevice(object):
self._slots = self._ethernet_adapters + self._serial_adapters self._slots = self._ethernet_adapters + self._serial_adapters
self._use_default_iou_values = True # for RAM & NVRAM values self._use_default_iou_values = True # for RAM & NVRAM values
self._nvram = 128 # Kilobytes self._nvram = 128 # Kilobytes
self._startup_config = "" self._initial_config = ""
self._ram = 256 # Megabytes self._ram = 256 # Megabytes
self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes).
# update the working directory working_dir_path = os.path.join(working_dir, "iou", "device-{}".format(self._id))
self.working_dir = working_dir
# allocate a console port if iou_id and not os.path.isdir(working_dir_path):
try: raise IOUError("Working directory {} doesn't exist".format(working_dir_path))
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise IOUError(e)
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise IOUError(e)
if self._console in self._allocated_console_ports:
raise IOUError("Console port {} is already in used another IOU device".format(console))
self._allocated_console_ports.append(self._console) self._allocated_console_ports.append(self._console)
log.info("IOU device {name} [id={id}] has been created".format(name=self._name, log.info("IOU device {name} [id={id}] has been created".format(name=self._name,
id=self._id)) id=self._id))
@ -128,13 +148,14 @@ class IOUDevice(object):
iou_defaults = {"name": self._name, iou_defaults = {"name": self._name,
"path": self._path, "path": self._path,
"startup_config": self._startup_config, "intial_config": self._initial_config,
"use_default_iou_values": self._use_default_iou_values, "use_default_iou_values": self._use_default_iou_values,
"ram": self._ram, "ram": self._ram,
"nvram": self._nvram, "nvram": self._nvram,
"ethernet_adapters": len(self._ethernet_adapters), "ethernet_adapters": len(self._ethernet_adapters),
"serial_adapters": len(self._serial_adapters), "serial_adapters": len(self._serial_adapters),
"console": self._console} "console": self._console,
"l1_keepalives": self._l1_keepalives}
return iou_defaults return iou_defaults
@ -146,7 +167,7 @@ class IOUDevice(object):
:returns: id (integer) :returns: id (integer)
""" """
return(self._id) return self._id
@classmethod @classmethod
def reset(cls): def reset(cls):
@ -175,10 +196,35 @@ class IOUDevice(object):
:param new_name: name :param new_name: name
""" """
self._name = new_name if self._started:
raise IOUError("Cannot change the name to {} while the device is running".format(new_name))
new_working_dir = os.path.join(os.path.dirname(self._working_dir), new_name)
try:
shutil.move(self._working_dir, new_working_dir)
self._working_dir = new_working_dir
except OSError as e:
raise IOUError("Could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
if self._initial_config:
# update the initial-config
config_path = os.path.join(self._working_dir, "initial-config.cfg")
if os.path.isfile(config_path):
try:
with open(config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self._name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise IOUError("Could not amend the configuration {}: {}".format(config_path, e))
log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name, log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id, id=self._id,
new_name=new_name)) new_name=new_name))
self._name = new_name
@property @property
def path(self): def path(self):
@ -188,7 +234,7 @@ class IOUDevice(object):
:returns: path to IOU :returns: path to IOU
""" """
return(self._path) return self._path
@path.setter @path.setter
def path(self, path): def path(self, path):
@ -200,8 +246,8 @@ class IOUDevice(object):
self._path = path self._path = path
log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name, log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name,
id=self._id, id=self._id,
path=path)) path=path))
@property @property
def iourc(self): def iourc(self):
@ -211,14 +257,14 @@ class IOUDevice(object):
:returns: path to the iourc file :returns: path to the iourc file
""" """
return(self._iourc) return self._iourc
@iourc.setter @iourc.setter
def iourc(self, iourc): def iourc(self, iourc):
""" """
Sets the path to the iourc file. Sets the path to the iourc file.
:param path: path to the iourc file. :param iourc: path to the iourc file.
""" """
self._iourc = iourc self._iourc = iourc
@ -234,14 +280,14 @@ class IOUDevice(object):
:returns: path to iouyap :returns: path to iouyap
""" """
return(self._iouyap) return self._iouyap
@iouyap.setter @iouyap.setter
def iouyap(self, iouyap): def iouyap(self, iouyap):
""" """
Sets the path to iouyap. Sets the path to iouyap.
:param path: path to iouyap :param iouyap: path to iouyap
""" """
self._iouyap = iouyap self._iouyap = iouyap
@ -267,8 +313,6 @@ class IOUDevice(object):
:param working_dir: path to the working directory :param working_dir: path to the working directory
""" """
# create our own working directory
working_dir = os.path.join(working_dir, "iou", "device-{}".format(self._id))
try: try:
os.makedirs(working_dir) os.makedirs(working_dir)
except FileExistsError: except FileExistsError:
@ -278,8 +322,8 @@ class IOUDevice(object):
self._working_dir = working_dir self._working_dir = working_dir
log.info("IOU {name} [id={id}]: working directory changed to {wd}".format(name=self._name, log.info("IOU {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id, id=self._id,
wd=self._working_dir)) wd=self._working_dir))
@property @property
def console(self): def console(self):
@ -300,14 +344,14 @@ class IOUDevice(object):
""" """
if console in self._allocated_console_ports: if console in self._allocated_console_ports:
raise IOUError("Console port {} is already in used by another IOU device".format(console)) raise IOUError("Console port {} is already used by another IOU device".format(console))
self._allocated_console_ports.remove(self._console) self._allocated_console_ports.remove(self._console)
self._console = console self._console = console
self._allocated_console_ports.append(self._console) self._allocated_console_ports.append(self._console)
log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name, log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id, id=self._id,
port=console)) port=console))
def command(self): def command(self):
""" """
@ -324,7 +368,8 @@ class IOUDevice(object):
""" """
self.stop() self.stop()
self._instances.remove(self._id) if self._id in self._instances:
self._instances.remove(self._id)
if self.console: if self.console:
self._allocated_console_ports.remove(self.console) self._allocated_console_ports.remove(self.console)
@ -332,6 +377,29 @@ class IOUDevice(object):
log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name, log.info("IOU device {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
def clean_delete(self):
"""
Deletes this IOU device & all files (nvram, initial-config etc.)
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
try:
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete IOU device {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
return
log.info("IOU device {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
id=self._id))
@property @property
def started(self): def started(self):
""" """
@ -359,6 +427,7 @@ class IOUDevice(object):
for unit in adapter.ports.keys(): for unit in adapter.ports.keys():
nio = adapter.get_nio(unit) nio = adapter.get_nio(unit)
if nio: if nio:
connection = None
if isinstance(nio, NIO_UDP): if isinstance(nio, NIO_UDP):
# UDP tunnel # UDP tunnel
connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport,
@ -372,7 +441,8 @@ class IOUDevice(object):
# Ethernet interface # Ethernet interface
connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)} connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)}
config["{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)] = connection if connection:
config["{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id)] = connection
unit_id += 1 unit_id += 1
bay_id += 1 bay_id += 1
@ -523,25 +593,11 @@ class IOUDevice(object):
Stops the IOU process. 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
self._started = False
# stop console support # stop console support
if self._ioucon_thead: if self._ioucon_thead:
self._ioucon_thread_stop_event.set() self._ioucon_thread_stop_event.set()
if self._ioucon_thead.is_alive(): if self._ioucon_thead.is_alive():
self._ioucon_thead.join(timeout=0.10) self._ioucon_thead.join(timeout=3.0) # wait for the thread to free the console port
self._ioucon_thead = None self._ioucon_thead = None
# stop iouyap # stop iouyap
@ -552,11 +608,25 @@ class IOUDevice(object):
self._iouyap_process.wait(1) self._iouyap_process.wait(1)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self._iouyap_process.kill() self._iouyap_process.kill()
if self._iouyap_process.poll() == None: if self._iouyap_process.poll() is None:
log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid, log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid,
self._id)) self._id))
self._iouyap_process = None self._iouyap_process = None
# 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() is None:
log.warn("IOU instance {} PID={} is still running".format(self._id,
self._process.pid))
self._process = None
self._started = False
def read_iou_stdout(self): def read_iou_stdout(self):
""" """
Reads the standard output of the IOU process. Reads the standard output of the IOU process.
@ -566,7 +636,7 @@ class IOUDevice(object):
output = "" output = ""
if self._iou_stdout_file: if self._iou_stdout_file:
try: try:
with open(self._iou_stdout_file) as file: with open(self._iou_stdout_file, errors="replace") as file:
output = file.read() output = file.read()
except OSError as e: except OSError as e:
log.warn("could not read {}: {}".format(self._iou_stdout_file, e)) log.warn("could not read {}: {}".format(self._iou_stdout_file, e))
@ -581,7 +651,7 @@ class IOUDevice(object):
output = "" output = ""
if self._iouyap_stdout_file: if self._iouyap_stdout_file:
try: try:
with open(self._iouyap_stdout_file) as file: with open(self._iouyap_stdout_file, errors="replace") as file:
output = file.read() output = file.read()
except OSError as e: except OSError as e:
log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e)) log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e))
@ -594,7 +664,7 @@ class IOUDevice(object):
:returns: True or False :returns: True or False
""" """
if self._process and self._process.poll() == None: if self._process and self._process.poll() is None:
return True return True
return False return False
@ -605,7 +675,7 @@ class IOUDevice(object):
:returns: True or False :returns: True or False
""" """
if self._iouyap_process and self._iouyap_process.poll() == None: if self._iouyap_process and self._iouyap_process.poll() is None:
return True return True
return False return False
@ -671,6 +741,24 @@ class IOUDevice(object):
return nio return nio
def _enable_l1_keepalives(self, command):
"""
Enables L1 keepalive messages if supported.
:param command: command line
"""
env = os.environ.copy()
env["IOURC"] = self._iourc
try:
output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env)
if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")):
command.extend(["-l"])
else:
raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path)))
except OSError as e:
log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e))
def _build_command(self): def _build_command(self):
""" """
Command to start the IOU process. Command to start the IOU process.
@ -711,8 +799,10 @@ class IOUDevice(object):
command.extend(["-n", str(self._nvram)]) command.extend(["-n", str(self._nvram)])
command.extend(["-m", str(self._ram)]) command.extend(["-m", str(self._ram)])
command.extend(["-L"]) # disable local console, use remote console command.extend(["-L"]) # disable local console, use remote console
if self._startup_config: if self._initial_config:
command.extend(["-c", self._startup_config]) command.extend(["-c", self._initial_config])
if self._l1_keepalives:
self._enable_l1_keepalives(command)
command.extend([str(self._id)]) command.extend([str(self._id)])
return command return command
@ -740,6 +830,30 @@ class IOUDevice(object):
else: else:
log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id))
@property
def l1_keepalives(self):
"""
Returns either layer 1 keepalive messages option is enabled or disabled.
:returns: boolean
"""
return self._l1_keepalives
@l1_keepalives.setter
def l1_keepalives(self, state):
"""
Enables or disables layer 1 keepalive messages.
:param state: boolean
"""
self._l1_keepalives = state
if state:
log.info("IOU {name} [id={id}]: has activated layer 1 keepalive messages".format(name=self._name, id=self._id))
else:
log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id))
@property @property
def ram(self): def ram(self):
""" """
@ -796,27 +910,27 @@ class IOUDevice(object):
self._nvram = nvram self._nvram = nvram
@property @property
def startup_config(self): def initial_config(self):
""" """
Returns the startup-config for this IOU instance. Returns the initial-config for this IOU instance.
:returns: path to startup-config file :returns: path to initial-config file
""" """
return self._startup_config return self._initial_config
@startup_config.setter @initial_config.setter
def startup_config(self, startup_config): def initial_config(self, initial_config):
""" """
Sets the startup-config for this IOU instance. Sets the initial-config for this IOU instance.
:param startup_config: path to startup-config file :param initial_config: path to initial-config file
""" """
self._startup_config = startup_config self._initial_config = initial_config
log.info("IOU {name} [id={id}]: startup_config set to {config}".format(name=self._name, log.info("IOU {name} [id={id}]: initial_config set to {config}".format(name=self._name,
id=self._id, id=self._id,
config=self._startup_config)) config=self._initial_config))
@property @property
def ethernet_adapters(self): def ethernet_adapters(self):

View File

@ -56,32 +56,32 @@ EXIT_ABORT = 2
# Mostly from: # Mostly from:
# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py # https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
#--[ Telnet Commands ]--------------------------------------------------------- #--[ Telnet Commands ]---------------------------------------------------------
SE = 240 # End of subnegotiation parameters SE = 240 # End of sub-negotiation parameters
NOP = 241 # No operation NOP = 241 # No operation
DATMK = 242 # Data stream portion of a sync. DATMK = 242 # Data stream portion of a sync.
BREAK = 243 # NVT Character BRK BREAK = 243 # NVT Character BRK
IP = 244 # Interrupt Process IP = 244 # Interrupt Process
AO = 245 # Abort Output AO = 245 # Abort Output
AYT = 246 # Are you there AYT = 246 # Are you there
EC = 247 # Erase Character EC = 247 # Erase Character
EL = 248 # Erase Line EL = 248 # Erase Line
GA = 249 # The Go Ahead Signal GA = 249 # The Go Ahead Signal
SB = 250 # Sub-option to follow SB = 250 # Sub-option to follow
WILL = 251 # Will; request or confirm option begin WILL = 251 # Will; request or confirm option begin
WONT = 252 # Wont; deny option request WONT = 252 # Wont; deny option request
DO = 253 # Do = Request or confirm remote option DO = 253 # Do = Request or confirm remote option
DONT = 254 # Don't = Demand or confirm option halt DONT = 254 # Don't = Demand or confirm option halt
IAC = 255 # Interpret as Command IAC = 255 # Interpret as Command
SEND = 1 # Sub-process negotiation SEND command SEND = 1 # Sub-process negotiation SEND command
IS = 0 # Sub-process negotiation IS command IS = 0 # Sub-process negotiation IS command
#--[ Telnet Options ]---------------------------------------------------------- #--[ Telnet Options ]----------------------------------------------------------
BINARY = 0 # Transmit Binary BINARY = 0 # Transmit Binary
ECHO = 1 # Echo characters back to sender ECHO = 1 # Echo characters back to sender
RECON = 2 # Reconnection RECON = 2 # Reconnection
SGA = 3 # Suppress Go-Ahead SGA = 3 # Suppress Go-Ahead
TMARK = 6 # Timing Mark TMARK = 6 # Timing Mark
TTYPE = 24 # Terminal Type TTYPE = 24 # Terminal Type
NAWS = 31 # Negotiate About Window Size NAWS = 31 # Negotiate About Window Size
LINEMO = 34 # Line Mode LINEMO = 34 # Line Mode
@ -139,7 +139,10 @@ class FileLock:
def unlock(self): def unlock(self):
if self.fd: if self.fd:
# Deleting first prevents a race condition # Deleting first prevents a race condition
os.unlink(self.fd.name) try:
os.unlink(self.fd.name)
except FileNotFoundError as e:
log.debug("{}".format(e))
self.fd.close() self.fd.close()
def __enter__(self): def __enter__(self):
@ -296,9 +299,7 @@ class TelnetServer(Console):
buf.extend(self._read_block(1)) buf.extend(self._read_block(1))
iac_cmd.append(buf[iac_loc + 2]) iac_cmd.append(buf[iac_loc + 2])
# We do ECHO, SGA, and BINARY. Period. # We do ECHO, SGA, and BINARY. Period.
if (iac_cmd[1] == DO if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]:
and iac_cmd[2] not in [ECHO, SGA, BINARY]):
self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) self._write_cur(bytes([IAC, WONT, iac_cmd[2]]))
log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
else: else:
@ -323,7 +324,7 @@ class TelnetServer(Console):
fd.send(bytes([IAC, WILL, ECHO, fd.send(bytes([IAC, WILL, ECHO,
IAC, WILL, SGA, IAC, WILL, SGA,
IAC, WILL, BINARY, IAC, WILL, BINARY,
IAC, DO, BINARY])) IAC, DO, BINARY]))
if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: if args.telnet_limit and len(self.fd_dict) > args.telnet_limit:
fd.send(b'\r\nToo many connections\r\n') fd.send(b'\r\nToo many connections\r\n')
@ -334,7 +335,10 @@ class TelnetServer(Console):
def _disconnect(self, fileno): def _disconnect(self, fileno):
fd = self.fd_dict.pop(fileno) fd = self.fd_dict.pop(fileno)
log.info("Telnet client disconnected") log.info("Telnet client disconnected")
fd.shutdown(socket.SHUT_RDWR) try:
fd.shutdown(socket.SHUT_RDWR)
except OSError as e:
log.warn("shutdown: {}".format(e))
fd.close() fd.close()
def __enter__(self): def __enter__(self):
@ -375,7 +379,10 @@ class IOU(Router):
return buf return buf
def write(self, buf): def write(self, buf):
self.fd.send(buf) try:
self.fd.send(buf)
except BlockingIOError:
return
def _open(self): def _open(self):
self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
@ -387,14 +394,12 @@ class IOU(Router):
except FileNotFoundError: except FileNotFoundError:
pass pass
except Exception as e: except Exception as e:
raise NetioError("Couldn't unlink socket {}: {}" raise NetioError("Couldn't unlink socket {}: {}".format(self.ttyC, e))
.format(self.ttyC, e))
try: try:
self.fd.bind(self.ttyC) self.fd.bind(self.ttyC)
except Exception as e: except Exception as e:
raise NetioError("Couldn't create socket {}: {}" raise NetioError("Couldn't create socket {}: {}".format(self.ttyC, e))
.format(self.ttyC, e))
def _connect(self): def _connect(self):
# Keep trying until we connect or die trying # Keep trying until we connect or die trying
@ -405,8 +410,7 @@ class IOU(Router):
log.debug("Waiting to connect to {}".format(self.ttyS)) log.debug("Waiting to connect to {}".format(self.ttyS))
time.sleep(RETRY_DELAY) time.sleep(RETRY_DELAY)
except Exception as e: except Exception as e:
raise NetioError("Couldn't connect to socket {}: {}" raise NetioError("Couldn't connect to socket {}: {}".format(self.ttyS, e))
.format(self.ttyS, e))
else: else:
break break
@ -459,8 +463,7 @@ def mkdir_netio(netio_dir):
except FileExistsError: except FileExistsError:
pass pass
except Exception as e: except Exception as e:
raise NetioError("Couldn't create directory {}: {}" raise NetioError("Couldn't create directory {}: {}".format(netio_dir, e))
.format(netio_dir, e))
def send_recv_loop(console, router, esc_char, stop_event): def send_recv_loop(console, router, esc_char, stop_event):
@ -599,7 +602,7 @@ def start_ioucon(cmdline_args, stop_event):
nport = int(port) nport = int(port)
except ValueError: except ValueError:
pass pass
if (addr == '' or nport == 0): if addr == '' or nport == 0:
raise ConfigError('format for --telnet-server must be ' raise ConfigError('format for --telnet-server must be '
'ADDR:PORT (like 127.0.0.1:20000)') 'ADDR:PORT (like 127.0.0.1:20000)')
@ -625,7 +628,7 @@ def start_ioucon(cmdline_args, stop_event):
if args.debug: if args.debug:
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
else: else:
print(e, file=sys.stderr) log.error("ioucon: {}".format(e))
sys.exit(EXIT_FAILURE) sys.exit(EXIT_FAILURE)
log.info("exiting...") log.info("exiting...")

View File

@ -26,13 +26,24 @@ IOU_CREATE_SCHEMA = {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
"iou_id": {
"description": "IOU device instance ID",
"type": "integer"
},
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"path": { "path": {
"description": "path to the IOU executable", "description": "path to the IOU executable",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
} }
}, },
"required": ["path"] "additionalProperties": False,
"required": ["name", "path"],
} }
IOU_DELETE_SCHEMA = { IOU_DELETE_SCHEMA = {
@ -45,6 +56,7 @@ IOU_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -67,8 +79,8 @@ IOU_UPDATE_SCHEMA = {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
"startup_config": { "initial_config": {
"description": "path to the IOU startup configuration file", "description": "path to the IOU initial configuration file",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
@ -102,11 +114,16 @@ IOU_UPDATE_SCHEMA = {
"description": "use the default IOU RAM & NVRAM values", "description": "use the default IOU RAM & NVRAM values",
"type": "boolean" "type": "boolean"
}, },
"startup_config_base64": { "l1_keepalives": {
"description": "startup configuration base64 encoded", "description": "enable or disable layer 1 keepalive messages",
"type": "boolean"
},
"initial_config_base64": {
"description": "initial configuration base64 encoded",
"type": "string" "type": "string"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -120,6 +137,7 @@ IOU_START_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -133,6 +151,7 @@ IOU_STOP_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -146,6 +165,7 @@ IOU_RELOAD_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
@ -163,6 +183,7 @@ IOU_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
@ -172,129 +193,129 @@ IOU_ADD_NIO_SCHEMA = {
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -331,6 +352,7 @@ IOU_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id", "slot", "port", "nio"] "required": ["id", "port_id", "slot", "port", "nio"]
} }
@ -357,5 +379,6 @@ IOU_DELETE_NIO_SCHEMA = {
"maximum": 3 "maximum": 3
}, },
}, },
"additionalProperties": False,
"required": ["id", "slot", "port"] "required": ["id", "slot", "port"]
} }

View File

@ -20,16 +20,12 @@ VPCS server module.
""" """
import os import os
import sys
import base64 import base64
import tempfile
import struct
import socket import socket
import shutil import shutil
from gns3server.modules import IModule from gns3server.modules import IModule
from gns3server.config import Config from gns3server.config import Config
import gns3server.jsonrpc as jsonrpc
from .vpcs_device import VPCSDevice from .vpcs_device import VPCSDevice
from .vpcs_error import VPCSError from .vpcs_error import VPCSError
from .nios.nio_udp import NIO_UDP from .nios.nio_udp import NIO_UDP
@ -66,18 +62,15 @@ class VPCS(IModule):
vpcs_config = config.get_section_config(name.upper()) vpcs_config = config.get_section_config(name.upper())
self._vpcs = vpcs_config.get("vpcs") self._vpcs = vpcs_config.get("vpcs")
if not self._vpcs or not os.path.isfile(self._vpcs): if not self._vpcs or not os.path.isfile(self._vpcs):
vpcs_in_cwd = os.path.join(os.getcwd(), "vpcs") paths = [os.getcwd()] + os.environ["PATH"].split(":")
if os.path.isfile(vpcs_in_cwd): # look for VPCS in the current working directory and $PATH
self._vpcs = vpcs_in_cwd for path in paths:
else: try:
# look for vpcs if none is defined or accessible if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK):
for path in os.environ["PATH"].split(":"): self._vpcs = os.path.join(path, "vpcs")
try: break
if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK): except OSError:
self._vpcs = os.path.join(path, "vpcs") continue
break
except OSError:
continue
if not self._vpcs: if not self._vpcs:
log.warning("VPCS binary couldn't be found!") log.warning("VPCS binary couldn't be found!")
@ -87,22 +80,16 @@ class VPCS(IModule):
# a new process start when calling IModule # a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs) IModule.__init__(self, name, *args, **kwargs)
self._vpcs_instances = {} self._vpcs_instances = {}
self._console_start_port_range = 4001 self._console_start_port_range = 4512
self._console_end_port_range = 4512 self._console_end_port_range = 5000
self._allocated_console_ports = [] self._allocated_udp_ports = []
self._current_console_port = self._console_start_port_range self._udp_start_port_range = 40001
self._udp_start_port_range = 30001 self._udp_end_port_range = 40512
self._udp_end_port_range = 40001
self._current_udp_port = self._udp_start_port_range
self._host = kwargs["host"] self._host = kwargs["host"]
self._projects_dir = kwargs["projects_dir"] self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"] self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir self._working_dir = self._projects_dir
# check every 5 seconds
#self._vpcs_callback = self.add_periodic_callback(self._check_vpcs_is_alive, 5000)
#self._vpcs_callback.start()
def stop(self, signum=None): def stop(self, signum=None):
""" """
Properly stops the module. Properly stops the module.
@ -110,7 +97,6 @@ class VPCS(IModule):
:param signum: signal number (if called by the signal handler) :param signum: signal number (if called by the signal handler)
""" """
# self._vpcs_callback.stop()
# delete all VPCS instances # delete all VPCS instances
for vpcs_id in self._vpcs_instances: for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id] vpcs_instance = self._vpcs_instances[vpcs_id]
@ -118,27 +104,6 @@ class VPCS(IModule):
IModule.stop(self, signum) # this will stop the I/O loop IModule.stop(self, signum) # this will stop the I/O loop
def _check_vpcs_is_alive(self):
"""
Periodic callback to check if VPCS is alive
for each VPCS instance.
Sends a notification to the client if not.
"""
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
if vpcs_instance.started and (not vpcs_instance.is_running() or not vpcs_instance.is_vpcs_running()):
notification = {"module": self.name,
"id": vpcs_id,
"name": vpcs_instance.name}
if not vpcs_instance.is_running():
stdout = vpcs_instance.read_vpcs_stdout()
notification["message"] = "VPCS has stopped running"
notification["details"] = stdout
self.send_notification("{}.vpcs_stopped".format(self.name), notification)
vpcs_instance.stop()
def get_vpcs_instance(self, vpcs_id): def get_vpcs_instance(self, vpcs_id):
""" """
Returns a VPCS device instance. Returns a VPCS device instance.
@ -171,9 +136,7 @@ class VPCS(IModule):
VPCSDevice.reset() VPCSDevice.reset()
self._vpcs_instances.clear() self._vpcs_instances.clear()
self._remote_server = False self._allocated_udp_ports.clear()
self._current_console_port = self._console_start_port_range
self._current_udp_port = self._udp_start_port_range
log.info("VPCS module has been reset") log.info("VPCS module has been reset")
@ -183,6 +146,7 @@ class VPCS(IModule):
Set or update settings. Set or update settings.
Optional request parameters: Optional request parameters:
- path (path to vpcs)
- working_dir (path to a working directory) - working_dir (path to a working directory)
- project_name - project_name
- console_start_port_range - console_start_port_range
@ -193,19 +157,22 @@ class VPCS(IModule):
:param request: JSON request :param request: JSON request
""" """
if request == None: if request is None:
self.send_param_error() self.send_param_error()
return return
if "vpcs" in request and request["vpcs"]: if "path" in request and request["path"]:
self._vpcs = request["vpcs"] self._vpcs = request["path"]
log.info("VPCS path set to {}".format(self._vpcs)) log.info("VPCS path set to {}".format(self._vpcs))
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.path = self._vpcs
if "working_dir" in request: if "working_dir" in request:
new_working_dir = request["working_dir"] new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir)) log.info("this server is local with working directory path to {}".format(new_working_dir))
else: else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"] + ".gns3") new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir)) log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir: if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir): if not os.path.isdir(new_working_dir):
@ -234,31 +201,16 @@ class VPCS(IModule):
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))
def test_result(self, message, result="error"):
"""
"""
return {"result": result, "message": message}
@IModule.route("vpcs.test_settings")
def test_settings(self, request):
"""
"""
response = []
self.send_response(response)
@IModule.route("vpcs.create") @IModule.route("vpcs.create")
def vpcs_create(self, request): def vpcs_create(self, request):
""" """
Creates a new VPCS instance. Creates a new VPCS instance.
Mandatory request parameters: Mandatory request parameters:
- path (path to the VPCS executable) - name (VPCS name)
Optional request parameters: Optional request parameters:
- name (VPCS name) - console (VPCS console port)
Response parameters: Response parameters:
- id (VPCS instance identifier) - id (VPCS instance identifier)
@ -272,28 +224,24 @@ class VPCS(IModule):
if not self.validate_request(request, VPCS_CREATE_SCHEMA): if not self.validate_request(request, VPCS_CREATE_SCHEMA):
return return
name = None name = request["name"]
if "name" in request: console = request.get("console")
name = request["name"] vpcs_id = request.get("vpcs_id")
vpcs_path = request["path"]
try: try:
try:
os.makedirs(self._working_dir)
except FileExistsError:
pass
except OSError as e:
raise VPCSError("Could not create working directory {}".format(e))
vpcs_instance = VPCSDevice(vpcs_path, self._working_dir, host=self._host, name=name) if not self._vpcs:
# find a console port raise VPCSError("No path to a VPCS executable has been set")
if self._current_console_port > self._console_end_port_range:
self._current_console_port = self._console_start_port_range vpcs_instance = VPCSDevice(name,
try: self._vpcs,
vpcs_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host) self._working_dir,
except Exception as e: self._host,
raise VPCSError(e) vpcs_id,
self._current_console_port += 1 console,
self._console_start_port_range,
self._console_end_port_range)
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
@ -330,7 +278,7 @@ class VPCS(IModule):
return return
try: try:
vpcs_instance.delete() vpcs_instance.clean_delete()
del self._vpcs_instances[request["id"]] del self._vpcs_instances[request["id"]]
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
@ -348,7 +296,7 @@ class VPCS(IModule):
Optional request parameters: Optional request parameters:
- any setting to update - any setting to update
- script_file_base64 (script-file base64 encoded) - script_file_base64 (base64 encoded)
Response parameters: Response parameters:
- updated settings - updated settings
@ -365,28 +313,42 @@ class VPCS(IModule):
if not vpcs_instance: if not vpcs_instance:
return return
response = {} config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc")
try: try:
# a new script-file has been pushed
if "script_file_base64" in request: if "script_file_base64" in request:
config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8") # a new startup-config has been pushed
config = "!\n" + config.replace("\r", "") config = base64.decodebytes(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name) config = config.replace('%h', vpcs_instance.name)
config_path = os.path.join(vpcs_instance.working_dir, "script-file")
try: try:
with open(config_path, "w") as f: with open(config_path, "w") as f:
log.info("saving script-file to {}".format(config_path)) log.info("saving script file to {}".format(config_path))
f.write(config) f.write(config)
except OSError as e: except OSError as e:
raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) raise VPCSError("Could not save the configuration {}: {}".format(config_path, e))
# update the request with the new local script-file path # update the request with the new local startup-config path
request["script_file"] = os.path.basename(config_path) request["script_file"] = os.path.basename(config_path)
elif "script_file" in request:
if os.path.isfile(request["script_file"]) and request["script_file"] != config_path:
# this is a local file set in the GUI
try:
with open(request["script_file"], "r", errors="replace") as f:
config = f.read()
with open(config_path, "w") as f:
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name)
f.write(config)
request["script_file"] = os.path.basename(config_path)
except OSError as e:
raise VPCSError("Could not save the configuration from {} to {}: {}".format(request["script_file"], config_path, e))
elif not os.path.isfile(config_path):
raise VPCSError("Startup-config {} could not be found on this server".format(request["script_file"]))
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
# update the VPCS settings # update the VPCS settings
response = {}
for name, value in request.items(): for name, value in request.items():
if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value: if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value:
try: try:
@ -422,8 +384,6 @@ class VPCS(IModule):
return return
try: try:
log.debug("starting VPCS with command: {}".format(vpcs_instance.command()))
vpcs_instance.vpcs = self._vpcs
vpcs_instance.start() vpcs_instance.start()
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
@ -518,53 +478,25 @@ class VPCS(IModule):
return return
try: try:
port = find_unused_port(self._udp_start_port_range,
# find a UDP port self._udp_end_port_range,
if self._current_udp_port >= self._udp_end_port_range: host=self._host,
self._current_udp_port = self._udp_start_port_range socket_type="UDP",
try: ignore_ports=self._allocated_udp_ports)
port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") except Exception as e:
except Exception as e:
raise VPCSError(e)
self._current_udp_port += 1
log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name,
vpcs_instance.id,
port,
self._host))
response = {"lport": port}
except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
response["port_id"] = request["port_id"] self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name,
vpcs_instance.id,
port,
self._host))
response = {"lport": port,
"port_id": request["port_id"]}
self.send_response(response) self.send_response(response)
def _check_for_privileged_access(self, device):
"""
Check if VPCS can access Ethernet and TAP devices.
:param device: device name
"""
# we are root, so vpcs should have privileged access too
if os.geteuid() == 0:
return
# test if VPCS has the CAP_NET_RAW capability
if "security.capability" in os.listxattr(self._vpcs):
try:
caps = os.getxattr(self._vpcs, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(self._vpcs, e))
return
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, device))
@IModule.route("vpcs.add_nio") @IModule.route("vpcs.add_nio")
def add_nio(self, request): def add_nio(self, request):
""" """
@ -572,7 +504,6 @@ class VPCS(IModule):
Mandatory request parameters: Mandatory request parameters:
- id (VPCS instance identifier) - id (VPCS instance identifier)
- slot (slot number)
- port (port number) - port (port number)
- port_id (unique port identifier) - port_id (unique port identifier)
- nio (one of the following) - nio (one of the following)
@ -598,7 +529,6 @@ class VPCS(IModule):
if not vpcs_instance: if not vpcs_instance:
return return
slot = request["slot"]
port = request["port"] port = request["port"]
try: try:
nio = None nio = None
@ -606,10 +536,17 @@ class VPCS(IModule):
lport = request["nio"]["lport"] lport = request["nio"]["lport"]
rhost = request["nio"]["rhost"] rhost = request["nio"]["rhost"]
rport = request["nio"]["rport"] rport = request["nio"]["rport"]
try:
#TODO: handle IPv6
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect((rhost, rport))
except OSError as e:
raise VPCSError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
nio = NIO_UDP(lport, rhost, rport) nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap": elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"] tap_device = request["nio"]["tap_device"]
self._check_for_privileged_access(tap_device) if not self.has_privileged_access(self._vpcs):
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, tap_device))
nio = NIO_TAP(tap_device) nio = NIO_TAP(tap_device)
if not nio: if not nio:
raise VPCSError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) raise VPCSError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
@ -618,7 +555,7 @@ class VPCS(IModule):
return return
try: try:
vpcs_instance.slot_add_nio_binding(slot, port, nio) vpcs_instance.port_add_nio_binding(port, nio)
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
@ -632,7 +569,6 @@ class VPCS(IModule):
Mandatory request parameters: Mandatory request parameters:
- id (VPCS instance identifier) - id (VPCS instance identifier)
- slot (slot identifier)
- port (port identifier) - port (port identifier)
Response parameters: Response parameters:
@ -650,10 +586,11 @@ class VPCS(IModule):
if not vpcs_instance: if not vpcs_instance:
return return
slot = request["slot"]
port = request["port"] port = request["port"]
try: try:
vpcs_instance.slot_remove_nio_binding(slot, port) nio = vpcs_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
@ -668,7 +605,7 @@ class VPCS(IModule):
:param request: JSON request :param request: JSON request
""" """
if request == None: if request is None:
self.send_param_error() self.send_param_error()
else: else:
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))

View File

@ -26,18 +26,24 @@ VPCS_CREATE_SCHEMA = {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
"path": { "vpcs_id": {
"description": "path to the VPCS executable", "description": "VPCS device instance ID",
"type": "string", "type": "integer"
"minLength": 1, },
} "console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
}, },
"required": ["path"] "additionalProperties": False,
"required": ["name"]
} }
VPCS_DELETE_SCHEMA = { VPCS_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete an VPCS instance", "description": "Request validation to delete a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -45,12 +51,13 @@ VPCS_DELETE_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
VPCS_UPDATE_SCHEMA = { VPCS_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update an VPCS instance", "description": "Request validation to update a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -62,27 +69,29 @@ VPCS_UPDATE_SCHEMA = {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
"path": { "console": {
"description": "path to the VPCS executable", "description": "console TCP port",
"type": "string", "minimum": 1,
"minLength": 1, "maximum": 65535,
"type": "integer"
}, },
"script_file": { "script_file": {
"description": "path to the VPCS startup configuration file", "description": "Path to the VPCS script file file",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
}, },
"script_file_base64": { "script_file_base64": {
"description": "startup configuration base64 encoded", "description": "Script file base64 encoded",
"type": "string" "type": "string"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
VPCS_START_SCHEMA = { VPCS_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start an VPCS instance", "description": "Request validation to start a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -90,12 +99,13 @@ VPCS_START_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
VPCS_STOP_SCHEMA = { VPCS_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop an VPCS instance", "description": "Request validation to stop a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -103,12 +113,13 @@ VPCS_STOP_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
VPCS_RELOAD_SCHEMA = { VPCS_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload an VPCS instance", "description": "Request validation to reload a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -116,12 +127,13 @@ VPCS_RELOAD_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id"] "required": ["id"]
} }
VPCS_ALLOCATE_UDP_PORT_SCHEMA = { VPCS_ALLOCATE_UDP_PORT_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to allocate an UDP port for an VPCS instance", "description": "Request validation to allocate an UDP port for a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@ -133,138 +145,139 @@ VPCS_ALLOCATE_UDP_PORT_SCHEMA = {
"type": "integer" "type": "integer"
}, },
}, },
"additionalProperties": False,
"required": ["id", "port_id"] "required": ["id", "port_id"]
} }
VPCS_ADD_NIO_SCHEMA = { VPCS_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for an VPCS instance", "description": "Request validation to add a NIO for a VPCS instance",
"type": "object", "type": "object",
"definitions": { "definitions": {
"UDP": { "UDP": {
"description": "UDP Network Input/Output", "description": "UDP Network Input/Output",
"properties": { "properties": {
"type": { "type": {
"enum": ["nio_udp"] "enum": ["nio_udp"]
}, },
"lport": { "lport": {
"description": "Local port", "description": "Local port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"rhost": { "rhost": {
"description": "Remote host", "description": "Remote host",
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"rport": { "rport": {
"description": "Remote port", "description": "Remote port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
} }
},
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
},
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
}, },
"required": ["type", "lport", "rhost", "rport"],
"additionalProperties": False
}, },
"Ethernet": {
"description": "Generic Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_generic_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"LinuxEthernet": {
"description": "Linux Ethernet Network Input/Output",
"properties": {
"type": {
"enum": ["nio_linux_ethernet"]
},
"ethernet_device": {
"description": "Ethernet device name e.g. eth0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "ethernet_device"],
"additionalProperties": False
},
"TAP": {
"description": "TAP Network Input/Output",
"properties": {
"type": {
"enum": ["nio_tap"]
},
"tap_device": {
"description": "TAP device name e.g. tap0",
"type": "string",
"minLength": 1
},
},
"required": ["type", "tap_device"],
"additionalProperties": False
},
"UNIX": {
"description": "UNIX Network Input/Output",
"properties": {
"type": {
"enum": ["nio_unix"]
},
"local_file": {
"description": "path to the UNIX socket file (local)",
"type": "string",
"minLength": 1
},
"remote_file": {
"description": "path to the UNIX socket file (remote)",
"type": "string",
"minLength": 1
},
},
"required": ["type", "local_file", "remote_file"],
"additionalProperties": False
},
"VDE": {
"description": "VDE Network Input/Output",
"properties": {
"type": {
"enum": ["nio_vde"]
},
"control_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
"local_file": {
"description": "path to the VDE control file",
"type": "string",
"minLength": 1
},
},
"required": ["type", "control_file", "local_file"],
"additionalProperties": False
},
"NULL": {
"description": "NULL Network Input/Output",
"properties": {
"type": {
"enum": ["nio_null"]
},
},
"required": ["type"],
"additionalProperties": False
},
},
"properties": { "properties": {
"id": { "id": {
@ -275,6 +288,12 @@ VPCS_ADD_NIO_SCHEMA = {
"description": "Unique port identifier for the VPCS instance", "description": "Unique port identifier for the VPCS instance",
"type": "integer" "type": "integer"
}, },
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 0
},
"nio": { "nio": {
"type": "object", "type": "object",
"description": "Network Input/Output", "description": "Network Input/Output",
@ -289,18 +308,26 @@ VPCS_ADD_NIO_SCHEMA = {
] ]
}, },
}, },
"required": ["id", "port_id", "nio"] "additionalProperties": False,
"required": ["id", "port_id", "port", "nio"]
} }
VPCS_DELETE_NIO_SCHEMA = { VPCS_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for an VPCS instance", "description": "Request validation to delete a NIO for a VPCS instance",
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
"description": "VPCS device instance ID", "description": "VPCS device instance ID",
"type": "integer" "type": "integer"
}, },
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 0
},
}, },
"required": ["id"] "additionalProperties": False,
"required": ["id", "port"]
} }

View File

@ -21,13 +21,16 @@ order to run an VPCS instance.
""" """
import os import os
import subprocess
import sys import sys
import socket import subprocess
import signal
import shutil
from .vpcs_error import VPCSError from .vpcs_error import VPCSError
from .adapters.ethernet_adapter import EthernetAdapter from .adapters.ethernet_adapter import EthernetAdapter
from .nios.nio_udp import NIO_UDP from .nios.nio_udp import NIO_UDP
from .nios.nio_tap import NIO_TAP from .nios.nio_tap import NIO_TAP
from ..attic import find_unused_port
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -37,52 +40,90 @@ class VPCSDevice(object):
""" """
VPCS device implementation. VPCS device implementation.
:param name: name of this VPCS device
:param path: path to VPCS executable :param path: path to VPCS executable
:param working_dir: path to a working directory :param working_dir: path to a working directory
:param host: host/address to bind for console and UDP connections :param host: host/address to bind for console and UDP connections
:param name: name of this VPCS device :param vpcs_id: VPCS instance ID
:param console: TCP console port
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
""" """
_instances = [] _instances = []
_allocated_console_ports = []
def __init__(self, path, working_dir, host="127.0.0.1", name=None): def __init__(self,
name,
path,
working_dir,
host="127.0.0.1",
vpcs_id=None,
console=None,
console_start_port_range=4512,
console_end_port_range=5000):
# find an instance identifier (1 <= id <= 255)
# This 255 limit is due to a restriction on the number of possible
# mac addresses given in VPCS using the -m option
self._id = 0
for identifier in range(1, 256):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if self._id == 0: if not vpcs_id:
raise VPCSError("Maximum number of VPCS instances reached") # find an instance identifier is none is provided (1 <= id <= 255)
# This 255 limit is due to a restriction on the number of possible
# MAC addresses given in VPCS using the -m option
self._id = 0
for identifier in range(1, 256):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
break
if name: if self._id == 0:
self._name = name raise VPCSError("Maximum number of VPCS instances reached")
else: else:
self._name = "VPCS{}".format(self._id) if vpcs_id in self._instances:
raise VPCSError("VPCS identifier {} is already used by another VPCS device".format(vpcs_id))
self._id = vpcs_id
self._instances.append(self._id)
self._name = name
self._path = path self._path = path
self._console = None self._console = console
self._working_dir = None self._working_dir = None
self._host = host
self._command = [] self._command = []
self._process = None self._process = None
self._vpcs_stdout_file = "" self._vpcs_stdout_file = ""
self._host = "127.0.0.1" self._host = "127.0.0.1"
self._started = False self._started = False
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# VPCS settings # VPCS settings
self._script_file = "" self._script_file = ""
self._ethernet_adapters = [EthernetAdapter()] # one adapter = 1 interfaces self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
self._slots = self._ethernet_adapters
# update the working directory working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id))
self.working_dir = working_dir
if vpcs_id and not os.path.isdir(working_dir_path):
raise VPCSError("Working directory {} doesn't exist".format(working_dir_path))
# create the device own working directory
self.working_dir = working_dir_path
if not self._console:
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise VPCSError(e)
if self._console in self._allocated_console_ports:
raise VPCSError("Console port {} is already used by another VPCS device".format(console))
self._allocated_console_ports.append(self._console)
log.info("VPCS device {name} [id={id}] has been created".format(name=self._name, log.info("VPCS device {name} [id={id}] has been created".format(name=self._name,
id=self._id)) id=self._id))
def defaults(self): def defaults(self):
""" """
@ -92,7 +133,6 @@ class VPCSDevice(object):
""" """
vpcs_defaults = {"name": self._name, vpcs_defaults = {"name": self._name,
"path": self._path,
"script_file": self._script_file, "script_file": self._script_file,
"console": self._console} "console": self._console}
@ -106,7 +146,7 @@ class VPCSDevice(object):
:returns: id (integer) :returns: id (integer)
""" """
return(self._id) return self._id
@classmethod @classmethod
def reset(cls): def reset(cls):
@ -115,6 +155,7 @@ class VPCSDevice(object):
""" """
cls._instances.clear() cls._instances.clear()
cls._allocated_console_ports.clear()
@property @property
def name(self): def name(self):
@ -134,10 +175,35 @@ class VPCSDevice(object):
:param new_name: name :param new_name: name
""" """
self._name = new_name if self._started:
raise VPCSError("Cannot change the name to {} while the device is running".format(new_name))
new_working_dir = os.path.join(os.path.dirname(self._working_dir), new_name)
try:
shutil.move(self._working_dir, new_working_dir)
self._working_dir = new_working_dir
except OSError as e:
raise VPCSError("Could not move working directory from {} to {}: {}".format(self._working_dir,
new_working_dir,
e))
if self._script_file:
# update the startup.vpc
config_path = os.path.join(self._working_dir, "startup.vpc")
if os.path.isfile(config_path):
try:
with open(config_path, "r+", errors="replace") as f:
old_config = f.read()
new_config = old_config.replace(self._name, new_name)
f.seek(0)
f.write(new_config)
except OSError as e:
raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e))
log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name,
id=self._id, id=self._id,
new_name=new_name)) new_name=new_name))
self._name = new_name
@property @property
def path(self): def path(self):
@ -147,7 +213,7 @@ class VPCSDevice(object):
:returns: path to VPCS :returns: path to VPCS
""" """
return(self._path) return self._path
@path.setter @path.setter
def path(self, path): def path(self, path):
@ -159,8 +225,8 @@ class VPCSDevice(object):
self._path = path self._path = path
log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name, log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name,
id=self._id, id=self._id,
path=path)) path=path))
@property @property
def working_dir(self): def working_dir(self):
@ -180,8 +246,6 @@ class VPCSDevice(object):
:param working_dir: path to the working directory :param working_dir: path to the working directory
""" """
# create our own working directory
working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id))
try: try:
os.makedirs(working_dir) os.makedirs(working_dir)
except FileExistsError: except FileExistsError:
@ -191,8 +255,8 @@ class VPCSDevice(object):
self._working_dir = working_dir self._working_dir = working_dir
log.info("VPCS {name} [id={id}]: working directory changed to {wd}".format(name=self._name, log.info("VPCS {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
id=self._id, id=self._id,
wd=self._working_dir)) wd=self._working_dir))
@property @property
def console(self): def console(self):
@ -212,10 +276,15 @@ class VPCSDevice(object):
:param console: console port (integer) :param console: console port (integer)
""" """
if console in self._allocated_console_ports:
raise VPCSError("Console port {} is already used by another VPCS device".format(console))
self._allocated_console_ports.remove(self._console)
self._console = console self._console = console
self._allocated_console_ports.append(self._console)
log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name, log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id, id=self._id,
port=console)) port=console))
def command(self): def command(self):
""" """
@ -232,9 +301,37 @@ class VPCSDevice(object):
""" """
self.stop() self.stop()
self._instances.remove(self._id) if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name, log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name,
id=self._id)) id=self._id))
def clean_delete(self):
"""
Deletes this VPCS device & all files (configs, logs etc.)
"""
self.stop()
if self._id in self._instances:
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
try:
shutil.rmtree(self._working_dir)
except OSError as e:
log.error("could not delete VPCS device {name} [id={id}]: {error}".format(name=self._name,
id=self._id,
error=e))
return
log.info("VPCS device {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
id=self._id))
@property @property
def started(self): def started(self):
@ -253,22 +350,32 @@ class VPCSDevice(object):
if not self.is_running(): if not self.is_running():
if not self._path:
raise VPCSError("No path to a VPCS executable has been set")
if not os.path.isfile(self._path): if not os.path.isfile(self._path):
raise VPCSError("VPCS image '{}' is not accessible".format(self._path)) raise VPCSError("VPCS program '{}' is not accessible".format(self._path))
if not os.access(self._path, os.X_OK): if not os.access(self._path, os.X_OK):
raise VPCSError("VPCS image '{}' is not executable".format(self._path)) raise VPCSError("VPCS program '{}' is not executable".format(self._path))
if not self._ethernet_adapter.get_nio(0):
raise VPCSError("This VPCS instance must be connected in order to start")
self._command = self._build_command() self._command = self._build_command()
try: try:
log.info("starting VPCS: {}".format(self._command)) log.info("starting VPCS: {}".format(self._command))
self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log") self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log")
log.info("logging to {}".format(self._vpcs_stdout_file)) log.info("logging to {}".format(self._vpcs_stdout_file))
flags = 0
if sys.platform.startswith("win32"):
flags = subprocess.CREATE_NEW_PROCESS_GROUP
with open(self._vpcs_stdout_file, "w") as fd: with open(self._vpcs_stdout_file, "w") as fd:
self._process = subprocess.Popen(self._command, self._process = subprocess.Popen(self._command,
stdout=fd, stdout=fd,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
cwd=self._working_dir) cwd=self._working_dir,
creationflags=flags)
log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
self._started = True self._started = True
except OSError as e: except OSError as e:
@ -284,14 +391,13 @@ class VPCSDevice(object):
# stop the VPCS process # stop the VPCS process
if self.is_running(): if self.is_running():
log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid))
try: if sys.platform.startswith("win32"):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._process.send_signal(signal.CTRL_BREAK_EVENT)
sock.connect((self._host, self._console)) else:
sock.send(bytes("quit\n", 'UTF-8')) self._process.terminate()
sock.close()
except TypeError as e: self._process.wait()
log.warn("VPCS instance {} PID={} is still running. Error: {}".format(self._id,
self._process.pid, e))
self._process = None self._process = None
self._started = False self._started = False
@ -304,7 +410,7 @@ class VPCSDevice(object):
output = "" output = ""
if self._vpcs_stdout_file: if self._vpcs_stdout_file:
try: try:
with open(self._vpcs_stdout_file) as file: with open(self._vpcs_stdout_file, errors="replace") as file:
output = file.read() output = file.read()
except OSError as e: except OSError as e:
log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e)) log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e))
@ -317,69 +423,48 @@ class VPCSDevice(object):
:returns: True or False :returns: True or False
""" """
if self._process: if self._process and self._process.poll() is None:
try: return True
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self._host, self._console))
sock.close()
return True
except:
e = sys.exc_info()[0]
log.warn("Could not connect to {}:{}. Error: {}".format(self._host, self._console, e))
return False
return False return False
def slot_add_nio_binding(self, slot_id, port_id, nio): def port_add_nio_binding(self, port_id, nio):
""" """
Adds a slot NIO binding. Adds a port NIO binding.
:param slot_id: slot ID
:param port_id: port ID :param port_id: port ID
:param nio: NIO instance to add to the slot/port :param nio: NIO instance to add to the slot/port
""" """
try: if not self._ethernet_adapter.port_exists(port_id):
adapter = self._slots[slot_id] raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
except IndexError: port_id=port_id))
raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id): self._ethernet_adapter.add_nio(port_id, nio)
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name,
port_id=port_id)) id=self._id,
nio=nio,
port_id=port_id))
adapter.add_nio(port_id, nio) def port_remove_nio_binding(self, port_id):
log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, """
Removes a port NIO binding.
:param port_id: port ID
:returns: NIO instance
"""
if not self._ethernet_adapter.port_exists(port_id):
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_id=port_id))
nio = self._ethernet_adapter.get_nio(port_id)
self._ethernet_adapter.remove_nio(port_id)
log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name,
id=self._id, id=self._id,
nio=nio, nio=nio,
slot_id=slot_id,
port_id=port_id)) port_id=port_id))
return nio
def slot_remove_nio_binding(self, slot_id, port_id):
"""
Removes a slot NIO binding.
:param slot_id: slot ID
:param port_id: port ID
"""
try:
adapter = self._slots[slot_id]
except IndexError:
raise VPCSError("Slot {slot_id} doesn't exist on VPCS {name}".format(name=self._name,
slot_id=slot_id))
if not adapter.port_exists(port_id):
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter,
port_id=port_id))
nio = adapter.get_nio(port_id)
adapter.remove_nio(port_id)
log.info("VPCS {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name,
id=self._id,
nio=nio,
slot_id=slot_id,
port_id=port_id))
def _build_command(self): def _build_command(self):
""" """
@ -392,12 +477,13 @@ class VPCSDevice(object):
-h print this help then exit -h print this help then exit
-v print version information then exit -v print version information then exit
-i num number of vpc instances to start (default is 9)
-p port run as a daemon listening on the tcp 'port' -p port run as a daemon listening on the tcp 'port'
-m num start byte of ether address, default from 0 -m num start byte of ether address, default from 0
-r file load and execute script file -r file load and execute script file
compatible with older versions, DEPRECATED. compatible with older versions, DEPRECATED.
-e tap mode, using /dev/tapx (linux only) -e tap mode, using /dev/tapx by default (linux only)
-u udp mode, default -u udp mode, default
udp mode options: udp mode options:
@ -405,33 +491,36 @@ class VPCSDevice(object):
-c port remote udp base port (dynamips udp port), default from 30000 -c port remote udp base port (dynamips udp port), default from 30000
-t ip remote host IP, default 127.0.0.1 -t ip remote host IP, default 127.0.0.1
tap mode options:
-d device device name, works only when -i is set to 1
hypervisor mode option: hypervisor mode option:
-H port run as the hypervisor listening on the tcp 'port' -H port run as the hypervisor listening on the tcp 'port'
If no 'scriptfile' specified, VPCS will read and execute the file named If no 'scriptfile' specified, vpcs will read and execute the file named
'startup.vpc' if it exsits in the current directory. 'startup.vpc' if it exsits in the current directory.
""" """
command = [self._path] command = [self._path]
command.extend(["-p", str(self._console)]) command.extend(["-p", str(self._console)]) # listen to console port
for adapter in self._slots: nio = self._ethernet_adapter.get_nio(0)
for unit in adapter.ports.keys(): if nio:
nio = adapter.get_nio(unit) if isinstance(nio, NIO_UDP):
if nio: # UDP tunnel
if isinstance(nio, NIO_UDP): command.extend(["-s", str(nio.lport)]) # source UDP port
# UDP tunnel command.extend(["-c", str(nio.rport)]) # destination UDP port
command.extend(["-s", str(nio.lport)]) command.extend(["-t", nio.rhost]) # destination host
command.extend(["-c", str(nio.rport)])
command.extend(["-t", str(nio.rhost)])
elif isinstance(nio, NIO_TAP): elif isinstance(nio, NIO_TAP):
# TAP interface # TAP interface
command.extend(["-e"]) #, str(nio.tap_device)]) #TODO: Fix, currently vpcs doesn't allow specific tap_device command.extend(["-e"])
command.extend(["-d", nio.tap_device])
command.extend(["-m", str(self._id)]) # The unique ID is used to set the mac address offset command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset
command.extend(["-i", str(1)]) # Option to start only one pc instance command.extend(["-i", "1"]) # option to start only one VPC instance
command.extend(["-F"]) # option to avoid the daemonization of VPCS
if self._script_file: if self._script_file:
command.extend([self._script_file]) command.extend([self._script_file])
return command return command
@ -441,7 +530,7 @@ class VPCSDevice(object):
""" """
Returns the script-file for this VPCS instance. Returns the script-file for this VPCS instance.
:returns: path to script-file file :returns: path to script-file
""" """
return self._script_file return self._script_file
@ -451,10 +540,10 @@ class VPCSDevice(object):
""" """
Sets the script-file for this VPCS instance. Sets the script-file for this VPCS instance.
:param script_file: path to script-file file :param script_file: path to base-script-file
""" """
self._script_file = script_file self._script_file = script_file
log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name,
id=self._id, id=self._id,
config=self._script_file)) config=self._script_file))

View File

@ -32,6 +32,7 @@ import socket
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
import tornado.autoreload import tornado.autoreload
import pkg_resources
from pkg_resources import parse_version from pkg_resources import parse_version
from .config import Config from .config import Config
@ -143,8 +144,12 @@ class Server(object):
router = self._create_zmq_router() router = self._create_zmq_router()
# Add our JSON-RPC Websocket handler to Tornado # Add our JSON-RPC Websocket handler to Tornado
self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))]) self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))])
if hasattr(sys, "frozen"):
templates_dir = "templates"
else:
templates_dir = pkg_resources.resource_filename("gns3server", "templates")
tornado_app = tornado.web.Application(self.handlers, tornado_app = tornado.web.Application(self.handlers,
template_path=os.path.join(os.path.dirname(__file__), "templates"), template_path=templates_dir,
debug=True) # FIXME: debug mode! debug=True) # FIXME: debug mode!
try: try:
@ -160,7 +165,7 @@ class Server(object):
except OSError as e: except OSError as e:
if e.errno == errno.EADDRINUSE: # socket already in use if e.errno == errno.EADDRINUSE: # socket already in use
logging.critical("socket in use for {}:{}".format(self._host, self._port)) logging.critical("socket in use for {}:{}".format(self._host, self._port))
self._cleanup() self._cleanup(graceful=False)
ioloop = tornado.ioloop.IOLoop.instance() ioloop = tornado.ioloop.IOLoop.instance()
self._stream = zmqstream.ZMQStream(router, ioloop) self._stream = zmqstream.ZMQStream(router, ioloop)
@ -201,7 +206,7 @@ class Server(object):
self._router.bind("ipc:///tmp/gns3.ipc") self._router.bind("ipc:///tmp/gns3.ipc")
except zmq.error.ZMQError as e: except zmq.error.ZMQError as e:
log.critical("Could not start ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e)) log.critical("Could not start ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e))
self._cleanup() self._cleanup(graceful=False)
raise SystemExit raise SystemExit
log.info("ZeroMQ server listening to ipc:///tmp/gns3.ipc") log.info("ZeroMQ server listening to ipc:///tmp/gns3.ipc")
else: else:
@ -209,7 +214,7 @@ class Server(object):
self._router.bind("tcp://127.0.0.1:{}".format(self._zmq_port)) self._router.bind("tcp://127.0.0.1:{}".format(self._zmq_port))
except zmq.error.ZMQError as e: except zmq.error.ZMQError as e:
log.critical("Could not start ZeroMQ server on 127.0.0.1:{}, reason: {}".format(self._zmq_port, e)) log.critical("Could not start ZeroMQ server on 127.0.0.1:{}, reason: {}".format(self._zmq_port, e))
self._cleanup() self._cleanup(graceful=False)
raise SystemExit raise SystemExit
log.info("ZeroMQ server listening to 127.0.0.1:{}".format(self._zmq_port)) log.info("ZeroMQ server listening to 127.0.0.1:{}".format(self._zmq_port))
return self._router return self._router
@ -251,25 +256,26 @@ class Server(object):
ioloop = tornado.ioloop.IOLoop.instance() ioloop = tornado.ioloop.IOLoop.instance()
ioloop.stop() ioloop.stop()
def _cleanup(self, signum=None): def _cleanup(self, signum=None, graceful=True):
""" """
Shutdowns any running module processes Shutdowns any running module processes
and adds a callback to stop the event loop & ZeroMQ and adds a callback to stop the event loop & ZeroMQ
:param signum: signal number (if called by a signal handler) :param signum: signal number (if called by a signal handler)
:param graceful: gracefully stop the modules
""" """
# terminate all modules # terminate all modules
for module in self._modules: for module in self._modules:
if module.is_alive(): if module.is_alive() and graceful:
log.info("stopping {}".format(module.name)) log.info("stopping {}".format(module.name))
self.stop_module(module.name) self.stop_module(module.name)
module.join(timeout=3) module.join(timeout=3)
if module.is_alive(): if module.is_alive():
# just kill the module if it is still alive. # just kill the module if it is still alive.
log.info("terminating {}".format(module.name)) log.info("terminating {}".format(module.name))
module.terminate() module.terminate()
module.join(timeout=1) module.join(timeout=1)
ioloop = tornado.ioloop.IOLoop.instance() ioloop = tornado.ioloop.IOLoop.instance()
if signum: if signum:

View File

@ -23,5 +23,5 @@
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
__version__ = "1.0a4.dev2" __version__ = "1.0a7.dev3"
__version_info__ = (1, 0, 0, -99) __version_info__ = (1, 0, 0, -99)

View File

@ -1,3 +1,4 @@
netifaces
tornado tornado
pyzmq pyzmq
netifaces-py3 netifaces-py3

View File

@ -46,14 +46,14 @@ setup(
long_description=open("README.rst", "r").read(), long_description=open("README.rst", "r").read(),
install_requires=[ install_requires=[
"tornado>=3.1", "tornado>=3.1",
"pyzmq>=13.1.0", # this is the strict minimum, recommended is >= 14.0.0 "pyzmq>=14.0.0",
"jsonschema==2.3.0" "jsonschema==2.3.0"
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"gns3server = gns3server.main:main", "gns3server = gns3server.main:main",
] ]
}, },
packages=find_packages(), packages=find_packages(),
package_data={"gns3server": ["templates/upload.html"]}, package_data={"gns3server": ["templates/upload.html"]},
include_package_data=True, include_package_data=True,
@ -71,5 +71,5 @@ setup(
"Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
], ],
) )

View File

@ -40,7 +40,7 @@ class JSONRPC(AsyncTestCase):
AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request)) AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request))
response = self.wait() response = self.wait()
json_response = json_decode(response) json_response = json_decode(response)
assert json_response["id"] == None assert json_response["id"] is None
assert json_response["error"].get("code") == -32600 assert json_response["error"].get("code") == -32600
def test_request_with_invalid_json(self): def test_request_with_invalid_json(self):
@ -49,7 +49,7 @@ class JSONRPC(AsyncTestCase):
AsyncWSRequest(self.URL, self.io_loop, self.stop, request) AsyncWSRequest(self.URL, self.io_loop, self.stop, request)
response = self.wait() response = self.wait()
json_response = json_decode(response) json_response = json_decode(response)
assert json_response["id"] == None assert json_response["id"] is None
assert json_response["error"].get("code") == -32700 assert json_response["error"].get("code") == -32700
def test_request_with_invalid_jsonrpc_field(self): def test_request_with_invalid_jsonrpc_field(self):
@ -58,7 +58,7 @@ class JSONRPC(AsyncTestCase):
AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request)) AsyncWSRequest(self.URL, self.io_loop, self.stop, json_encode(request))
response = self.wait() response = self.wait()
json_response = json_decode(response) json_response = json_decode(response)
assert json_response["id"] == None assert json_response["id"] is None
assert json_response["error"].get("code") == -32700 assert json_response["error"].get("code") == -32700
def test_request_with_no_params(self): def test_request_with_no_params(self):

View File

@ -34,7 +34,7 @@ class TestVersionHandler(AsyncHTTPTestCase):
self.http_client.fetch(self.get_url(self.URL), self.stop) self.http_client.fetch(self.get_url(self.URL), self.stop)
response = self.wait() response = self.wait()
assert(response.headers['Content-Type'].startswith('application/json')) assert response.headers['Content-Type'].startswith('application/json')
assert(response.body) assert response.body
body = json_decode(response.body) body = json_decode(response.body)
assert body['version'] == __version__ assert body['version'] == __version__