mirror of
https://github.com/GNS3/gns3-server.git
synced 2024-12-19 04:47:54 +00:00
Merge pull request #11 from joebowen/master
Initial pull request for VPCS module
This commit is contained in:
commit
db4280713c
3
.gitignore
vendored
3
.gitignore
vendored
@ -34,3 +34,6 @@ nosetests.xml
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
|
||||
# Gedit Backup Files
|
||||
*~
|
||||
|
@ -18,8 +18,10 @@
|
||||
import sys
|
||||
from .base import IModule
|
||||
from .dynamips import Dynamips
|
||||
from .vpcs import VPCS
|
||||
|
||||
MODULES = [Dynamips]
|
||||
MODULES.append(VPCS)
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU runs only on Linux
|
||||
|
675
gns3server/modules/vpcs/__init__.py
Normal file
675
gns3server/modules/vpcs/__init__.py
Normal file
@ -0,0 +1,675 @@
|
||||
# -*- 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 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
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
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
|
||||
- script_file_base64 (script-file 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 script-file has been pushed
|
||||
if "script_file_base64" in request:
|
||||
config = base64.decodestring(request["script_file_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, "script-file")
|
||||
try:
|
||||
with open(config_path, "w") as f:
|
||||
log.info("saving script-file 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 script-file path
|
||||
request["script_file"] = 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.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)
|
0
gns3server/modules/vpcs/adapters/__init__.py
Normal file
0
gns3server/modules/vpcs/adapters/__init__.py
Normal file
104
gns3server/modules/vpcs/adapters/adapter.py
Normal file
104
gns3server/modules/vpcs/adapters/adapter.py
Normal file
@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class Adapter(object):
|
||||
"""
|
||||
Base class for adapters.
|
||||
|
||||
:param interfaces: number of interfaces supported by this adapter.
|
||||
"""
|
||||
|
||||
def __init__(self, interfaces=1):
|
||||
|
||||
self._interfaces = interfaces
|
||||
|
||||
self._ports = {}
|
||||
for port_id in range(0, interfaces):
|
||||
self._ports[port_id] = None
|
||||
|
||||
def removable(self):
|
||||
"""
|
||||
Returns True if the adapter can be removed from a slot
|
||||
and False if not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def port_exists(self, port_id):
|
||||
"""
|
||||
Checks if a port exists on this adapter.
|
||||
|
||||
:returns: True is the port exists,
|
||||
False otherwise.
|
||||
"""
|
||||
|
||||
if port_id in self._ports:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_nio(self, port_id, nio):
|
||||
"""
|
||||
Adds a NIO to a port on this adapter.
|
||||
|
||||
:param port_id: port ID (integer)
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._ports[port_id] = nio
|
||||
|
||||
def remove_nio(self, port_id):
|
||||
"""
|
||||
Removes a NIO from a port on this adapter.
|
||||
|
||||
:param port_id: port ID (integer)
|
||||
"""
|
||||
|
||||
self._ports[port_id] = None
|
||||
|
||||
def get_nio(self, port_id):
|
||||
"""
|
||||
Returns the NIO assigned to a port.
|
||||
|
||||
:params port_id: port ID (integer)
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
return self._ports[port_id]
|
||||
|
||||
@property
|
||||
def ports(self):
|
||||
"""
|
||||
Returns port to NIO mapping
|
||||
|
||||
:returns: dictionary port -> NIO
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
@property
|
||||
def interfaces(self):
|
||||
"""
|
||||
Returns the number of interfaces supported by this adapter.
|
||||
|
||||
:returns: number of interfaces
|
||||
"""
|
||||
|
||||
return self._interfaces
|
31
gns3server/modules/vpcs/adapters/ethernet_adapter.py
Normal file
31
gns3server/modules/vpcs/adapters/ethernet_adapter.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .adapter import Adapter
|
||||
|
||||
|
||||
class EthernetAdapter(Adapter):
|
||||
"""
|
||||
VPCS Ethernet adapter.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Adapter.__init__(self, interfaces=1)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "VPCS Ethernet adapter"
|
46
gns3server/modules/vpcs/nios/nio_tap.py
Normal file
46
gns3server/modules/vpcs/nios/nio_tap.py
Normal file
@ -0,0 +1,46 @@
|
||||
# -*- 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):
|
||||
"""
|
||||
IOU 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"
|
72
gns3server/modules/vpcs/nios/nio_udp.py
Normal file
72
gns3server/modules/vpcs/nios/nio_udp.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Interface for UDP NIOs.
|
||||
"""
|
||||
|
||||
|
||||
class NIO_UDP(object):
|
||||
"""
|
||||
IOU UDP NIO.
|
||||
|
||||
:param lport: local port number
|
||||
:param rhost: remote address/host
|
||||
:param rport: remote port number
|
||||
"""
|
||||
|
||||
_instance_count = 0
|
||||
|
||||
def __init__(self, lport, rhost, rport):
|
||||
|
||||
self._lport = lport
|
||||
self._rhost = rhost
|
||||
self._rport = rport
|
||||
|
||||
@property
|
||||
def lport(self):
|
||||
"""
|
||||
Returns the local port
|
||||
|
||||
:returns: local port number
|
||||
"""
|
||||
|
||||
return self._lport
|
||||
|
||||
@property
|
||||
def rhost(self):
|
||||
"""
|
||||
Returns the remote host
|
||||
|
||||
:returns: remote address/host
|
||||
"""
|
||||
|
||||
return self._rhost
|
||||
|
||||
@property
|
||||
def rport(self):
|
||||
"""
|
||||
Returns the remote port
|
||||
|
||||
:returns: remote port number
|
||||
"""
|
||||
|
||||
return self._rport
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "NIO UDP"
|
306
gns3server/modules/vpcs/schemas.py
Normal file
306
gns3server/modules/vpcs/schemas.py
Normal file
@ -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 <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"]
|
||||
}
|
469
gns3server/modules/vpcs/vpcs_device.py
Normal file
469
gns3server/modules/vpcs/vpcs_device.py
Normal file
@ -0,0 +1,469 @@
|
||||
# -*- 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
|
||||
import sys
|
||||
import socket
|
||||
from .vpcs_error import VPCSError
|
||||
from .adapters.ethernet_adapter import EthernetAdapter
|
||||
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 (1 <= id <= 255)
|
||||
# This 255 limit is due to a restriction on the number of possible
|
||||
# mac addresses given in vpcs using the -m option
|
||||
self._id = 0
|
||||
for identifier in range(1, 256):
|
||||
if identifier not in self._instances:
|
||||
self._id = identifier
|
||||
self._instances.append(self._id)
|
||||
break
|
||||
|
||||
if self._id == 0:
|
||||
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._host = "127.0.0.1"
|
||||
self._started = False
|
||||
|
||||
# vpcs settings
|
||||
self._script_file = ""
|
||||
self._ethernet_adapters = [EthernetAdapter()] # one adapter = 1 interfaces
|
||||
self._slots = self._ethernet_adapters
|
||||
|
||||
# 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(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 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))
|
||||
|
||||
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:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((self._host, self._console))
|
||||
sock.send(bytes("quit\n", 'UTF-8'))
|
||||
sock.close()
|
||||
except TypeError as e:
|
||||
log.warn("vpcs instance {} PID={} is still running. Error: {}".format(self._id,
|
||||
self._process.pid, e))
|
||||
self._process = None
|
||||
self._started = False
|
||||
|
||||
|
||||
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:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((self._host, self._console))
|
||||
sock.close()
|
||||
return True
|
||||
except:
|
||||
e = sys.exc_info()[0]
|
||||
log.warn("Could not connect to {}:{}. Error: {}".format(self._host, self._console, e))
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
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)])
|
||||
|
||||
for adapter in self._slots:
|
||||
for unit in adapter.ports.keys():
|
||||
nio = adapter.get_nio(unit)
|
||||
if nio:
|
||||
if isinstance(nio, NIO_UDP):
|
||||
# UDP tunnel
|
||||
command.extend(["-s", str(nio.lport)])
|
||||
command.extend(["-c", str(nio.rport)])
|
||||
command.extend(["-t", str(nio.rhost)])
|
||||
|
||||
elif isinstance(nio, NIO_TAP):
|
||||
# TAP interface
|
||||
command.extend(["-e"]) #, str(nio.tap_device)]) #TODO: Fix, currently vpcs doesn't allow specific tap_device
|
||||
|
||||
command.extend(["-m", str(self._id)]) #The unique ID is used to set the mac address offset
|
||||
command.extend(["-i", str(1)]) #Option to start only one pc instance
|
||||
if self._script_file:
|
||||
command.extend([self._script_file])
|
||||
return command
|
||||
|
||||
@property
|
||||
def script_file(self):
|
||||
"""
|
||||
Returns the script-file for this vpcs instance.
|
||||
|
||||
:returns: path to script-file file
|
||||
"""
|
||||
|
||||
return self._script_file
|
||||
|
||||
@script_file.setter
|
||||
def script_file(self, script_file):
|
||||
"""
|
||||
Sets the script-file for this vpcs instance.
|
||||
|
||||
:param script_file: path to script-file 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))
|
||||
|
||||
|
39
gns3server/modules/vpcs/vpcs_error.py
Normal file
39
gns3server/modules/vpcs/vpcs_error.py
Normal file
@ -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 <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
|
29
tests/vpcs/test_vpcs_device.py
Normal file
29
tests/vpcs/test_vpcs_device.py
Normal file
@ -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, "vpcs")
|
||||
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