mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
New base server.
This commit is contained in:
1 parent
5e665cc67a
commit
61344a1669
51 files changed
+2632
-1835
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2015 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
|
||||
@@ -16,16 +16,11 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from .base import IModule
|
||||
from .deadman import DeadMan
|
||||
from .dynamips import Dynamips
|
||||
from .qemu import Qemu
|
||||
from .vpcs import VPCS
|
||||
from .virtualbox import VirtualBox
|
||||
|
||||
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
|
||||
MODULES = [VPCS]
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU runs only on Linux
|
||||
from .iou import IOU
|
||||
MODULES.append(IOU)
|
||||
#if sys.platform.startswith("linux"):
|
||||
# # IOU runs only on Linux
|
||||
# from .iou import IOU
|
||||
# MODULES.append(IOU)
|
||||
+42
-317
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2015 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
|
||||
@@ -15,327 +15,52 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Base class (interface) for modules
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import gns3server.jsonrpc as jsonrpc
|
||||
import multiprocessing
|
||||
import zmq
|
||||
import signal
|
||||
|
||||
from gns3server.config import Config
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
import asyncio
|
||||
|
||||
|
||||
class IModule(multiprocessing.Process):
|
||||
"""
|
||||
Module interface.
|
||||
|
||||
:param name: module name
|
||||
:param args: arguments for the module
|
||||
:param kwargs: named arguments for the module
|
||||
"""
|
||||
|
||||
modules = {}
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
|
||||
config = Config.instance()
|
||||
server_config = config.get_default_section()
|
||||
self._images_dir = os.path.expandvars(os.path.expanduser(server_config.get("upload_directory", "~/GNS3/images")))
|
||||
multiprocessing.Process.__init__(self, name=name)
|
||||
self._context = None
|
||||
self._ioloop = None
|
||||
self._stream = None
|
||||
self._dealer = None
|
||||
self._zmq_host = args[0] # ZeroMQ server address
|
||||
self._zmq_port = args[1] # ZeroMQ server port
|
||||
self._current_session = None
|
||||
self._current_destination = None
|
||||
self._current_call_id = None
|
||||
self._stopping = False
|
||||
self._cloud_settings = config.cloud_settings()
|
||||
|
||||
def _setup(self):
|
||||
"""
|
||||
Sets up PyZMQ and creates the stream to handle requests
|
||||
"""
|
||||
|
||||
self._context = zmq.Context()
|
||||
self._ioloop = zmq.eventloop.ioloop.IOLoop.instance()
|
||||
self._stream = self._create_stream(self._zmq_host, self._zmq_port, self._decode_request)
|
||||
|
||||
def _create_stream(self, host=None, port=0, callback=None):
|
||||
"""
|
||||
Creates a new ZMQ stream.
|
||||
|
||||
:returns: ZMQ stream instance
|
||||
"""
|
||||
|
||||
self._dealer = self._context.socket(zmq.DEALER)
|
||||
self._dealer.setsockopt(zmq.IDENTITY, self.name.encode("utf-8"))
|
||||
if host and port:
|
||||
log.info("ZeroMQ client ({}) connecting to {}:{}".format(self.name, host, port))
|
||||
try:
|
||||
self._dealer.connect("tcp://{}:{}".format(host, port))
|
||||
except zmq.error.ZMQError as e:
|
||||
log.critical("Could not connect to ZeroMQ server on {}:{}, reason: {}".format(host, port, e))
|
||||
raise SystemExit
|
||||
else:
|
||||
log.info("ZeroMQ client ({}) connecting to ipc:///tmp/gns3.ipc".format(self.name))
|
||||
try:
|
||||
self._dealer.connect("ipc:///tmp/gns3.ipc")
|
||||
except zmq.error.ZMQError as e:
|
||||
log.critical("Could not connect to ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e))
|
||||
raise SystemExit
|
||||
|
||||
stream = zmq.eventloop.zmqstream.ZMQStream(self._dealer, self._ioloop)
|
||||
if callback:
|
||||
stream.on_recv(callback)
|
||||
return stream
|
||||
|
||||
def add_periodic_callback(self, callback, time):
|
||||
"""
|
||||
Adds a periodic callback to the ioloop.
|
||||
|
||||
:param callback: callback to be called
|
||||
:param time: frequency when the callback is executed
|
||||
"""
|
||||
|
||||
periodic_callback = zmq.eventloop.ioloop.PeriodicCallback(callback, time, self._ioloop)
|
||||
return periodic_callback
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Starts the event loop
|
||||
"""
|
||||
|
||||
def signal_handler(signum=None, frame=None):
|
||||
log.warning("Module {} got signal {}, exiting...".format(self.name, signum))
|
||||
self.stop(signum)
|
||||
|
||||
signals = [signal.SIGTERM, signal.SIGINT]
|
||||
if not sys.platform.startswith("win"):
|
||||
signals.extend([signal.SIGHUP, signal.SIGQUIT])
|
||||
else:
|
||||
signals.extend([signal.SIGBREAK])
|
||||
for sig in signals:
|
||||
signal.signal(sig, signal_handler)
|
||||
|
||||
log.info("{} module running with PID {}".format(self.name, self.pid))
|
||||
self._setup()
|
||||
try:
|
||||
self._ioloop.start()
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
log.info("{} module has stopped".format(self.name))
|
||||
|
||||
def _shutdown(self):
|
||||
"""
|
||||
Shutdowns the I/O loop and the ZeroMQ stream & socket
|
||||
"""
|
||||
|
||||
self._ioloop.stop()
|
||||
|
||||
if self._stream and not self._stream.closed:
|
||||
# close the zeroMQ stream
|
||||
self._stream.close()
|
||||
|
||||
if self._dealer and not self._dealer.closed:
|
||||
# close the ZeroMQ dealer socket
|
||||
self._dealer.close()
|
||||
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
Adds a callback to stop the event loop & ZeroMQ.
|
||||
|
||||
:param signum: signal number (if called by the signal handler)
|
||||
"""
|
||||
|
||||
if not self._stopping:
|
||||
self._stopping = True
|
||||
if signum:
|
||||
self._ioloop.add_callback_from_signal(self._shutdown)
|
||||
else:
|
||||
self._ioloop.add_callback(self._shutdown)
|
||||
|
||||
def send_response(self, results):
|
||||
"""
|
||||
Sends a response back to the requester.
|
||||
|
||||
:param results: JSON results to the ZeroMQ server
|
||||
"""
|
||||
|
||||
jsonrpc_response = jsonrpc.JSONRPCResponse(results, self._current_call_id)()
|
||||
|
||||
# add session to the response
|
||||
response = [self._current_session, jsonrpc_response]
|
||||
log.debug("ZeroMQ client ({}) sending: {}".format(self.name, response))
|
||||
self._stream.send_json(response)
|
||||
|
||||
def send_param_error(self):
|
||||
"""
|
||||
Sends a param error back to the requester.
|
||||
"""
|
||||
|
||||
jsonrpc_response = jsonrpc.JSONRPCInvalidParams(self._current_call_id)()
|
||||
|
||||
# add session to the response
|
||||
response = [self._current_session, jsonrpc_response]
|
||||
log.info("ZeroMQ client ({}) sending JSON-RPC param error for call id {}".format(self.name, self._current_call_id))
|
||||
self._stream.send_json(response)
|
||||
|
||||
def send_internal_error(self):
|
||||
"""
|
||||
Sends a param error back to the requester.
|
||||
"""
|
||||
|
||||
jsonrpc_response = jsonrpc.JSONRPCInternalError()()
|
||||
|
||||
# add session to the response
|
||||
response = [self._current_session, jsonrpc_response]
|
||||
log.critical("ZeroMQ client ({}) sending JSON-RPC internal error".format(self.name))
|
||||
self._stream.send_json(response)
|
||||
|
||||
def send_custom_error(self, message, code=-3200):
|
||||
"""
|
||||
Sends a custom error back to the requester.
|
||||
"""
|
||||
|
||||
jsonrpc_response = jsonrpc.JSONRPCCustomError(code, message, self._current_call_id)()
|
||||
|
||||
# add session to the response
|
||||
response = [self._current_session, jsonrpc_response]
|
||||
log.info("ZeroMQ client ({}) sending JSON-RPC custom error: {} for call id {}".format(self.name,
|
||||
message,
|
||||
self._current_call_id))
|
||||
self._stream.send_json(response)
|
||||
|
||||
def send_notification(self, destination, results):
|
||||
"""
|
||||
Sends a notification
|
||||
|
||||
:param destination: destination (or method)
|
||||
:param results: JSON results to the ZeroMQ router
|
||||
"""
|
||||
|
||||
jsonrpc_response = jsonrpc.JSONRPCNotification(destination, results)()
|
||||
|
||||
# add session to the response
|
||||
response = [self._current_session, jsonrpc_response]
|
||||
log.debug("ZeroMQ client ({}) sending: {}".format(self.name, response))
|
||||
self._stream.send_json(response)
|
||||
|
||||
def _decode_request(self, request):
|
||||
"""
|
||||
Decodes the request to JSON.
|
||||
|
||||
:param request: request from ZeroMQ server
|
||||
"""
|
||||
|
||||
# server is shutting down, do not process
|
||||
# more request
|
||||
if self._stopping:
|
||||
return
|
||||
|
||||
# handle special request to stop the module
|
||||
# e.g. useful on Windows where the
|
||||
# SIGBREAK signal cound't be propagated
|
||||
if request[0] == b"stop":
|
||||
self.stop()
|
||||
return
|
||||
|
||||
try:
|
||||
request = zmq.utils.jsonapi.loads(request[0])
|
||||
except ValueError:
|
||||
self._current_session = None
|
||||
self.send_internal_error()
|
||||
return
|
||||
|
||||
log.debug("ZeroMQ client ({}) received: {}".format(self.name, request))
|
||||
self._current_session = request[0]
|
||||
self._current_call_id = request[1].get("id")
|
||||
destination = request[1].get("method")
|
||||
params = request[1].get("params")
|
||||
|
||||
if destination not in self.modules[self.name]:
|
||||
self.send_internal_error()
|
||||
return
|
||||
|
||||
log.debug("Routing request to {}: {}".format(destination, request[1]))
|
||||
|
||||
try:
|
||||
self.modules[self.name][destination](self, params)
|
||||
except Exception as e:
|
||||
log.error("uncaught exception {type}".format(type=type(e)), exc_info=1)
|
||||
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||
lines = traceback.format_exception(exc_type, exc_value, exc_tb)
|
||||
tb = "".join(lines)
|
||||
self.send_custom_error("uncaught exception {type}: {string}\n{tb}".format(type=type(e),
|
||||
string=str(e),
|
||||
tb=tb))
|
||||
|
||||
def validate_request(self, request, schema):
|
||||
"""
|
||||
Validates a request.
|
||||
|
||||
:param request: request (JSON-RPC params)
|
||||
:param schema: JSON-SCHEMA to validate the request
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
# check if we have a request
|
||||
if request is None:
|
||||
self.send_param_error()
|
||||
return False
|
||||
log.debug("received request {}".format(request))
|
||||
|
||||
# validate the request
|
||||
try:
|
||||
validate(request, schema)
|
||||
except ValidationError as e:
|
||||
self.send_custom_error("request validation error: {}".format(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def destinations(self):
|
||||
"""
|
||||
Destinations handled by this module.
|
||||
|
||||
:returns: list of destinations
|
||||
"""
|
||||
|
||||
if not self.name in self.modules:
|
||||
log.warn("no destinations found for module {}".format(self.name))
|
||||
return []
|
||||
return self.modules[self.name].keys()
|
||||
#TODO: make this more generic (not json but *args?)
|
||||
class BaseModule(object):
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def route(cls, destination):
|
||||
"""
|
||||
Decorator to register a destination routed to a method
|
||||
def instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
asyncio.async(cls._instance.run())
|
||||
return cls._instance
|
||||
|
||||
:param destination: destination to be routed
|
||||
"""
|
||||
def __init__(self):
|
||||
self._queue = asyncio.Queue()
|
||||
|
||||
def wrapper(method):
|
||||
module = destination.split(".")[0]
|
||||
if not module in cls.modules:
|
||||
cls.modules[module] = {}
|
||||
cls.modules[module][destination] = method
|
||||
return method
|
||||
return wrapper
|
||||
@classmethod
|
||||
def destroy(cls):
|
||||
future = asyncio.Future()
|
||||
cls._instance._queue.put_nowait((future, None, ))
|
||||
yield from asyncio.wait([future])
|
||||
cls._instance = None
|
||||
|
||||
@property
|
||||
def images_directory(self):
|
||||
@asyncio.coroutine
|
||||
def put(self, json):
|
||||
|
||||
return self._images_dir
|
||||
future = asyncio.Future()
|
||||
self._queue.put_nowait((future, json, ))
|
||||
yield from asyncio.wait([future])
|
||||
return future.result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def run(self):
|
||||
|
||||
while True:
|
||||
future, json = yield from self._queue.get()
|
||||
if json is None:
|
||||
future.set_result(True)
|
||||
break
|
||||
try:
|
||||
result = yield from self.process(json)
|
||||
future.set_result(result)
|
||||
except Exception as e:
|
||||
future.set_exception(e)
|
||||
|
||||
@asyncio.coroutine
|
||||
def process(self, json):
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,652 @@
|
||||
# -*- 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 base64
|
||||
import socket
|
||||
import shutil
|
||||
|
||||
from gns3server.modules import IModule
|
||||
from gns3server.config import Config
|
||||
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
|
||||
from .schemas import VPCS_EXPORT_CONFIG_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_path")
|
||||
if not self._vpcs or not os.path.isfile(self._vpcs):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for VPCS in the current working directory and $PATH
|
||||
for path in paths:
|
||||
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 = vpcs_config.get("console_start_port_range", 4501)
|
||||
self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501)
|
||||
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
|
||||
self._host = vpcs_config.get("host", kwargs["host"])
|
||||
self._console_host = vpcs_config.get("console_host", kwargs["console_host"])
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
Properly stops the module.
|
||||
|
||||
:param signum: signal number (if called by the signal handler)
|
||||
"""
|
||||
|
||||
# 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 get_vpcs_instance(self, vpcs_id):
|
||||
"""
|
||||
Returns a 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._allocated_udp_ports.clear()
|
||||
|
||||
self._working_dir = self._projects_dir
|
||||
log.info("VPCS module has been reset")
|
||||
|
||||
@IModule.route("vpcs.settings")
|
||||
def settings(self, request):
|
||||
"""
|
||||
Set or update settings.
|
||||
|
||||
Optional request parameters:
|
||||
- path (path to vpcs)
|
||||
- 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 is None:
|
||||
self.send_param_error()
|
||||
return
|
||||
|
||||
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"])
|
||||
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 = os.path.join(self._working_dir, "vpcs", "pc-{}".format(vpcs_instance.id))
|
||||
|
||||
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))
|
||||
|
||||
@IModule.route("vpcs.create")
|
||||
def vpcs_create(self, request):
|
||||
"""
|
||||
Creates a new VPCS instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- name (VPCS name)
|
||||
|
||||
Optional request parameters:
|
||||
- console (VPCS console port)
|
||||
|
||||
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 = request["name"]
|
||||
console = request.get("console")
|
||||
vpcs_id = request.get("vpcs_id")
|
||||
|
||||
try:
|
||||
|
||||
if not self._vpcs:
|
||||
raise VPCSError("No path to a VPCS executable has been set")
|
||||
|
||||
vpcs_instance = VPCSDevice(name,
|
||||
self._vpcs,
|
||||
self._working_dir,
|
||||
vpcs_id,
|
||||
console,
|
||||
self._console_host,
|
||||
self._console_start_port_range,
|
||||
self._console_end_port_range)
|
||||
|
||||
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 a 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.clean_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 a VPCS instance
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (VPCS instance identifier)
|
||||
|
||||
Optional request parameters:
|
||||
- any setting to update
|
||||
- script_file_base64 (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
|
||||
|
||||
config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc")
|
||||
try:
|
||||
if "script_file_base64" in request:
|
||||
# a new startup-config has been pushed
|
||||
config = base64.decodebytes(request["script_file_base64"].encode("utf-8")).decode("utf-8")
|
||||
config = config.replace("\r", "")
|
||||
config = config.replace('%h', vpcs_instance.name)
|
||||
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 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", errors="replace") 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(request["script_file"]))
|
||||
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:
|
||||
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 vpcs_start(self, request):
|
||||
"""
|
||||
Starts a 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:
|
||||
vpcs_instance.start()
|
||||
except VPCSError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("vpcs.stop")
|
||||
def vpcs_stop(self, request):
|
||||
"""
|
||||
Stops a 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 vpcs_reload(self, request):
|
||||
"""
|
||||
Reloads a 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:
|
||||
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,
|
||||
"port_id": request["port_id"]}
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("vpcs.add_nio")
|
||||
def add_nio(self, request):
|
||||
"""
|
||||
Adds an NIO (Network Input/Output) for a VPCS instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (VPCS instance identifier)
|
||||
- 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
|
||||
|
||||
port = request["port"]
|
||||
try:
|
||||
nio = None
|
||||
if request["nio"]["type"] == "nio_udp":
|
||||
lport = request["nio"]["lport"]
|
||||
rhost = request["nio"]["rhost"]
|
||||
rport = request["nio"]["rport"]
|
||||
try:
|
||||
#TODO: handle IPv6
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.connect((rhost, rport))
|
||||
except OSError as e:
|
||||
raise VPCSError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
nio = NIO_UDP(lport, rhost, rport)
|
||||
elif request["nio"]["type"] == "nio_tap":
|
||||
tap_device = request["nio"]["tap_device"]
|
||||
if not self.has_privileged_access(self._vpcs):
|
||||
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"]))
|
||||
except VPCSError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
try:
|
||||
vpcs_instance.port_add_nio_binding(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)
|
||||
- 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
|
||||
|
||||
port = request["port"]
|
||||
try:
|
||||
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
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("vpcs.export_config")
|
||||
def export_config(self, request):
|
||||
"""
|
||||
Exports the script file from a VPCS instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- script_file_base64 (script file base64 encoded)
|
||||
- False if no configuration can be exported
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, VPCS_EXPORT_CONFIG_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
vpcs_instance = self.get_vpcs_instance(request["id"])
|
||||
if not vpcs_instance:
|
||||
return
|
||||
|
||||
response = {}
|
||||
script_file_path = os.path.join(vpcs_instance.working_dir, vpcs_instance.script_file)
|
||||
try:
|
||||
with open(script_file_path, "rb") as f:
|
||||
config = f.read()
|
||||
response["script_file_base64"] = base64.encodebytes(config).decode("utf-8")
|
||||
except OSError as e:
|
||||
self.send_custom_error("unable to export the script file: {}".format(e))
|
||||
return
|
||||
|
||||
if not response:
|
||||
self.send_response(False)
|
||||
else:
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("vpcs.echo")
|
||||
def echo(self, request):
|
||||
"""
|
||||
Echo end point for testing purposes.
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request is None:
|
||||
self.send_param_error()
|
||||
else:
|
||||
log.debug("received request {}".format(request))
|
||||
self.send_response(request)
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2015 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
|
||||
@@ -19,634 +19,19 @@
|
||||
VPCS server module.
|
||||
"""
|
||||
|
||||
import os
|
||||
import base64
|
||||
import socket
|
||||
import shutil
|
||||
import asyncio
|
||||
|
||||
from gns3server.modules import IModule
|
||||
from gns3server.config import Config
|
||||
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 ..base import BaseModule
|
||||
|
||||
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
|
||||
from .schemas import VPCS_EXPORT_CONFIG_SCHEMA
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
class VPCS(BaseModule):
|
||||
|
||||
@asyncio.coroutine
|
||||
def process(self, json):
|
||||
yield from asyncio.sleep(1)
|
||||
return 42
|
||||
|
||||
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_path")
|
||||
if not self._vpcs or not os.path.isfile(self._vpcs):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for VPCS in the current working directory and $PATH
|
||||
for path in paths:
|
||||
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 = vpcs_config.get("console_start_port_range", 4501)
|
||||
self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501)
|
||||
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
|
||||
self._host = vpcs_config.get("host", kwargs["host"])
|
||||
self._console_host = vpcs_config.get("console_host", kwargs["console_host"])
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
Properly stops the module.
|
||||
|
||||
:param signum: signal number (if called by the signal handler)
|
||||
"""
|
||||
|
||||
# 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 get_vpcs_instance(self, vpcs_id):
|
||||
"""
|
||||
Returns a 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._allocated_udp_ports.clear()
|
||||
|
||||
self._working_dir = self._projects_dir
|
||||
log.info("VPCS module has been reset")
|
||||
|
||||
@IModule.route("vpcs.settings")
|
||||
def settings(self, request):
|
||||
"""
|
||||
Set or update settings.
|
||||
|
||||
Optional request parameters:
|
||||
- path (path to vpcs)
|
||||
- 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 is None:
|
||||
self.send_param_error()
|
||||
return
|
||||
|
||||
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"])
|
||||
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 = os.path.join(self._working_dir, "vpcs", "pc-{}".format(vpcs_instance.id))
|
||||
|
||||
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))
|
||||
|
||||
@IModule.route("vpcs.create")
|
||||
def vpcs_create(self, request):
|
||||
"""
|
||||
Creates a new VPCS instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- name (VPCS name)
|
||||
|
||||
Optional request parameters:
|
||||
- console (VPCS console port)
|
||||
|
||||
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 = request["name"]
|
||||
console = request.get("console")
|
||||
vpcs_id = request.get("vpcs_id")
|
||||
|
||||
try:
|
||||
|
||||
if not self._vpcs:
|
||||
raise VPCSError("No path to a VPCS executable has been set")
|
||||
|
||||
vpcs_instance = VPCSDevice(name,
|
||||
self._vpcs,
|
||||
self._working_dir,
|
||||
vpcs_id,
|
||||
console,
|
||||
self._console_host,
|
||||
self._console_start_port_range,
|
||||
self._console_end_port_range)
|
||||
|
||||
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 a 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.clean_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 a VPCS instance
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (VPCS instance identifier)
|
||||
|
||||
Optional request parameters:
|
||||
- any setting to update
|
||||
- script_file_base64 (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
|
||||
|
||||
config_path = os.path.join(vpcs_instance.working_dir, "startup.vpc")
|
||||
try:
|
||||
if "script_file_base64" in request:
|
||||
# a new startup-config has been pushed
|
||||
config = base64.decodebytes(request["script_file_base64"].encode("utf-8")).decode("utf-8")
|
||||
config = config.replace("\r", "")
|
||||
config = config.replace('%h', vpcs_instance.name)
|
||||
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 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", errors="replace") 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(request["script_file"]))
|
||||
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:
|
||||
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 vpcs_start(self, request):
|
||||
"""
|
||||
Starts a 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:
|
||||
vpcs_instance.start()
|
||||
except VPCSError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("vpcs.stop")
|
||||
def vpcs_stop(self, request):
|
||||
"""
|
||||
Stops a 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 vpcs_reload(self, request):
|
||||
"""
|
||||
Reloads a 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:
|
||||
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,
|
||||
"port_id": request["port_id"]}
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("vpcs.add_nio")
|
||||
def add_nio(self, request):
|
||||
"""
|
||||
Adds an NIO (Network Input/Output) for a VPCS instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (VPCS instance identifier)
|
||||
- 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
|
||||
|
||||
port = request["port"]
|
||||
try:
|
||||
nio = None
|
||||
if request["nio"]["type"] == "nio_udp":
|
||||
lport = request["nio"]["lport"]
|
||||
rhost = request["nio"]["rhost"]
|
||||
rport = request["nio"]["rport"]
|
||||
try:
|
||||
#TODO: handle IPv6
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.connect((rhost, rport))
|
||||
except OSError as e:
|
||||
raise VPCSError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
nio = NIO_UDP(lport, rhost, rport)
|
||||
elif request["nio"]["type"] == "nio_tap":
|
||||
tap_device = request["nio"]["tap_device"]
|
||||
if not self.has_privileged_access(self._vpcs):
|
||||
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"]))
|
||||
except VPCSError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
try:
|
||||
vpcs_instance.port_add_nio_binding(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)
|
||||
- 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
|
||||
|
||||
port = request["port"]
|
||||
try:
|
||||
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
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("vpcs.export_config")
|
||||
def export_config(self, request):
|
||||
"""
|
||||
Exports the script file from a VPCS instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (vm identifier)
|
||||
|
||||
Response parameters:
|
||||
- script_file_base64 (script file base64 encoded)
|
||||
- False if no configuration can be exported
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, VPCS_EXPORT_CONFIG_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
vpcs_instance = self.get_vpcs_instance(request["id"])
|
||||
if not vpcs_instance:
|
||||
return
|
||||
|
||||
response = {}
|
||||
script_file_path = os.path.join(vpcs_instance.working_dir, vpcs_instance.script_file)
|
||||
try:
|
||||
with open(script_file_path, "rb") as f:
|
||||
config = f.read()
|
||||
response["script_file_base64"] = base64.encodebytes(config).decode("utf-8")
|
||||
except OSError as e:
|
||||
self.send_custom_error("unable to export the script file: {}".format(e))
|
||||
return
|
||||
|
||||
if not response:
|
||||
self.send_response(False)
|
||||
else:
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("vpcs.echo")
|
||||
def echo(self, request):
|
||||
"""
|
||||
Echo end point for testing purposes.
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
if request is None:
|
||||
self.send_param_error()
|
||||
else:
|
||||
log.debug("received request {}".format(request))
|
||||
self.send_response(request)
|
||||
@asyncio.coroutine
|
||||
def create(self, json):
|
||||
i = yield from self.put(json)
|
||||
return i
|
||||
Reference in new issue
Block a user