Working VPCS implementation.

pull/21/head^2
grossmj 10 years ago
parent 85ef421d72
commit 0af4ea81ff

@ -19,10 +19,16 @@
Useful functions... in the attic ;)
"""
import sys
import os
import struct
import socket
import errno
import time
import logging
log = logging.getLogger(__name__)
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]):
"""
@ -102,3 +108,31 @@ def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
break
return (connection_success, last_exception)
def has_privileged_access(executable, device):
"""
Check if an executable can access Ethernet and TAP devices in
RAW mode.
:param executable: executable path
:param device: device name
:returns: True or False
"""
# we are root, so we should have privileged access too
if os.geteuid() == 0:
return True
# test if the executable has the CAP_NET_RAW capability (Linux only)
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable):
try:
caps = os.getxattr(executable, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return True
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(executable, e))
return False

@ -37,6 +37,7 @@ from .nios.nio_udp import NIO_UDP
from .nios.nio_tap import NIO_TAP
from .nios.nio_generic_ethernet import NIO_GenericEthernet
from ..attic import find_unused_port
from ..attic import has_privileged_access
from .schemas import IOU_CREATE_SCHEMA
from .schemas import IOU_DELETE_SCHEMA
@ -206,6 +207,7 @@ class IOU(IModule):
- iourc (base64 encoded iourc file)
Optional request parameters:
- iouyap (path to iouyap)
- working_dir (path to a working directory)
- project_name
- console_start_port_range
@ -406,7 +408,6 @@ class IOU(IModule):
if not iou_instance:
return
response = {}
config_path = os.path.join(iou_instance.working_dir, "startup-config")
try:
if "startup_config_base64" in request:
@ -435,13 +436,14 @@ class IOU(IModule):
request["startup_config"] = os.path.basename(config_path)
except OSError as e:
raise IOUError("Could not save the configuration from {} to {}: {}".format(request["startup_config"], config_path, e))
elif not os.path.isfile(os.path.join(iou_instance.working_dir, request["startup_config"])):
raise IOUError("Startup-config {} could not be found on this server".format(request["startup_config"]))
elif not os.path.isfile(config_path):
raise IOUError("Startup-config {} could not be found on this server".format(config_path))
except IOUError as e:
self.send_custom_error(str(e))
return
# update the IOU settings
response = {}
for name, value in request.items():
if hasattr(iou_instance, name) and getattr(iou_instance, name) != value:
try:
@ -591,30 +593,6 @@ class IOU(IModule):
response["port_id"] = request["port_id"]
self.send_response(response)
def _check_for_privileged_access(self, device):
"""
Check if iouyap can access Ethernet and TAP devices.
:param device: device name
"""
# we are root, so iouyap should have privileged access too
if os.geteuid() == 0:
return
# test if iouyap has the CAP_NET_RAW capability
if "security.capability" in os.listxattr(self._iouyap):
try:
caps = os.getxattr(self._iouyap, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(self._iouyap, e))
return
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, device))
@IModule.route("iou.add_nio")
def add_nio(self, request):
"""
@ -667,10 +645,13 @@ class IOU(IModule):
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)
if not self.has_privileged_access(self._iouyap, tap_device):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, tap_device))
nio = NIO_TAP(tap_device)
elif request["nio"]["type"] == "nio_generic_ethernet":
ethernet_device = request["nio"]["ethernet_device"]
if not self.has_privileged_access(self._iouyap, ethernet_device):
raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device))
self._check_for_privileged_access(ethernet_device)
nio = NIO_GenericEthernet(ethernet_device)
if not nio:

@ -87,22 +87,16 @@ class VPCS(IModule):
# 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._console_start_port_range = 4512
self._console_end_port_range = 5000
self._allocated_udp_ports = []
self._udp_start_port_range = 40001
self._udp_end_port_range = 40512
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.
@ -118,27 +112,6 @@ class VPCS(IModule):
IModule.stop(self, signum) # this will stop the I/O loop
def _check_vpcs_is_alive(self):
"""
Periodic callback to check if VPCS is alive
for each VPCS instance.
Sends a notification to the client if not.
"""
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
if vpcs_instance.started and (not vpcs_instance.is_running() or not vpcs_instance.is_vpcs_running()):
notification = {"module": self.name,
"id": vpcs_id,
"name": vpcs_instance.name}
if not vpcs_instance.is_running():
stdout = vpcs_instance.read_vpcs_stdout()
notification["message"] = "VPCS has stopped running"
notification["details"] = stdout
self.send_notification("{}.vpcs_stopped".format(self.name), notification)
vpcs_instance.stop()
def get_vpcs_instance(self, vpcs_id):
"""
Returns a VPCS device instance.
@ -171,9 +144,7 @@ class VPCS(IModule):
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
self._allocated_udp_ports.clear()
log.info("VPCS module has been reset")
@ -183,6 +154,7 @@ class VPCS(IModule):
Set or update settings.
Optional request parameters:
- path (path to vpcs)
- working_dir (path to a working directory)
- project_name
- console_start_port_range
@ -197,15 +169,18 @@ class VPCS(IModule):
self.send_param_error()
return
if "vpcs" in request and request["vpcs"]:
self._vpcs = request["vpcs"]
if "path" in request and request["path"]:
self._vpcs = request["path"]
log.info("VPCS path set to {}".format(self._vpcs))
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.path = self._vpcs
if "working_dir" in request:
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")
new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir):
@ -234,29 +209,11 @@ class VPCS(IModule):
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)
@ -269,16 +226,12 @@ class VPCS(IModule):
"""
# validate the request
if not self.validate_request(request, VPCS_CREATE_SCHEMA):
if request and not self.validate_request(request, VPCS_CREATE_SCHEMA):
return
name = None
if "name" in request:
if request and "name" in request:
name = request["name"]
base_script_file = None
if "base_script_file" in request:
base_script_file = request["base_script_file"]
vpcs_path = request["path"]
try:
try:
@ -288,31 +241,13 @@ class VPCS(IModule):
except OSError as e:
raise VPCSError("Could not create working directory {}".format(e))
# a new base-script-file has been pushed
if "base_script_file_base64" in request:
config = base64.decodestring(request["base_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(self._working_dir, "base-script-file")
try:
with open(config_path, "w") as f:
log.info("saving base-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 base-script-file path
request["base_script_file"] = os.path.basename(config_path)
vpcs_instance = VPCSDevice(vpcs_path, config_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
vpcs_instance = VPCSDevice(self._vpcs,
self._working_dir,
self._host,
name,
self._console_start_port_range,
self._console_end_port_range)
except VPCSError as e:
self.send_custom_error(str(e))
return
@ -367,7 +302,7 @@ class VPCS(IModule):
Optional request parameters:
- any setting to update
- base_script_file_base64 (script-file base64 encoded)
- script_file_base64 (base64 encoded)
Response parameters:
- updated settings
@ -384,28 +319,42 @@ class VPCS(IModule):
if not vpcs_instance:
return
response = {}
config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc")
try:
# a new base-script-file has been pushed
if "base_script_file_base64" in request:
config = base64.decodestring(request["base_script_file_base64"].encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "")
if "script_file_base64" in request:
# a new startup-config has been pushed
config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name)
config_path = os.path.join(vpcs_instance.working_dir, "base-script-file")
try:
with open(config_path, "w") as f:
log.info("saving base-script-file to {}".format(config_path))
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 base-script-file path
request["base_script_file"] = os.path.basename(config_path)
# update the request with the new local startup-config path
request["script_file"] = os.path.basename(config_path)
elif "script_file" in request:
if os.path.isfile(request["script_file"]) and request["script_file"] != config_path:
# this is a local file set in the GUI
try:
with open(request["script_file"], "r") as f:
config = f.read()
with open(config_path, "w") as f:
config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name)
f.write(config)
request["script_file"] = os.path.basename(config_path)
except OSError as e:
raise VPCSError("Could not save the configuration from {} to {}: {}".format(request["script_file"], config_path, e))
elif not os.path.isfile(config_path):
raise VPCSError("Startup-config {} could not be found on this server".format(config_path))
except VPCSError as e:
self.send_custom_error(str(e))
return
# update the VPCS settings
response = {}
for name, value in request.items():
if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value:
try:
@ -442,7 +391,6 @@ class VPCS(IModule):
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))
@ -537,53 +485,24 @@ class VPCS(IModule):
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:
port = find_unused_port(self._udp_start_port_range,
self._udp_end_port_range,
host=self._host,
socket_type="UDP",
ignore_ports=self._allocated_udp_ports)
except Exception as e:
self.send_custom_error(str(e))
return
self._allocated_udp_ports.append(port)
log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name,
vpcs_instance.id,
port,
self._host))
response = {"lport": port}
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):
"""
@ -591,7 +510,6 @@ class VPCS(IModule):
Mandatory request parameters:
- id (VPCS instance identifier)
- slot (slot number)
- port (port number)
- port_id (unique port identifier)
- nio (one of the following)
@ -617,7 +535,6 @@ class VPCS(IModule):
if not vpcs_instance:
return
slot = request["slot"]
port = request["port"]
try:
nio = None
@ -634,7 +551,8 @@ class VPCS(IModule):
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)
if not self.has_privileged_access(self._vpcs, tap_device):
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, 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"]))
@ -643,7 +561,7 @@ class VPCS(IModule):
return
try:
vpcs_instance.slot_add_nio_binding(slot, port, nio)
vpcs_instance.port_add_nio_binding(port, nio)
except VPCSError as e:
self.send_custom_error(str(e))
return
@ -657,7 +575,6 @@ class VPCS(IModule):
Mandatory request parameters:
- id (VPCS instance identifier)
- slot (slot identifier)
- port (port identifier)
Response parameters:
@ -675,10 +592,11 @@ class VPCS(IModule):
if not vpcs_instance:
return
slot = request["slot"]
port = request["port"]
try:
vpcs_instance.slot_remove_nio_binding(slot, port)
nio = vpcs_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except VPCSError as e:
self.send_custom_error(str(e))
return

