diff --git a/.gitignore b/.gitignore
index 49bac9bd..1355fd28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,6 @@ nosetests.xml
.project
.pydevproject
.settings
+
+#Gedit temp files
+*~
diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py
index 59304d19..c57ddf2b 100644
--- a/gns3server/modules/__init__.py
+++ b/gns3server/modules/__init__.py
@@ -18,8 +18,9 @@
import sys
from .base import IModule
from .dynamips import Dynamips
+from .vpcs import VPCS
-MODULES = [Dynamips]
+MODULES = [Dynamips, VPCS]
if sys.platform.startswith("linux"):
# IOU runs only on Linux
diff --git a/gns3server/modules/iou/schemas.py b/gns3server/modules/iou/schemas.py
index 62ac0ec4..62b35747 100644
--- a/gns3server/modules/iou/schemas.py
+++ b/gns3server/modules/iou/schemas.py
@@ -313,13 +313,6 @@ IOU_ADD_NIO_SCHEMA = {
"minimum": 0,
"maximum": 3
},
-
- "slot": {
- "description": "Slot number",
- "type": "integer",
- "minimum": 0,
- "maximum": 15
- },
"nio": {
"type": "object",
"description": "Network Input/Output",
diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py
new file mode 100644
index 00000000..251cb280
--- /dev/null
+++ b/gns3server/modules/vpcs/__init__.py
@@ -0,0 +1,690 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+VPCS server module.
+"""
+
+import os
+import sys
+import base64
+import tempfile
+import fcntl
+import struct
+import socket
+import shutil
+
+from gns3server.modules import IModule
+from gns3server.config import Config
+import gns3server.jsonrpc as jsonrpc
+from .vpcs_device import VPCSDevice
+from .vpcs_error import VPCSError
+from .nios.nio_udp import NIO_UDP
+from .nios.nio_tap import NIO_TAP
+from ..attic import find_unused_port
+
+from .schemas import VPCS_CREATE_SCHEMA
+from .schemas import VPCS_DELETE_SCHEMA
+from .schemas import VPCS_UPDATE_SCHEMA
+from .schemas import VPCS_START_SCHEMA
+from .schemas import VPCS_STOP_SCHEMA
+from .schemas import VPCS_RELOAD_SCHEMA
+from .schemas import VPCS_ALLOCATE_UDP_PORT_SCHEMA
+from .schemas import VPCS_ADD_NIO_SCHEMA
+from .schemas import VPCS_DELETE_NIO_SCHEMA
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class VPCS(IModule):
+ """
+ VPCS module.
+
+ :param name: module name
+ :param args: arguments for the module
+ :param kwargs: named arguments for the module
+ """
+
+ def __init__(self, name, *args, **kwargs):
+
+ # get the VPCS location
+ config = Config.instance()
+ VPCS_config = config.get_section_config(name.upper())
+ self._VPCS = VPCS_config.get("VPCS")
+ if not self._VPCS or not os.path.isfile(self._VPCS):
+ VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS")
+ if os.path.isfile(VPCS_in_cwd):
+ self._VPCS = VPCS_in_cwd
+ else:
+ # look for VPCS if none is defined or accessible
+ for path in os.environ["PATH"].split(":"):
+ try:
+ if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK):
+ self._VPCS = os.path.join(path, "VPCS")
+ break
+ except OSError:
+ continue
+
+ if not self._VPCS:
+ log.warning("VPCS binary couldn't be found!")
+ elif not os.access(self._VPCS, os.X_OK):
+ log.warning("VPCS is not executable")
+
+ # a new process start when calling IModule
+ IModule.__init__(self, name, *args, **kwargs)
+ self._VPCS_instances = {}
+ self._console_start_port_range = 4001
+ self._console_end_port_range = 4512
+ self._allocated_console_ports = []
+ self._current_console_port = self._console_start_port_range
+ self._udp_start_port_range = 30001
+ self._udp_end_port_range = 40001
+ self._current_udp_port = self._udp_start_port_range
+ self._host = kwargs["host"]
+ self._projects_dir = kwargs["projects_dir"]
+ self._tempdir = kwargs["temp_dir"]
+ self._working_dir = self._projects_dir
+ self._VPCSrc = ""
+
+ # check every 5 seconds
+ self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000)
+ self._VPCS_callback.start()
+
+ def stop(self, signum=None):
+ """
+ Properly stops the module.
+
+ :param signum: signal number (if called by the signal handler)
+ """
+
+ self._VPCS_callback.stop()
+ # delete all VPCS instances
+ for VPCS_id in self._VPCS_instances:
+ VPCS_instance = self._VPCS_instances[VPCS_id]
+ VPCS_instance.delete()
+
+ IModule.stop(self, signum) # this will stop the I/O loop
+
+ def _check_VPCS_is_alive(self):
+ """
+ Periodic callback to check if VPCS and VPCS are 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)
+ elif not VPCS_instance.is_VPCS_running():
+ stdout = VPCS_instance.read_VPCS_stdout()
+ notification["message"] = "VPCS has stopped running"
+ notification["details"] = stdout
+ self.send_notification("{}.VPCS_stopped".format(self.name), notification)
+ VPCS_instance.stop()
+
+ def get_VPCS_instance(self, VPCS_id):
+ """
+ Returns an VPCS device instance.
+
+ :param VPCS_id: VPCS device identifier
+
+ :returns: VPCSDevice instance
+ """
+
+ if VPCS_id not in self._VPCS_instances:
+ log.debug("VPCS device ID {} doesn't exist".format(VPCS_id), exc_info=1)
+ self.send_custom_error("VPCS device ID {} doesn't exist".format(VPCS_id))
+ return None
+ return self._VPCS_instances[VPCS_id]
+
+ @IModule.route("VPCS.reset")
+ def reset(self, request):
+ """
+ Resets the module.
+
+ :param request: JSON request
+ """
+
+ # delete all VPCS instances
+ for VPCS_id in self._VPCS_instances:
+ VPCS_instance = self._VPCS_instances[VPCS_id]
+ VPCS_instance.delete()
+
+ # resets the instance IDs
+ VPCSDevice.reset()
+
+ self._VPCS_instances.clear()
+ self._remote_server = False
+ self._current_console_port = self._console_start_port_range
+ self._current_udp_port = self._udp_start_port_range
+
+ if self._VPCSrc and os.path.isfile(self._VPCSrc):
+ try:
+ log.info("deleting VPCSrc file {}".format(self._VPCSrc))
+ os.remove(self._VPCSrc)
+ except OSError as e:
+ log.warn("could not delete VPCSrc file {}: {}".format(self._VPCSrc, e))
+
+ log.info("VPCS module has been reset")
+
+ @IModule.route("VPCS.settings")
+ def settings(self, request):
+ """
+ Set or update settings.
+
+ Optional request parameters:
+ - working_dir (path to a working directory)
+ - project_name
+ - console_start_port_range
+ - console_end_port_range
+ - udp_start_port_range
+ - udp_end_port_range
+
+ :param request: JSON request
+ """
+
+ if request == None:
+ self.send_param_error()
+ return
+
+ if "VPCS" in request and request["VPCS"]:
+ self._VPCS = request["VPCS"]
+ log.info("VPCS path set to {}".format(self._VPCS))
+
+ if "working_dir" in request:
+ new_working_dir = request["working_dir"]
+ log.info("this server is local with working directory path to {}".format(new_working_dir))
+ else:
+ new_working_dir = os.path.join(self._projects_dir, request["project_name"] + ".gns3")
+ log.info("this server is remote with working directory path to {}".format(new_working_dir))
+ if self._projects_dir != self._working_dir != new_working_dir:
+ if not os.path.isdir(new_working_dir):
+ try:
+ shutil.move(self._working_dir, new_working_dir)
+ except OSError as e:
+ log.error("could not move working directory from {} to {}: {}".format(self._working_dir,
+ new_working_dir,
+ e))
+ return
+
+ # update the working directory if it has changed
+ if self._working_dir != new_working_dir:
+ self._working_dir = new_working_dir
+ for VPCS_id in self._VPCS_instances:
+ VPCS_instance = self._VPCS_instances[VPCS_id]
+ VPCS_instance.working_dir = self._working_dir
+
+ if "console_start_port_range" in request and "console_end_port_range" in request:
+ self._console_start_port_range = request["console_start_port_range"]
+ self._console_end_port_range = request["console_end_port_range"]
+
+ if "udp_start_port_range" in request and "udp_end_port_range" in request:
+ self._udp_start_port_range = request["udp_start_port_range"]
+ self._udp_end_port_range = request["udp_end_port_range"]
+
+ log.debug("received request {}".format(request))
+
+ def test_result(self, message, result="error"):
+ """
+ """
+
+ return {"result": result, "message": message}
+
+ @IModule.route("VPCS.test_settings")
+ def test_settings(self, request):
+ """
+ """
+
+ response = []
+
+ self.send_response(response)
+
+ @IModule.route("VPCS.create")
+ def VPCS_create(self, request):
+ """
+ Creates a new VPCS instance.
+
+ Mandatory request parameters:
+ - path (path to the VPCS executable)
+
+ Optional request parameters:
+ - name (VPCS name)
+
+ Response parameters:
+ - id (VPCS instance identifier)
+ - name (VPCS name)
+ - default settings
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_CREATE_SCHEMA):
+ return
+
+ name = None
+ if "name" in request:
+ name = request["name"]
+ VPCS_path = request["path"]
+
+ try:
+ try:
+ os.makedirs(self._working_dir)
+ except FileExistsError:
+ pass
+ except OSError as e:
+ raise VPCSError("Could not create working directory {}".format(e))
+
+ VPCS_instance = VPCSDevice(VPCS_path, self._working_dir, host=self._host, name=name)
+ # find a console port
+ if self._current_console_port > self._console_end_port_range:
+ self._current_console_port = self._console_start_port_range
+ try:
+ VPCS_instance.console = find_unused_port(self._current_console_port, self._console_end_port_range, self._host)
+ except Exception as e:
+ raise VPCSError(e)
+ self._current_console_port += 1
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+
+ response = {"name": VPCS_instance.name,
+ "id": VPCS_instance.id}
+
+ defaults = VPCS_instance.defaults()
+ response.update(defaults)
+ self._VPCS_instances[VPCS_instance.id] = VPCS_instance
+ self.send_response(response)
+
+ @IModule.route("VPCS.delete")
+ def VPCS_delete(self, request):
+ """
+ Deletes an VPCS instance.
+
+ Mandatory request parameters:
+ - id (VPCS instance identifier)
+
+ Response parameter:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_DELETE_SCHEMA):
+ return
+
+ # get the instance
+ VPCS_instance = self.get_VPCS_instance(request["id"])
+ if not VPCS_instance:
+ return
+
+ try:
+ VPCS_instance.delete()
+ del self._VPCS_instances[request["id"]]
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(True)
+
+ @IModule.route("VPCS.update")
+ def VPCS_update(self, request):
+ """
+ Updates an VPCS instance
+
+ Mandatory request parameters:
+ - id (VPCS instance identifier)
+
+ Optional request parameters:
+ - any setting to update
+ - startup_config_base64 (startup-config base64 encoded)
+
+ Response parameters:
+ - updated settings
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_UPDATE_SCHEMA):
+ return
+
+ # get the instance
+ VPCS_instance = self.get_VPCS_instance(request["id"])
+ if not VPCS_instance:
+ return
+
+ response = {}
+ try:
+ # a new startup-config has been pushed
+ if "startup_config_base64" in request:
+ config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8")
+ config = "!\n" + config.replace("\r", "")
+ config = config.replace('%h', VPCS_instance.name)
+ config_path = os.path.join(VPCS_instance.working_dir, "startup-config")
+ try:
+ with open(config_path, "w") as f:
+ log.info("saving startup-config to {}".format(config_path))
+ f.write(config)
+ except OSError as e:
+ raise VPCSError("Could not save the configuration {}: {}".format(config_path, e))
+ # update the request with the new local startup-config path
+ request["startup_config"] = os.path.basename(config_path)
+
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+
+ # update the VPCS settings
+ for name, value in request.items():
+ if hasattr(VPCS_instance, name) and getattr(VPCS_instance, name) != value:
+ try:
+ setattr(VPCS_instance, name, value)
+ response[name] = value
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(response)
+
+ @IModule.route("VPCS.start")
+ def vm_start(self, request):
+ """
+ Starts an VPCS instance.
+
+ Mandatory request parameters:
+ - id (VPCS instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_START_SCHEMA):
+ return
+
+ # get the instance
+ VPCS_instance = self.get_VPCS_instance(request["id"])
+ if not VPCS_instance:
+ return
+
+ try:
+ log.debug("starting VPCS with command: {}".format(VPCS_instance.command()))
+ VPCS_instance.VPCS = self._VPCS
+ VPCS_instance.VPCSrc = self._VPCSrc
+ VPCS_instance.start()
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("VPCS.stop")
+ def vm_stop(self, request):
+ """
+ Stops an VPCS instance.
+
+ Mandatory request parameters:
+ - id (VPCS instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_STOP_SCHEMA):
+ return
+
+ # get the instance
+ VPCS_instance = self.get_VPCS_instance(request["id"])
+ if not VPCS_instance:
+ return
+
+ try:
+ VPCS_instance.stop()
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("VPCS.reload")
+ def vm_reload(self, request):
+ """
+ Reloads an VPCS instance.
+
+ Mandatory request parameters:
+ - id (VPCS identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_RELOAD_SCHEMA):
+ return
+
+ # get the instance
+ VPCS_instance = self.get_VPCS_instance(request["id"])
+ if not VPCS_instance:
+ return
+
+ try:
+ if VPCS_instance.is_running():
+ VPCS_instance.stop()
+ VPCS_instance.start()
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("VPCS.allocate_udp_port")
+ def allocate_udp_port(self, request):
+ """
+ Allocates a UDP port in order to create an UDP NIO.
+
+ Mandatory request parameters:
+ - id (VPCS identifier)
+ - port_id (unique port identifier)
+
+ Response parameters:
+ - port_id (unique port identifier)
+ - lport (allocated local port)
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ if not self.validate_request(request, VPCS_ALLOCATE_UDP_PORT_SCHEMA):
+ return
+
+ # get the instance
+ VPCS_instance = self.get_VPCS_instance(request["id"])
+ if not VPCS_instance:
+ return
+
+ try:
+
+ # find a UDP port
+ if self._current_udp_port >= self._udp_end_port_range:
+ self._current_udp_port = self._udp_start_port_range
+ try:
+ port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP")
+ except Exception as e:
+ raise VPCSError(e)
+ self._current_udp_port += 1
+
+ log.info("{} [id={}] has allocated UDP port {} with host {}".format(VPCS_instance.name,
+ VPCS_instance.id,
+ port,
+ self._host))
+ response = {"lport": port}
+
+ except VPCSError as e:
+ self.send_custom_error(str(e))
+ return
+
+ response["port_id"] = request["port_id"]
+ self.send_response(response)
+
+ def _check_for_privileged_access(self, device):
+ """
+ Check if VPCS can access Ethernet and TAP devices.
+
+ :param device: device name
+ """
+
+ # we are root, so VPCS should have privileged access too
+ if os.geteuid() == 0:
+ return
+
+ # test if VPCS has the CAP_NET_RAW capability
+ if "security.capability" in os.listxattr(self._VPCS):
+ try:
+ caps = os.getxattr(self._VPCS, "security.capability")
+ # test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
+ if struct.unpack(".
+
+"""
+Interface for TAP NIOs (UNIX based OSes only).
+"""
+
+
+class NIO_TAP(object):
+ """
+ VPCS TAP NIO.
+
+ :param tap_device: TAP device name (e.g. tap0)
+ """
+
+ def __init__(self, tap_device):
+
+ self._tap_device = tap_device
+
+ @property
+ def tap_device(self):
+ """
+ Returns the TAP device used by this NIO.
+
+ :returns: the TAP device name
+ """
+
+ return self._tap_device
+
+ def __str__(self):
+
+ return "NIO TAP"
diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py
new file mode 100644
index 00000000..8dca91b3
--- /dev/null
+++ b/gns3server/modules/vpcs/nios/nio_udp.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Interface for UDP NIOs.
+"""
+
+
+class NIO_UDP(object):
+ """
+ VPCS UDP NIO.
+
+ :param lport: local port number
+ :param rhost: remote address/host
+ :param rport: remote port number
+ """
+
+ _instance_count = 0
+
+ def __init__(self, lport, rhost, rport):
+
+ self._lport = lport
+ self._rhost = rhost
+ self._rport = rport
+
+ @property
+ def lport(self):
+ """
+ Returns the local port
+
+ :returns: local port number
+ """
+
+ return self._lport
+
+ @property
+ def rhost(self):
+ """
+ Returns the remote host
+
+ :returns: remote address/host
+ """
+
+ return self._rhost
+
+ @property
+ def rport(self):
+ """
+ Returns the remote port
+
+ :returns: remote port number
+ """
+
+ return self._rport
+
+ def __str__(self):
+
+ return "NIO UDP"
diff --git a/gns3server/modules/vpcs/schemas.py b/gns3server/modules/vpcs/schemas.py
new file mode 100644
index 00000000..d1061384
--- /dev/null
+++ b/gns3server/modules/vpcs/schemas.py
@@ -0,0 +1,306 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+VPCS_CREATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to create a new VPCS instance",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "VPCS device name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "path": {
+ "description": "path to the VPCS executable",
+ "type": "string",
+ "minLength": 1,
+ }
+ },
+ "required": ["path"]
+}
+
+VPCS_DELETE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to delete an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ },
+ "required": ["id"]
+}
+
+VPCS_UPDATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to update an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "VPCS device name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "path": {
+ "description": "path to the VPCS executable",
+ "type": "string",
+ "minLength": 1,
+ },
+ "script_file": {
+ "description": "path to the VPCS startup configuration file",
+ "type": "string",
+ "minLength": 1,
+ },
+ "script_file_base64": {
+ "description": "startup configuration base64 encoded",
+ "type": "string"
+ },
+ },
+ "required": ["id"]
+}
+
+VPCS_START_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to start an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ },
+ "required": ["id"]
+}
+
+VPCS_STOP_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to stop an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ },
+ "required": ["id"]
+}
+
+VPCS_RELOAD_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to reload an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ },
+ "required": ["id"]
+}
+
+VPCS_ALLOCATE_UDP_PORT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to allocate an UDP port for an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the VPCS instance",
+ "type": "integer"
+ },
+ },
+ "required": ["id", "port_id"]
+}
+
+VPCS_ADD_NIO_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to add a NIO for an VPCS instance",
+ "type": "object",
+
+ "definitions": {
+ "UDP": {
+ "description": "UDP Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_udp"]
+ },
+ "lport": {
+ "description": "Local port",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "rhost": {
+ "description": "Remote host",
+ "type": "string",
+ "minLength": 1
+ },
+ "rport": {
+ "description": "Remote port",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ }
+ },
+ "required": ["type", "lport", "rhost", "rport"],
+ "additionalProperties": False
+ },
+ "Ethernet": {
+ "description": "Generic Ethernet Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_generic_ethernet"]
+ },
+ "ethernet_device": {
+ "description": "Ethernet device name e.g. eth0",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "ethernet_device"],
+ "additionalProperties": False
+ },
+ "LinuxEthernet": {
+ "description": "Linux Ethernet Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_linux_ethernet"]
+ },
+ "ethernet_device": {
+ "description": "Ethernet device name e.g. eth0",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "ethernet_device"],
+ "additionalProperties": False
+ },
+ "TAP": {
+ "description": "TAP Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_tap"]
+ },
+ "tap_device": {
+ "description": "TAP device name e.g. tap0",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "tap_device"],
+ "additionalProperties": False
+ },
+ "UNIX": {
+ "description": "UNIX Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_unix"]
+ },
+ "local_file": {
+ "description": "path to the UNIX socket file (local)",
+ "type": "string",
+ "minLength": 1
+ },
+ "remote_file": {
+ "description": "path to the UNIX socket file (remote)",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "local_file", "remote_file"],
+ "additionalProperties": False
+ },
+ "VDE": {
+ "description": "VDE Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_vde"]
+ },
+ "control_file": {
+ "description": "path to the VDE control file",
+ "type": "string",
+ "minLength": 1
+ },
+ "local_file": {
+ "description": "path to the VDE control file",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "control_file", "local_file"],
+ "additionalProperties": False
+ },
+ "NULL": {
+ "description": "NULL Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_null"]
+ },
+ },
+ "required": ["type"],
+ "additionalProperties": False
+ },
+ },
+
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the VPCS instance",
+ "type": "integer"
+ },
+ "nio": {
+ "type": "object",
+ "description": "Network Input/Output",
+ "oneOf": [
+ {"$ref": "#/definitions/UDP"},
+ {"$ref": "#/definitions/Ethernet"},
+ {"$ref": "#/definitions/LinuxEthernet"},
+ {"$ref": "#/definitions/TAP"},
+ {"$ref": "#/definitions/UNIX"},
+ {"$ref": "#/definitions/VDE"},
+ {"$ref": "#/definitions/NULL"},
+ ]
+ },
+ },
+ "required": ["id", "port_id", "nio"]
+}
+
+VPCS_DELETE_NIO_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to delete a NIO for an VPCS instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "VPCS device instance ID",
+ "type": "integer"
+ },
+ },
+ "required": ["id"]
+}
diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py
new file mode 100644
index 00000000..3bd3f95a
--- /dev/null
+++ b/gns3server/modules/vpcs/vpcs_device.py
@@ -0,0 +1,471 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+VPCS device management (creates command line, processes, files etc.) in
+order to run an VPCS instance.
+"""
+
+import os
+import re
+import signal
+import subprocess
+import argparse
+import threading
+import configparser
+from .vpcscon import start_vpcscon
+from .vpcs_error import VPCSError
+from .nios.nio_udp import NIO_UDP
+from .nios.nio_tap import NIO_TAP
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class VPCSDevice(object):
+ """
+ VPCS device implementation.
+
+ :param path: path to VPCS executable
+ :param working_dir: path to a working directory
+ :param host: host/address to bind for console and UDP connections
+ :param name: name of this VPCS device
+ """
+
+ _instances = []
+
+ def __init__(self, path, working_dir, host="127.0.0.1", name=None):
+
+ # find an instance identifier (0 <= 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(0, 255):
+ if identifier not in self._instances:
+ self._id = identifier
+ self._instances.append(self._id)
+ break
+
+ if self._id == 0:
+ raise VPCSError("Maximum number of VPCS instances reached")
+
+ if name:
+ self._name = name
+ else:
+ self._name = "VPCS{}".format(self._id)
+ self._path = path
+ self._console = None
+ self._working_dir = None
+ self._command = []
+ self._process = None
+ self._vpcs_stdout_file = ""
+ self._vpcscon_thead = None
+ self._vpcscon_thread_stop_event = None
+ self._host = host
+ self._started = False
+
+ # VPCS settings
+ self._script_file = ""
+
+ # update the working directory
+ self.working_dir = working_dir
+
+ log.info("VPCS device {name} [id={id}] has been created".format(name=self._name,
+ id=self._id))
+
+ def defaults(self):
+ """
+ Returns all the default attribute values for VPCS.
+
+ :returns: default values (dictionary)
+ """
+
+ vpcs_defaults = {"name": self._name,
+ "path": self._path,
+ "script_file": self._script_file,
+ "console": self._console}
+
+ return vpcs_defaults
+
+ @property
+ def id(self):
+ """
+ Returns the unique ID for this VPCS device.
+
+ :returns: id (integer)
+ """
+
+ return(self._id)
+
+ @classmethod
+ def reset(cls):
+ """
+ Resets allocated instance list.
+ """
+
+ cls._instances.clear()
+
+ @property
+ def name(self):
+ """
+ Returns the name of this VPCS device.
+
+ :returns: name
+ """
+
+ return self._name
+
+ @name.setter
+ def name(self, new_name):
+ """
+ Sets the name of this VPCS device.
+
+ :param new_name: name
+ """
+
+ self._name = new_name
+ log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name,
+ id=self._id,
+ new_name=new_name))
+
+ @property
+ def path(self):
+ """
+ Returns the path to the VPCS executable.
+
+ :returns: path to VPCS
+ """
+
+ return(self._path)
+
+ @path.setter
+ def path(self, path):
+ """
+ Sets the path to the VPCS executable.
+
+ :param path: path to VPCS
+ """
+
+ self._path = path
+ log.info("VPCS {name} [id={id}]: path changed to {path}".format(name=self._name,
+ id=self._id,
+ path=path))
+
+ @property
+ def working_dir(self):
+ """
+ Returns current working directory
+
+ :returns: path to the working directory
+ """
+
+ return self._working_dir
+
+ @working_dir.setter
+ def working_dir(self, working_dir):
+ """
+ Sets the working directory for VPCS.
+
+ :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:
+ os.makedirs(working_dir)
+ except FileExistsError:
+ pass
+ except OSError as e:
+ raise VPCSError("Could not create working directory {}: {}".format(working_dir, e))
+
+ self._working_dir = working_dir
+ log.info("VPCS {name} [id={id}]: working directory changed to {wd}".format(name=self._name,
+ id=self._id,
+ wd=self._working_dir))
+
+ @property
+ def console(self):
+ """
+ Returns the TCP console port.
+
+ :returns: console port (integer)
+ """
+
+ return self._console
+
+ @console.setter
+ def console(self, console):
+ """
+ Sets the TCP console port.
+
+ :param console: console port (integer)
+ """
+
+ self._console = console
+ log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name,
+ id=self._id,
+ port=console))
+
+ def command(self):
+ """
+ Returns the VPCS command line.
+
+ :returns: VPCS command line (string)
+ """
+
+ return " ".join(self._build_command())
+
+ def delete(self):
+ """
+ Deletes this VPCS device.
+ """
+
+ self.stop()
+ self._instances.remove(self._id)
+ log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name,
+ id=self._id))
+
+ @property
+ def started(self):
+ """
+ Returns either this VPCS device has been started or not.
+
+ :returns: boolean
+ """
+
+ return self._started
+
+ def _start_vpcscon(self):
+ """
+ Starts vpcscon thread (for console connections).
+ """
+
+ if not self._vpcscon_thead:
+ telnet_server = "{}:{}".format(self._host, self._console)
+ log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server))
+ args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server)
+ self._vpcscon_thread_stop_event = threading.Event()
+ self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event))
+ self._vpcscon_thead.start() ", ".join(missing_libs)))
+
+ def start(self):
+ """
+ Starts the VPCS process.
+ """
+
+ if not self.is_running():
+
+ if not os.path.isfile(self._path):
+ raise VPCSError("VPCS image '{}' is not accessible".format(self._path))
+
+ if not os.access(self._path, os.X_OK):
+ raise VPCSError("VPCS image '{}' is not executable".format(self._path))
+
+ self._command = self._build_command()
+ try:
+ log.info("starting VPCS: {}".format(self._command))
+ self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log")
+ log.info("logging to {}".format(self._vpcs_stdout_file))
+ with open(self._vpcs_stdout_file, "w") as fd:
+ self._process = subprocess.Popen(self._command,
+ stdout=fd,
+ stderr=subprocess.STDOUT,
+ cwd=self._working_dir)
+ log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
+ self._started = True
+ except FileNotFoundError as e:
+ raise VPCSError("could not start VPCS: {}: 32-bit binary support is probably not installed".format(e))
+ except OSError as e:
+ vpcs_stdout = self.read_vpcs_stdout()
+ log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
+ raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
+
+ # start console support
+ self._start_vpcscon()
+
+ def stop(self):
+ """
+ Stops the VPCS process.
+ """
+
+ # stop the VPCS process
+ if self.is_running():
+ log.info("stopping VPCS 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("VPCS instance {} PID={} is still running".format(self._id,
+ self._process.pid))
+ self._process = None
+ self._started = False
+
+ # stop console support
+ if self._vpcscon_thead:
+ self._vpcscon_thread_stop_event.set()
+ if self._vpcscon_thead.is_alive():
+ self._vpcscon_thead.join(timeout=0.10)
+ self._vpcscon_thead = None
+
+
+ def read_vpcs_stdout(self):
+ """
+ Reads the standard output of the VPCS process.
+ Only use when the process has been stopped or has crashed.
+ """
+
+ output = ""
+ if self._vpcs_stdout_file:
+ try:
+ with open(self._vpcs_stdout_file) as file:
+ output = file.read()
+ except OSError as e:
+ log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e))
+ return output
+
+ def is_running(self):
+ """
+ Checks if the VPCS process is running
+
+ :returns: True or False
+ """
+
+ if self._process and self._process.poll() == None:
+ return True
+ return False
+
+
+ def slot_add_nio_binding(self, slot_id, port_id, nio):
+ """
+ Adds a slot NIO binding.
+
+ :param slot_id: slot ID
+ :param port_id: port ID
+ :param nio: NIO instance to add to the slot/port
+ """
+
+ try:
+ adapter = self._slots[slot_id]
+ except IndexError:
+ raise 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))
+
+ adapter.add_nio(port_id, nio)
+ log.info("VPCS {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name,
+ id=self._id,
+ nio=nio,
+ slot_id=slot_id,
+ port_id=port_id))
+
+ 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):
+ """
+ Command to start the VPCS process.
+ (to be passed to subprocess.Popen())
+
+ VPCS command line:
+ usage: vpcs [options] [scriptfile]
+ Option:
+ -h print this help then exit
+ -v print version information then exit
+
+ -p port run as a daemon listening on the tcp 'port'
+ -m num start byte of ether address, default from 0
+ -r file load and execute script file
+ compatible with older versions, DEPRECATED.
+
+ -e tap mode, using /dev/tapx (linux only)
+ -u udp mode, default
+
+ udp mode options:
+ -s port local udp base port, default from 20000
+ -c port remote udp base port (dynamips udp port), default from 30000
+ -t ip remote host IP, default 127.0.0.1
+
+ hypervisor mode option:
+ -H port run as the hypervisor listening on the tcp 'port'
+
+ If no 'scriptfile' specified, vpcs will read and execute the file named
+ 'startup.vpc' if it exsits in the current directory.
+
+ """
+
+ command = [self._path]
+ command.extend(["-p", str(self._console)])
+ command.extend(["-s", str(self._lport)])
+ command.extend(["-c", str(self._rport)])
+ command.extend(["-t", str(self._rhost)])
+ command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset
+ if self._script_file:
+ command.extend([self._script_file])
+ return command
+
+ @property
+ def script_file(self):
+ """
+ Returns the startup-config for this VPCS instance.
+
+ :returns: path to startup-config file
+ """
+
+ return self._script_file
+
+ @script_file.setter
+ def script_file(self, script_file):
+ """
+ Sets the startup-config for this VPCS instance.
+
+ :param script_file: path to startup-config file
+ """
+
+ self._script_file = script_file
+ log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name,
+ id=self._id,
+ config=self._script_file))
+
+
diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py
new file mode 100644
index 00000000..167129ba
--- /dev/null
+++ b/gns3server/modules/vpcs/vpcs_error.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Custom exceptions for VPCS module.
+"""
+
+
+class VPCSError(Exception):
+
+ def __init__(self, message, original_exception=None):
+
+ Exception.__init__(self, message)
+ if isinstance(message, Exception):
+ message = str(message)
+ self._message = message
+ self._original_exception = original_exception
+
+ def __repr__(self):
+
+ return self._message
+
+ def __str__(self):
+
+ return self._message
diff --git a/gns3server/modules/vpcs/vpcscon.py b/gns3server/modules/vpcs/vpcscon.py
new file mode 100644
index 00000000..b481175d
--- /dev/null
+++ b/gns3server/modules/vpcs/vpcscon.py
@@ -0,0 +1,642 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013, 2014 James E. Carpenter
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+import socket
+import sys
+import os
+import select
+import fcntl
+import struct
+import termios
+import tty
+import time
+import argparse
+import traceback
+
+
+import logging
+log = logging.getLogger(__name__)
+
+
+# Escape characters
+ESC_CHAR = '^^' # can be overriden from command line
+ESC_QUIT = 'q'
+
+# VPCS seems to only send *1* byte at a time. If
+# they ever fix that we'll be ready for it.
+BUFFER_SIZE = 1024
+
+# How long to wait before retrying a connection (seconds)
+RETRY_DELAY = 3
+
+# How often to test an idle connection (seconds)
+POLL_TIMEOUT = 3
+
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+EXIT_ABORT = 2
+
+# Mostly from:
+# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py
+#--[ Telnet Commands ]---------------------------------------------------------
+SE = 240 # End of subnegotiation parameters
+NOP = 241 # No operation
+DATMK = 242 # Data stream portion of a sync.
+BREAK = 243 # NVT Character BRK
+IP = 244 # Interrupt Process
+AO = 245 # Abort Output
+AYT = 246 # Are you there
+EC = 247 # Erase Character
+EL = 248 # Erase Line
+GA = 249 # The Go Ahead Signal
+SB = 250 # Sub-option to follow
+WILL = 251 # Will; request or confirm option begin
+WONT = 252 # Wont; deny option request
+DO = 253 # Do = Request or confirm remote option
+DONT = 254 # Don't = Demand or confirm option halt
+IAC = 255 # Interpret as Command
+SEND = 1 # Sub-process negotiation SEND command
+IS = 0 # Sub-process negotiation IS command
+#--[ Telnet Options ]----------------------------------------------------------
+BINARY = 0 # Transmit Binary
+ECHO = 1 # Echo characters back to sender
+RECON = 2 # Reconnection
+SGA = 3 # Suppress Go-Ahead
+TMARK = 6 # Timing Mark
+TTYPE = 24 # Terminal Type
+NAWS = 31 # Negotiate About Window Size
+LINEMO = 34 # Line Mode
+
+
+class FileLock:
+
+ # struct flock { /* from fcntl(2) */
+ # ...
+ # short l_type; /* Type of lock: F_RDLCK,
+ # F_WRLCK, F_UNLCK */
+ # short l_whence; /* How to interpret l_start:
+ # SEEK_SET, SEEK_CUR, SEEK_END */
+ # off_t l_start; /* Starting offset for lock */
+ # off_t l_len; /* Number of bytes to lock */
+ # pid_t l_pid; /* PID of process blocking our lock
+ # (F_GETLK only) */
+ # ...
+ # };
+ _flock = struct.Struct('hhqql')
+
+ def __init__(self, fname=None):
+ self.fd = None
+ self.fname = fname
+
+ def get_lock(self):
+ flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET,
+ 0, 0, os.getpid())
+ flk = self._flock.unpack(
+ fcntl.fcntl(self.fd, fcntl.F_GETLK, flk))
+
+ # If it's not locked (or is locked by us) then return None,
+ # otherwise return the PID of the owner.
+ if flk[0] == fcntl.F_UNLCK:
+ return None
+ return flk[4]
+
+ def lock(self):
+ try:
+ self.fd = open('{}.lck'.format(self.fname), 'a')
+ except Exception as e:
+ raise LockError("Couldn't get lock on {}: {}"
+ .format(self.fname, e))
+
+ flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
+ try:
+ fcntl.fcntl(self.fd, fcntl.F_SETLK, flk)
+ except BlockingIOError:
+ raise LockError("Already connected. PID {} has lock on {}"
+ .format(self.get_lock(), self.fname))
+
+ # If we got here then we must have the lock. Store the PID.
+ self.fd.truncate(0)
+ self.fd.write('{}\n'.format(os.getpid()))
+ self.fd.flush()
+
+ def unlock(self):
+ if self.fd:
+ # Deleting first prevents a race condition
+ os.unlink(self.fd.name)
+ self.fd.close()
+
+ def __enter__(self):
+ self.lock()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.unlock()
+ return False
+
+
+class Console:
+ def fileno(self):
+ raise NotImplementedError("Only routers have fileno()")
+
+
+class Router:
+ pass
+
+
+class TTY(Console):
+
+ def read(self, fileno, bufsize):
+ return self.fd.read(bufsize)
+
+ def write(self, buf):
+ return self.fd.write(buf)
+
+ def register(self, epoll):
+ self.epoll = epoll
+ epoll.register(self.fd, select.EPOLLIN | select.EPOLLET)
+
+ def __enter__(self):
+ try:
+ self.fd = open('/dev/tty', 'r+b', buffering=0)
+ except OSError as e:
+ raise TTYError("Couldn't open controlling TTY: {}".format(e))
+
+ # Save original flags
+ self.termios = termios.tcgetattr(self.fd)
+ self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
+
+ # Update flags
+ tty.setraw(self.fd, termios.TCSANOW)
+ fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK)
+
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+
+ # Restore flags to original settings
+ termios.tcsetattr(self.fd, termios.TCSANOW, self.termios)
+ fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl)
+
+ self.fd.close()
+
+ return False
+
+
+class TelnetServer(Console):
+
+ def __init__(self, addr, port, stop_event):
+ self.addr = addr
+ self.port = port
+ self.fd_dict = {}
+ self.stop_event = stop_event
+
+ def read(self, fileno, bufsize):
+ # Someone wants to connect?
+ if fileno == self.sock_fd.fileno():
+ self._accept()
+ return None
+
+ self._cur_fileno = fileno
+
+ # Read a maximum of _bufsize_ bytes without blocking. When it
+ # would want to block it means there's no more data. An empty
+ # buffer normally means that we've been disconnected.
+ try:
+ buf = self._read_cur(bufsize, socket.MSG_DONTWAIT)
+ except BlockingIOError:
+ return None
+ if not buf:
+ self._disconnect(fileno)
+
+ # Process and remove any telnet commands from the buffer
+ if IAC in buf:
+ buf = self._IAC_parser(buf)
+
+ return buf
+
+ def write(self, buf):
+ for fd in self.fd_dict.values():
+ fd.send(buf)
+
+ def register(self, epoll):
+ self.epoll = epoll
+ epoll.register(self.sock_fd, select.EPOLLIN)
+
+ def _read_block(self, bufsize):
+ buf = self._read_cur(bufsize, socket.MSG_WAITALL)
+ # If we don't get everything we were looking for then the
+ # client probably disconnected.
+ if len(buf) < bufsize:
+ self._disconnect(self._cur_fileno)
+ return buf
+
+ def _read_cur(self, bufsize, flags):
+ return self.fd_dict[self._cur_fileno].recv(bufsize, flags)
+
+ def _write_cur(self, buf):
+ return self.fd_dict[self._cur_fileno].send(buf)
+
+ def _IAC_parser(self, buf):
+ skip_to = 0
+ while not self.stop_event.is_set():
+ # Locate an IAC to process
+ iac_loc = buf.find(IAC, skip_to)
+ if iac_loc < 0:
+ break
+
+ # Get the TELNET command
+ iac_cmd = bytearray([IAC])
+ try:
+ iac_cmd.append(buf[iac_loc + 1])
+ except IndexError:
+ buf.extend(self._read_block(1))
+ iac_cmd.append(buf[iac_loc + 1])
+
+ # Is this just a 2-byte TELNET command?
+ if iac_cmd[1] not in [WILL, WONT, DO, DONT]:
+ if iac_cmd[1] == AYT:
+ log.debug("Telnet server received Are-You-There (AYT)")
+ self._write_cur(
+ b'\r\nYour Are-You-There received. I am here.\r\n'
+ )
+ elif iac_cmd[1] == IAC:
+ # It's data, not an IAC
+ iac_cmd.pop()
+ # This prevents the 0xff from being
+ # interputed as yet another IAC
+ skip_to = iac_loc + 1
+ log.debug("Received IAC IAC")
+ elif iac_cmd[1] == NOP:
+ pass
+ else:
+ log.debug("Unhandled telnet command: "
+ "{0:#x} {1:#x}".format(*iac_cmd))
+
+ # This must be a 3-byte TELNET command
+ else:
+ try:
+ iac_cmd.append(buf[iac_loc + 2])
+ except IndexError:
+ buf.extend(self._read_block(1))
+ iac_cmd.append(buf[iac_loc + 2])
+ # We do ECHO, SGA, and BINARY. Period.
+ if (iac_cmd[1] == DO
+ and iac_cmd[2] not in [ECHO, SGA, BINARY]):
+
+ self._write_cur(bytes([IAC, WONT, iac_cmd[2]]))
+ log.debug("Telnet WON'T {:#x}".format(iac_cmd[2]))
+ else:
+ log.debug("Unhandled telnet command: "
+ "{0:#x} {1:#x} {2:#x}".format(*iac_cmd))
+
+ # Remove the entire TELNET command from the buffer
+ buf = buf.replace(iac_cmd, b'', 1)
+
+ # Return the new copy of the buffer, minus telnet commands
+ return buf
+
+ def _accept(self):
+ fd, addr = self.sock_fd.accept()
+ self.fd_dict[fd.fileno()] = fd
+ self.epoll.register(fd, select.EPOLLIN | select.EPOLLET)
+
+ log.info("Telnet connection from {}:{}".format(addr[0], addr[1]))
+
+ # This is a one-way negotiation. This is very basic so there
+ # shouldn't be any problems with any decent client.
+ fd.send(bytes([IAC, WILL, ECHO,
+ IAC, WILL, SGA,
+ IAC, WILL, BINARY,
+ IAC, DO, BINARY]))
+
+ if args.telnet_limit and len(self.fd_dict) > args.telnet_limit:
+ fd.send(b'\r\nToo many connections\r\n')
+ self._disconnect(fd.fileno())
+ log.warn("Client disconnected because of too many connections. "
+ "(limit currently {})".format(args.telnet_limit))
+
+ def _disconnect(self, fileno):
+ fd = self.fd_dict.pop(fileno)
+ log.info("Telnet client disconnected")
+ fd.shutdown(socket.SHUT_RDWR)
+ fd.close()
+
+ def __enter__(self):
+ # Open a socket and start listening
+ sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ sock_fd.bind((self.addr, self.port))
+ except OSError:
+ raise TelnetServerError("Cannot bind to {}:{}"
+ .format(self.addr, self.port))
+
+ sock_fd.listen(socket.SOMAXCONN)
+ self.sock_fd = sock_fd
+ log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port))
+
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ for fileno in list(self.fd_dict.keys()):
+ self._disconnect(fileno)
+ self.sock_fd.close()
+ return False
+
+
+class VPCS(Router):
+
+ def __init__(self, ttyC, ttyS, stop_event):
+ self.ttyC = ttyC
+ self.ttyS = ttyS
+ self.stop_event = stop_event
+
+ def read(self, bufsize):
+ try:
+ buf = self.fd.recv(bufsize)
+ except BlockingIOError:
+ return None
+ return buf
+
+ def write(self, buf):
+ self.fd.send(buf)
+
+ def _open(self):
+ self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+ self.fd.setblocking(False)
+
+ def _bind(self):
+ try:
+ os.unlink(self.ttyC)
+ except FileNotFoundError:
+ pass
+ except Exception as e:
+ raise NetioError("Couldn't unlink socket {}: {}"
+ .format(self.ttyC, e))
+
+ try:
+ self.fd.bind(self.ttyC)
+ except Exception as e:
+ raise NetioError("Couldn't create socket {}: {}"
+ .format(self.ttyC, e))
+
+ def _connect(self):
+ # Keep trying until we connect or die trying
+ while not self.stop_event.is_set():
+ try:
+ self.fd.connect(self.ttyS)
+ except FileNotFoundError:
+ log.debug("Waiting to connect to {}".format(self.ttyS))
+ time.sleep(RETRY_DELAY)
+ except Exception as e:
+ raise NetioError("Couldn't connect to socket {}: {}"
+ .format(self.ttyS, e))
+ else:
+ break
+
+ def register(self, epoll):
+ self.epoll = epoll
+ epoll.register(self.fd, select.EPOLLIN | select.EPOLLET)
+
+ def fileno(self):
+ return self.fd.fileno()
+
+ def __enter__(self):
+ self._open()
+ self._bind()
+ self._connect()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ os.unlink(self.ttyC)
+ self.fd.close()
+ return False
+
+
+class VPCSConError(Exception):
+ pass
+
+
+class LockError(VPCSConError):
+ pass
+
+
+class NetioError(VPCSConError):
+ pass
+
+
+class TTYError(VPCSConError):
+ pass
+
+
+class TelnetServerError(VPCSConError):
+ pass
+
+
+class ConfigError(VPCSConError):
+ pass
+
+
+def mkdir_netio(netio_dir):
+ try:
+ os.mkdir(netio_dir)
+ except FileExistsError:
+ pass
+ except Exception as e:
+ raise NetioError("Couldn't create directory {}: {}"
+ .format(netio_dir, e))
+
+
+def send_recv_loop(console, router, esc_char, stop_event):
+
+ epoll = select.epoll()
+ router.register(epoll)
+ console.register(epoll)
+
+ router_fileno = router.fileno()
+ esc_quit = bytes(ESC_QUIT.upper(), 'ascii')
+ esc_state = False
+
+ while not stop_event.is_set():
+ event_list = epoll.poll(timeout=POLL_TIMEOUT)
+
+ # When/if the poll times out we send an empty datagram. If VPCS
+ # has gone away then this will toss a ConnectionRefusedError
+ # exception.
+ if not event_list:
+ router.write(b'')
+ continue
+
+ for fileno, event in event_list:
+ buf = bytearray()
+
+ # VPCS --> tty(s)
+ if fileno == router_fileno:
+ while not stop_event.is_set():
+ data = router.read(BUFFER_SIZE)
+ if not data:
+ break
+ buf.extend(data)
+ console.write(buf)
+
+ # tty --> VPCS
+ else:
+ while not stop_event.is_set():
+ data = console.read(fileno, BUFFER_SIZE)
+ if not data:
+ break
+ buf.extend(data)
+
+ # If we just received the escape character then
+ # enter the escape state.
+ #
+ # If we are in the escape state then check for a
+ # quit command. Or if it's the escape character then
+ # send the escape character. Else, send the escape
+ # character we ate earlier and whatever character we
+ # just got. Exit escape state.
+ #
+ # If we're not in the escape state and this isn't an
+ # escape character then just send it to VPCS.
+ if esc_state:
+ if buf.upper() == esc_quit:
+ sys.exit(EXIT_SUCCESS)
+ elif buf == esc_char:
+ router.write(esc_char)
+ else:
+ router.write(esc_char)
+ router.write(buf)
+ esc_state = False
+ elif buf == esc_char:
+ esc_state = True
+ else:
+ router.write(buf)
+
+
+def get_args():
+ parser = argparse.ArgumentParser(
+ description='Connect to an VPCS console port.')
+ parser.add_argument('-d', '--debug', action='store_true',
+ help='display some debugging information')
+ parser.add_argument('-e', '--escape',
+ help='set escape character (default: %(default)s)',
+ default=ESC_CHAR, metavar='CHAR')
+ parser.add_argument('-t', '--telnet-server',
+ help='start telnet server listening on ADDR:PORT',
+ metavar='ADDR:PORT', default=False)
+ parser.add_argument('-l', '--telnet-limit',
+ help='maximum number of simultaneous '
+ 'telnet connections (default: %(default)s)',
+ metavar='LIMIT', type=int, default=1)
+ parser.add_argument('appl_id', help='VPCS instance identifier')
+ return parser.parse_args()
+
+
+def get_escape_character(escape):
+
+ # Figure out the escape character to use.
+ # Can be any ASCII character or a spelled out control
+ # character, like "^e". The string "none" disables it.
+ if escape.lower() == 'none':
+ esc_char = b''
+ elif len(escape) == 2 and escape[0] == '^':
+ c = ord(escape[1].upper()) - 0x40
+ if not 0 <= c <= 0x1f: # control code range
+ raise ConfigError("Invalid control code")
+ esc_char = bytes([c])
+ elif len(escape) == 1:
+ try:
+ esc_char = bytes(escape, 'ascii')
+ except ValueError as e:
+ raise ConfigError("Invalid escape character") from e
+ else:
+ raise ConfigError("Invalid length for escape character")
+
+ return esc_char
+
+
+def start_VPCScon(cmdline_args, stop_event):
+
+ global args
+ args = cmdline_args
+
+ if args.debug:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ # default logging level
+ logging.basicConfig(level=logging.INFO)
+
+ # Create paths for the Unix domain sockets
+ netio = '/tmp/netio{}'.format(os.getuid())
+ ttyC = '{}/ttyC{}'.format(netio, args.appl_id)
+ ttyS = '{}/ttyS{}'.format(netio, args.appl_id)
+
+ try:
+ mkdir_netio(netio)
+ with FileLock(ttyC):
+ esc_char = get_escape_character(args.escape)
+
+ if args.telnet_server:
+ addr, _, port = args.telnet_server.partition(':')
+ nport = 0
+ try:
+ nport = int(port)
+ except ValueError:
+ pass
+ if (addr == '' or nport == 0):
+ raise ConfigError('format for --telnet-server must be '
+ 'ADDR:PORT (like 127.0.0.1:20000)')
+
+ while not stop_event.is_set():
+ try:
+ if args.telnet_server:
+ with TelnetServer(addr, nport, stop_event) as console:
+ with VPCS(ttyC, ttyS, stop_event) as router:
+ send_recv_loop(console, router, b'', stop_event)
+ else:
+ with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console:
+ send_recv_loop(console, router, esc_char, stop_event)
+ except ConnectionRefusedError:
+ pass
+ except KeyboardInterrupt:
+ sys.exit(EXIT_ABORT)
+ finally:
+ # Put us at the beginning of a line
+ if not args.telnet_server:
+ print()
+
+ except VPCSConError as e:
+ if args.debug:
+ traceback.print_exc(file=sys.stderr)
+ else:
+ print(e, file=sys.stderr)
+ sys.exit(EXIT_FAILURE)
+
+ log.info("exiting...")
+
+
+def main():
+
+ import threading
+ stop_event = threading.Event()
+ args = get_args()
+ start_VPCScon(args, stop_event)
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/vpcs/test_vpcs_device.py b/tests/vpcs/test_vpcs_device.py
new file mode 100644
index 00000000..9d72f2e6
--- /dev/null
+++ b/tests/vpcs/test_vpcs_device.py
@@ -0,0 +1,29 @@
+from gns3server.modules.vpcs import VPCSDevice
+import os
+import pytest
+
+
+@pytest.fixture(scope="session")
+def vpcs(request):
+
+ cwd = os.path.dirname(os.path.abspath(__file__))
+ vpcs_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
+ vpcs_device = VPCSDevice(vpcs_path, "/tmp")
+ vpcs_device.start()
+ request.addfinalizer(vpcs_device.delete)
+ return vpcs_device
+
+
+def test_vpcs_is_started(vpcs):
+
+ print(vpcs.command())
+ assert vpcs.id == 1 # we should have only one VPCS running!
+ assert vpcs.is_running()
+
+
+def test_vpcs_restart(vpcs):
+
+ vpcs.stop()
+ assert not vpcs.is_running()
+ vpcs.start()
+ assert vpcs.is_running()