mirror of
https://github.com/GNS3/gns3-server.git
synced 2024-12-24 06:56:42 +00:00
parent
a61686a436
commit
c6b4ac04e1
3
.gitignore
vendored
3
.gitignore
vendored
@ -34,6 +34,3 @@ nosetests.xml
|
|||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
.settings
|
.settings
|
||||||
|
|
||||||
#Gedit temp files
|
|
||||||
*~
|
|
||||||
|
@ -18,9 +18,8 @@
|
|||||||
import sys
|
import sys
|
||||||
from .base import IModule
|
from .base import IModule
|
||||||
from .dynamips import Dynamips
|
from .dynamips import Dynamips
|
||||||
from .vpcs import VPCS
|
|
||||||
|
|
||||||
MODULES = [Dynamips, VPCS]
|
MODULES = [Dynamips]
|
||||||
|
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
# IOU runs only on Linux
|
# IOU runs only on Linux
|
||||||
|
@ -313,6 +313,13 @@ IOU_ADD_NIO_SCHEMA = {
|
|||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"maximum": 3
|
"maximum": 3
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"slot": {
|
||||||
|
"description": "Slot number",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 15
|
||||||
|
},
|
||||||
"nio": {
|
"nio": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Network Input/Output",
|
"description": "Network Input/Output",
|
||||||
|
@ -1,690 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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("<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")
|
|
||||||
def add_nio(self, request):
|
|
||||||
"""
|
|
||||||
Adds an NIO (Network Input/Output) for an VPCS instance.
|
|
||||||
|
|
||||||
Mandatory request parameters:
|
|
||||||
- id (VPCS instance identifier)
|
|
||||||
- slot (slot number)
|
|
||||||
- port (port number)
|
|
||||||
- port_id (unique port identifier)
|
|
||||||
- nio (one of the following)
|
|
||||||
- type "nio_udp"
|
|
||||||
- lport (local port)
|
|
||||||
- rhost (remote host)
|
|
||||||
- rport (remote port)
|
|
||||||
- type "nio_tap"
|
|
||||||
- tap_device (TAP device name e.g. tap0)
|
|
||||||
|
|
||||||
Response parameters:
|
|
||||||
- port_id (unique port identifier)
|
|
||||||
|
|
||||||
:param request: JSON request
|
|
||||||
"""
|
|
||||||
|
|
||||||
# validate the request
|
|
||||||
if not self.validate_request(request, VPCS_ADD_NIO_SCHEMA):
|
|
||||||
return
|
|
||||||
|
|
||||||
# get the instance
|
|
||||||
VPCS_instance = self.get_VPCS_instance(request["id"])
|
|
||||||
if not VPCS_instance:
|
|
||||||
return
|
|
||||||
|
|
||||||
slot = request["slot"]
|
|
||||||
port = request["port"]
|
|
||||||
try:
|
|
||||||
nio = None
|
|
||||||
if request["nio"]["type"] == "nio_udp":
|
|
||||||
lport = request["nio"]["lport"]
|
|
||||||
rhost = request["nio"]["rhost"]
|
|
||||||
rport = request["nio"]["rport"]
|
|
||||||
nio = NIO_UDP(lport, rhost, rport)
|
|
||||||
elif request["nio"]["type"] == "nio_tap":
|
|
||||||
tap_device = request["nio"]["tap_device"]
|
|
||||||
self._check_for_privileged_access(tap_device)
|
|
||||||
nio = NIO_TAP(tap_device)
|
|
||||||
if not nio:
|
|
||||||
raise VPCSError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
|
|
||||||
except VPCSError as e:
|
|
||||||
self.send_custom_error(str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
VPCS_instance.slot_add_nio_binding(slot, port, nio)
|
|
||||||
except VPCSError as e:
|
|
||||||
self.send_custom_error(str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_response({"port_id": request["port_id"]})
|
|
||||||
|
|
||||||
@IModule.route("VPCS.delete_nio")
|
|
||||||
def delete_nio(self, request):
|
|
||||||
"""
|
|
||||||
Deletes an NIO (Network Input/Output).
|
|
||||||
|
|
||||||
Mandatory request parameters:
|
|
||||||
- id (VPCS instance identifier)
|
|
||||||
- slot (slot identifier)
|
|
||||||
- port (port identifier)
|
|
||||||
|
|
||||||
Response parameters:
|
|
||||||
- True on success
|
|
||||||
|
|
||||||
:param request: JSON request
|
|
||||||
"""
|
|
||||||
|
|
||||||
# validate the request
|
|
||||||
if not self.validate_request(request, VPCS_DELETE_NIO_SCHEMA):
|
|
||||||
return
|
|
||||||
|
|
||||||
# get the instance
|
|
||||||
VPCS_instance = self.get_VPCS_instance(request["id"])
|
|
||||||
if not VPCS_instance:
|
|
||||||
return
|
|
||||||
|
|
||||||
slot = request["slot"]
|
|
||||||
port = request["port"]
|
|
||||||
try:
|
|
||||||
VPCS_instance.slot_remove_nio_binding(slot, port)
|
|
||||||
except VPCSError as e:
|
|
||||||
self.send_custom_error(str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_response(True)
|
|
||||||
|
|
||||||
@IModule.route("VPCS.echo")
|
|
||||||
def echo(self, request):
|
|
||||||
"""
|
|
||||||
Echo end point for testing purposes.
|
|
||||||
|
|
||||||
:param request: JSON request
|
|
||||||
"""
|
|
||||||
|
|
||||||
if request == None:
|
|
||||||
self.send_param_error()
|
|
||||||
else:
|
|
||||||
log.debug("received request {}".format(request))
|
|
||||||
self.send_response(request)
|
|
@ -1,46 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Interface for 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"
|
|
@ -1,72 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Interface for UDP NIOs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
@ -1,306 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
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"]
|
|
||||||
}
|
|
@ -1,471 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Custom exceptions for 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
|
|
@ -1,642 +0,0 @@
|
|||||||
#!/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 <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
|
|
||||||
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()
|
|
@ -1,29 +0,0 @@
|
|||||||
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()
|
|
Loading…
Reference in New Issue
Block a user