@ -26,27 +26,13 @@ VPCS_CREATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"path": {
"description": "path to the VPCS executable",
"type": "string",
"minLength": 1,
},
"base_script_file": {
"description": "path to the VPCS startup configuration file",
"type": "string",
"minLength": 1,
},
"base_script_file_base64": {
"description": "startup script file base64 encoded",
"type": "string"
},
},
"required": ["path"]
}
VPCS_DELETE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete an VPCS instance",
"description": "Request validation to delete a VPCS instance",
"type": "object",
"properties": {
"id": {
@ -59,7 +45,7 @@ VPCS_DELETE_SCHEMA = {
VPCS_UPDATE_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to update an VPCS instance",
"description": "Request validation to update a VPCS instance",
"type": "object",
"properties": {
"id": {
@ -71,18 +57,19 @@ VPCS_UPDATE_SCHEMA = {
"type": "string",
"minLength": 1,
},
"path": {
"description": "path to the VPCS executable",
"type": "string",
"minLength": 1,
"console": {
"description": "console TCP port",
"minimum": 1,
"maximum": 65535,
"type": "integer"
},
"base_script_file": {
"description": "path to the VPCS startup script file file",
"script_file": {
"description": "Path to the VPCS script file file",
"type": "string",
"minLength": 1,
},
"base_script_file_base64": {
"description": "startup script file base64 encoded",
"script_file_base64": {
"description": "Script file base64 encoded",
"type": "string"
},
},
@ -91,7 +78,7 @@ VPCS_UPDATE_SCHEMA = {
VPCS_START_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to start an VPCS instance",
"description": "Request validation to start a VPCS instance",
"type": "object",
"properties": {
"id": {
@ -104,7 +91,7 @@ VPCS_START_SCHEMA = {
VPCS_STOP_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to stop an VPCS instance",
"description": "Request validation to stop a VPCS instance",
"type": "object",
"properties": {
"id": {
@ -117,7 +104,7 @@ VPCS_STOP_SCHEMA = {
VPCS_RELOAD_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to reload an VPCS instance",
"description": "Request validation to reload a VPCS instance",
"type": "object",
"properties": {
"id": {
@ -130,7 +117,7 @@ VPCS_RELOAD_SCHEMA = {
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",
"description": "Request validation to allocate an UDP port for a VPCS instance",
"type": "object",
"properties": {
"id": {
@ -147,7 +134,7 @@ VPCS_ALLOCATE_UDP_PORT_SCHEMA = {
VPCS_ADD_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to add a NIO for an VPCS instance",
"description": "Request validation to add a NIO for a VPCS instance",
"type": "object",
"definitions": {
@ -284,6 +271,12 @@ VPCS_ADD_NIO_SCHEMA = {
"description": "Unique port identifier for the VPCS instance",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 0
},
"nio": {
"type": "object",
"description": "Network Input/Output",
@ -298,18 +291,24 @@ VPCS_ADD_NIO_SCHEMA = {
]
},
},
"required": ["id", "port_id", "nio"]
"required": ["id", "port_id", "port", "nio"]
}
VPCS_DELETE_NIO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Request validation to delete a NIO for an VPCS instance",
"description": "Request validation to delete a NIO for a VPCS instance",
"type": "object",
"properties": {
"id": {
"description": "VPCS device instance ID",
"type": "integer"
},
"port": {
"description": "Port number",
"type": "integer",
"minimum": 0,
"maximum": 0
},
},
"required": ["id"]
"required": ["id", "port"]
}

@ -22,12 +22,12 @@ order to run an VPCS instance.
import os
import subprocess
import sys
import socket
import signal
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
from ..attic import find_unused_port
import logging
log = logging.getLogger(__name__)
@ -41,17 +41,26 @@ class VPCSDevice(object):
: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
:param console_start_port_range: TCP console port range start
:param console_end_port_range: TCP console port range end
"""
_instances = []
def __init__(self, path, base_script_file, working_dir, host="127.0.0.1", name=None):
# find an instance identifier (1 <= id <= 512)
# This 512 limit is due to a restriction on the number of possible
# mac addresses given in VPCS using the -m option
_allocated_console_ports = []
def __init__(self,
path,
working_dir,
host="127.0.0.1",
name=None,
console_start_port_range=4512,
console_end_port_range=5000):
# find an instance identifier (1 <= id <= 255)
# This 255 limit is due to a restriction on the number of possible
# MAC addresses given in VPCS using the -m option
self._id = 0
for identifier in range(1, 513):
for identifier in range(1, 256):
if identifier not in self._instances:
self._id = identifier
self._instances.append(self._id)
@ -64,6 +73,7 @@ class VPCSDevice(object):
self._name = name
else:
self._name = "VPCS{}".format(self._id)
self._path = path
self._console = None
self._working_dir = None
@ -72,17 +82,29 @@ class VPCSDevice(object):
self._vpcs_stdout_file = ""
self._host = "127.0.0.1"
self._started = False
self._console_start_port_range = console_start_port_range
self._console_end_port_range = console_end_port_range
# VPCS settings
self._base_script_file = base_script_file
self._ethernet_adapters = [EthernetAdapter()] # one adapter = 1 interfaces
self._slots = self._ethernet_adapters
self._script_file = ""
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
# update the working directory
self.working_dir = working_dir
# allocate a console port
try:
self._console = find_unused_port(self._console_start_port_range,
self._console_end_port_range,
self._host,
ignore_ports=self._allocated_console_ports)
except Exception as e:
raise VPCSError(e)
self._allocated_console_ports.append(self._console)
log.info("VPCS device {name} [id={id}] has been created".format(name=self._name,
id=self._id))
id=self._id))
def defaults(self):
"""
@ -92,8 +114,7 @@ class VPCSDevice(object):
"""
vpcs_defaults = {"name": self._name,
"path": self._path,
"base_script_file": self._base_script_file,
"script_file": self._script_file,
"console": self._console}
return vpcs_defaults
@ -115,6 +136,7 @@ class VPCSDevice(object):
"""
cls._instances.clear()
cls._allocated_console_ports.clear()
@property
def name(self):
@ -181,7 +203,7 @@ class VPCSDevice(object):
"""
# create our own working directory
working_dir = os.path.join(working_dir, "vpcs", "device-{}".format(self._id))
working_dir = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id))
try:
os.makedirs(working_dir)
except FileExistsError:
@ -212,7 +234,12 @@ class VPCSDevice(object):
:param console: console port (integer)
"""
if console in self._allocated_console_ports:
raise VPCSError("Console port {} is already in used by another VPCS device".format(console))
self._allocated_console_ports.remove(self._console)
self._console = console
self._allocated_console_ports.append(self._console)
log.info("VPCS {name} [id={id}]: console port set to {port}".format(name=self._name,
id=self._id,
port=console))
@ -224,6 +251,7 @@ class VPCSDevice(object):
:returns: VPCS command line (string)
"""
print(self._build_command())
return " ".join(self._build_command())
def delete(self):
@ -233,6 +261,10 @@ class VPCSDevice(object):
self.stop()
self._instances.remove(self._id)
if self.console:
self._allocated_console_ports.remove(self.console)
log.info("VPCS device {name} [id={id}] has been deleted".format(name=self._name,
id=self._id))
@ -254,10 +286,13 @@ class VPCSDevice(object):
if not self.is_running():
if not os.path.isfile(self._path):
raise VPCSError("VPCS image '{}' is not accessible".format(self._path))
raise VPCSError("VPCS '{}' 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))
raise VPCSError("VPCS '{}' is not executable".format(self._path))
if not self._ethernet_adapter.get_nio(0):
raise VPCSError("This VPCS instance must be connected in order to start")
self._command = self._build_command()
try:
@ -284,14 +319,9 @@ class VPCSDevice(object):
# 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.send_signal(signal.SIGUSR1) # send SIGUSR1 will stop VPCS
self._process.wait()
self._process = None
self._started = False
@ -317,70 +347,49 @@ class VPCSDevice(object):
: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
if self._process and self._process.poll() == None:
return True
return False
def slot_add_nio_binding(self, slot_id, port_id, nio):
def port_add_nio_binding(self, port_id, nio):
"""
Adds a slot NIO binding.
Adds a port NIO binding.
:param slot_id: slot ID
:param port_id: port ID
:param 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,
if not self._ethernet_adapter.port_exists(port_id):
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_id=port_id))
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))
self._ethernet_adapter.add_nio(port_id, nio)
log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name,
id=self._id,
nio=nio,
port_id=port_id))
def slot_remove_nio_binding(self, slot_id, port_id):
def port_remove_nio_binding(self, port_id):
"""
Removes a slot NIO binding.
Removes a port 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))
:returns: NIO instance
"""
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,
if not self._ethernet_adapter.port_exists(port_id):
raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
port_id=port_id))
nio = self._ethernet_adapter.get_nio(port_id)
self._ethernet_adapter.remove_nio(port_id)
log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name,
id=self._id,
nio=nio,
port_id=port_id))
return nio
def _build_command(self):
"""
Command to start the VPCS process.
@ -392,12 +401,13 @@ class VPCSDevice(object):
-h print this help then exit
-v print version information then exit
-i num number of vpc instances to start (default is 9)
-p port run as a daemon listening on the tcp 'port'
-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)
-e tap mode, using /dev/tapx by default (linux only)
-u udp mode, default
udp mode options:
@ -405,56 +415,59 @@ class VPCSDevice(object):
-c port remote udp base port (dynamips udp port), default from 30000
-t ip remote host IP, default 127.0.0.1
tap mode options:
-d device device name, works only when -i is set to 1
hypervisor mode option:
-H port run as the hypervisor listening on the tcp 'port'
If no 'scriptfile' specified, VPCS will read and execute the file named
If no 'scriptfile' specified, vpcs will read and execute the file named
'startup.vpc' if it exsits in the current directory.
"""
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._base_script_file:
command.extend([self._base_script_file])
command.extend(["-p", str(self._console)]) # listen to console port
nio = self._ethernet_adapter.get_nio(0)
if nio:
if isinstance(nio, NIO_UDP):
# UDP tunnel
command.extend(["-s", str(nio.lport)]) # source UDP port
command.extend(["-c", str(nio.rport)]) # destination UDP port
command.extend(["-t", nio.rhost]) # destination host
elif isinstance(nio, NIO_TAP):
# TAP interface
command.extend(["-e"])
command.extend(["-d", nio.tap_device])
command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset
command.extend(["-i", "1"]) # option to start only one VPC instance
command.extend(["-F"]) # option to avoid the daemonization of VPCS
if self._script_file:
command.extend([self._script_file])
return command
@property
def base_script_file(self):
def script_file(self):
"""
Returns the script-file for this VPCS instance.
:returns: path to script-file file
:returns: path to script-file
"""
return self._base_script_file
return self._script_file
@base_script_file.setter
def base_script_file(self, base_script_file):
@script_file.setter
def script_file(self, script_file):
"""
Sets the base-script-file for this VPCS instance.
Sets the script-file for this VPCS instance.
:param base_script_file: path to base-script-file file
:param base_script_file: path to base-script-file
"""
self._base_script_file = base_script_file
log.info("VPCS {name} [id={id}]: base_script_file set to {config}".format(name=self._name,
id=self._id,
config=self._base_script_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))

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

Loading…
Cancel
Save