Working VPCS implementation.

This commit is contained in:
grossmj 2014-05-18 19:12:46 -06:00
parent 85ef421d72
commit 0af4ea81ff
6 changed files with 260 additions and 315 deletions

View File

@ -19,10 +19,16 @@
Useful functions... in the attic ;) Useful functions... in the attic ;)
""" """
import sys
import os
import struct
import socket import socket
import errno import errno
import time import time
import logging
log = logging.getLogger(__name__)
def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]): def find_unused_port(start_port, end_port, host='127.0.0.1', socket_type="TCP", ignore_ports=[]):
""" """
@ -102,3 +108,31 @@ def wait_socket_is_ready(host, port, wait=2.0, socket_timeout=10):
break break
return (connection_success, last_exception) 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

View File

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

View File

@ -87,22 +87,16 @@ class VPCS(IModule):
# a new process start when calling IModule # a new process start when calling IModule
IModule.__init__(self, name, *args, **kwargs) IModule.__init__(self, name, *args, **kwargs)
self._vpcs_instances = {} self._vpcs_instances = {}
self._console_start_port_range = 4001 self._console_start_port_range = 4512
self._console_end_port_range = 4512 self._console_end_port_range = 5000
self._allocated_console_ports = [] self._allocated_udp_ports = []
self._current_console_port = self._console_start_port_range self._udp_start_port_range = 40001
self._udp_start_port_range = 30001 self._udp_end_port_range = 40512
self._udp_end_port_range = 40001
self._current_udp_port = self._udp_start_port_range
self._host = kwargs["host"] self._host = kwargs["host"]
self._projects_dir = kwargs["projects_dir"] self._projects_dir = kwargs["projects_dir"]
self._tempdir = kwargs["temp_dir"] self._tempdir = kwargs["temp_dir"]
self._working_dir = self._projects_dir self._working_dir = self._projects_dir
# check every 5 seconds
#self._vpcs_callback = self.add_periodic_callback(self._check_vpcs_is_alive, 5000)
#self._vpcs_callback.start()
def stop(self, signum=None): def stop(self, signum=None):
""" """
Properly stops the module. Properly stops the module.
@ -118,27 +112,6 @@ class VPCS(IModule):
IModule.stop(self, signum) # this will stop the I/O loop IModule.stop(self, signum) # this will stop the I/O loop
def _check_vpcs_is_alive(self):
"""
Periodic callback to check if VPCS is alive
for each VPCS instance.
Sends a notification to the client if not.
"""
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
if vpcs_instance.started and (not vpcs_instance.is_running() or not vpcs_instance.is_vpcs_running()):
notification = {"module": self.name,
"id": vpcs_id,
"name": vpcs_instance.name}
if not vpcs_instance.is_running():
stdout = vpcs_instance.read_vpcs_stdout()
notification["message"] = "VPCS has stopped running"
notification["details"] = stdout
self.send_notification("{}.vpcs_stopped".format(self.name), notification)
vpcs_instance.stop()
def get_vpcs_instance(self, vpcs_id): def get_vpcs_instance(self, vpcs_id):
""" """
Returns a VPCS device instance. Returns a VPCS device instance.
@ -171,9 +144,7 @@ class VPCS(IModule):
VPCSDevice.reset() VPCSDevice.reset()
self._vpcs_instances.clear() self._vpcs_instances.clear()
self._remote_server = False self._allocated_udp_ports.clear()
self._current_console_port = self._console_start_port_range
self._current_udp_port = self._udp_start_port_range
log.info("VPCS module has been reset") log.info("VPCS module has been reset")
@ -183,6 +154,7 @@ class VPCS(IModule):
Set or update settings. Set or update settings.
Optional request parameters: Optional request parameters:
- path (path to vpcs)
- working_dir (path to a working directory) - working_dir (path to a working directory)
- project_name - project_name
- console_start_port_range - console_start_port_range
@ -197,15 +169,18 @@ class VPCS(IModule):
self.send_param_error() self.send_param_error()
return return
if "vpcs" in request and request["vpcs"]: if "path" in request and request["path"]:
self._vpcs = request["vpcs"] self._vpcs = request["path"]
log.info("VPCS path set to {}".format(self._vpcs)) log.info("VPCS path set to {}".format(self._vpcs))
for vpcs_id in self._vpcs_instances:
vpcs_instance = self._vpcs_instances[vpcs_id]
vpcs_instance.path = self._vpcs
if "working_dir" in request: if "working_dir" in request:
new_working_dir = request["working_dir"] new_working_dir = request["working_dir"]
log.info("this server is local with working directory path to {}".format(new_working_dir)) log.info("this server is local with working directory path to {}".format(new_working_dir))
else: else:
new_working_dir = os.path.join(self._projects_dir, request["project_name"] + ".gns3") new_working_dir = os.path.join(self._projects_dir, request["project_name"])
log.info("this server is remote with working directory path to {}".format(new_working_dir)) log.info("this server is remote with working directory path to {}".format(new_working_dir))
if self._projects_dir != self._working_dir != new_working_dir: if self._projects_dir != self._working_dir != new_working_dir:
if not os.path.isdir(new_working_dir): if not os.path.isdir(new_working_dir):
@ -234,29 +209,11 @@ class VPCS(IModule):
log.debug("received request {}".format(request)) log.debug("received request {}".format(request))
def test_result(self, message, result="error"):
"""
"""
return {"result": result, "message": message}
@IModule.route("vpcs.test_settings")
def test_settings(self, request):
"""
"""
response = []
self.send_response(response)
@IModule.route("vpcs.create") @IModule.route("vpcs.create")
def vpcs_create(self, request): def vpcs_create(self, request):
""" """
Creates a new VPCS instance. Creates a new VPCS instance.
Mandatory request parameters:
- path (path to the VPCS executable)
Optional request parameters: Optional request parameters:
- name (VPCS name) - name (VPCS name)
@ -269,16 +226,12 @@ class VPCS(IModule):
""" """
# validate the request # validate the request
if not self.validate_request(request, VPCS_CREATE_SCHEMA): if request and not self.validate_request(request, VPCS_CREATE_SCHEMA):
return return
name = None name = None
if "name" in request: if request and "name" in request:
name = request["name"] 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:
try: try:
@ -288,31 +241,13 @@ class VPCS(IModule):
except OSError as e: except OSError as e:
raise VPCSError("Could not create working directory {}".format(e)) raise VPCSError("Could not create working directory {}".format(e))
# a new base-script-file has been pushed vpcs_instance = VPCSDevice(self._vpcs,
if "base_script_file_base64" in request: self._working_dir,
config = base64.decodestring(request["base_script_file_base64"].encode("utf-8")).decode("utf-8") self._host,
config = "!\n" + config.replace("\r", "") name,
#config = config.replace('%h', vpcs_instance.name) self._console_start_port_range,
config_path = os.path.join(self._working_dir, "base-script-file") self._console_end_port_range)
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
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
@ -367,7 +302,7 @@ class VPCS(IModule):
Optional request parameters: Optional request parameters:
- any setting to update - any setting to update
- base_script_file_base64 (script-file base64 encoded) - script_file_base64 (base64 encoded)
Response parameters: Response parameters:
- updated settings - updated settings
@ -384,28 +319,42 @@ class VPCS(IModule):
if not vpcs_instance: if not vpcs_instance:
return return
response = {} config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc")
try: try:
# a new base-script-file has been pushed if "script_file_base64" in request:
if "base_script_file_base64" in request: # a new startup-config has been pushed
config = base64.decodestring(request["base_script_file_base64"].encode("utf-8")).decode("utf-8") config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8")
config = "!\n" + config.replace("\r", "") config = config.replace("\r", "")
config = config.replace('%h', vpcs_instance.name) config = config.replace('%h', vpcs_instance.name)
config_path = os.path.join(vpcs_instance.working_dir, "base-script-file")
try: try:
with open(config_path, "w") as f: 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) f.write(config)
except OSError as e: except OSError as e:
raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) raise VPCSError("Could not save the configuration {}: {}".format(config_path, e))
# update the request with the new local base-script-file path # update the request with the new local startup-config path
request["base_script_file"] = os.path.basename(config_path) request["script_file"] = os.path.basename(config_path)
elif "script_file" in request:
if os.path.isfile(request["script_file"]) and request["script_file"] != config_path:
# this is a local file set in the GUI
try:
with open(request["script_file"], "r") 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: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
# update the VPCS settings # update the VPCS settings
response = {}
for name, value in request.items(): for name, value in request.items():
if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value: if hasattr(vpcs_instance, name) and getattr(vpcs_instance, name) != value:
try: try:
@ -442,7 +391,6 @@ class VPCS(IModule):
try: try:
log.debug("starting VPCS with command: {}".format(vpcs_instance.command())) log.debug("starting VPCS with command: {}".format(vpcs_instance.command()))
vpcs_instance.vpcs = self._vpcs
vpcs_instance.start() vpcs_instance.start()
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
@ -537,53 +485,24 @@ class VPCS(IModule):
return return
try: try:
port = find_unused_port(self._udp_start_port_range,
# find a UDP port self._udp_end_port_range,
if self._current_udp_port >= self._udp_end_port_range: host=self._host,
self._current_udp_port = self._udp_start_port_range socket_type="UDP",
try: ignore_ports=self._allocated_udp_ports)
port = find_unused_port(self._current_udp_port, self._udp_end_port_range, host=self._host, socket_type="UDP") except Exception as e:
except Exception as e:
raise VPCSError(e)
self._current_udp_port += 1
log.info("{} [id={}] has allocated UDP port {} with host {}".format(vpcs_instance.name,
vpcs_instance.id,
port,
self._host))
response = {"lport": port}
except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return
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"] response["port_id"] = request["port_id"]
self.send_response(response) self.send_response(response)
def _check_for_privileged_access(self, device):
"""
Check if VPCS can access Ethernet and TAP devices.
:param device: device name
"""
# we are root, so vpcs should have privileged access too
if os.geteuid() == 0:
return
# test if VPCS has the CAP_NET_RAW capability
if "security.capability" in os.listxattr(self._vpcs):
try:
caps = os.getxattr(self._vpcs, "security.capability")
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
return
except Exception as e:
log.error("could not determine if CAP_NET_RAW capability is set for {}: {}".format(self._vpcs, e))
return
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, device))
@IModule.route("vpcs.add_nio") @IModule.route("vpcs.add_nio")
def add_nio(self, request): def add_nio(self, request):
""" """
@ -591,7 +510,6 @@ class VPCS(IModule):
Mandatory request parameters: Mandatory request parameters:
- id (VPCS instance identifier) - id (VPCS instance identifier)
- slot (slot number)
- port (port number) - port (port number)
- port_id (unique port identifier) - port_id (unique port identifier)
- nio (one of the following) - nio (one of the following)
@ -617,7 +535,6 @@ class VPCS(IModule):
if not vpcs_instance: if not vpcs_instance:
return return
slot = request["slot"]
port = request["port"] port = request["port"]
try: try:
nio = None nio = None
@ -634,7 +551,8 @@ class VPCS(IModule):
nio = NIO_UDP(lport, rhost, rport) nio = NIO_UDP(lport, rhost, rport)
elif request["nio"]["type"] == "nio_tap": elif request["nio"]["type"] == "nio_tap":
tap_device = request["nio"]["tap_device"] tap_device = request["nio"]["tap_device"]
self._check_for_privileged_access(tap_device) if not self.has_privileged_access(self._vpcs, tap_device):
raise VPCSError("{} has no privileged access to {}.".format(self._vpcs, tap_device))
nio = NIO_TAP(tap_device) nio = NIO_TAP(tap_device)
if not nio: if not nio:
raise VPCSError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) raise VPCSError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
@ -643,7 +561,7 @@ class VPCS(IModule):
return return
try: try:
vpcs_instance.slot_add_nio_binding(slot, port, nio) vpcs_instance.port_add_nio_binding(port, nio)
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return
@ -657,7 +575,6 @@ class VPCS(IModule):
Mandatory request parameters: Mandatory request parameters:
- id (VPCS instance identifier) - id (VPCS instance identifier)
- slot (slot identifier)
- port (port identifier) - port (port identifier)
Response parameters: Response parameters:
@ -675,10 +592,11 @@ class VPCS(IModule):
if not vpcs_instance: if not vpcs_instance:
return return
slot = request["slot"]
port = request["port"] port = request["port"]
try: try:
vpcs_instance.slot_remove_nio_binding(slot, port) nio = vpcs_instance.port_remove_nio_binding(port)
if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports:
self._allocated_udp_ports.remove(nio.lport)
except VPCSError as e: except VPCSError as e:
self.send_custom_error(str(e)) self.send_custom_error(str(e))
return return

View File

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

View File

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

View File

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