mirror of
https://github.com/GNS3/gns3-server.git
synced 2024-11-16 08:44:52 +02:00
Polishing base server implementation
This commit is contained in:
parent
f4e51ea74f
commit
2f23a092e3
0
gns3server/handlers/__init__.py
Normal file
0
gns3server/handlers/__init__.py
Normal file
@ -22,10 +22,10 @@ STOMP protocol over Websockets
|
||||
import zmq
|
||||
import uuid
|
||||
import tornado.websocket
|
||||
from .version import __version__
|
||||
from tornado.escape import json_decode
|
||||
from .stomp import frame as stomp_frame
|
||||
from .stomp import protocol as stomp_protocol
|
||||
from ..version import __version__
|
||||
from ..stomp import frame as stomp_frame
|
||||
from ..stomp import protocol as stomp_protocol
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -48,12 +48,15 @@ class StompWebSocket(tornado.websocket.WebSocketHandler):
|
||||
def __init__(self, application, request, zmq_router):
|
||||
tornado.websocket.WebSocketHandler.__init__(self, application, request)
|
||||
self._session_id = str(uuid.uuid4())
|
||||
self._connected = False
|
||||
self.zmq_router = zmq_router
|
||||
|
||||
@property
|
||||
def session_id(self):
|
||||
"""
|
||||
Session ID uniquely representing a Websocket client
|
||||
|
||||
:returns: the session id
|
||||
"""
|
||||
|
||||
return self._session_id
|
||||
@ -63,7 +66,7 @@ class StompWebSocket(tornado.websocket.WebSocketHandler):
|
||||
"""
|
||||
Sends a message to Websocket client
|
||||
|
||||
:param message: message from a module
|
||||
:param message: message from a module (received via ZeroMQ)
|
||||
"""
|
||||
|
||||
# Module name that is replying
|
||||
@ -117,6 +120,7 @@ class StompWebSocket(tornado.websocket.WebSocketHandler):
|
||||
else:
|
||||
self.write_message(self.stomp.connected(self.session_id,
|
||||
'gns3server/' + __version__))
|
||||
self._connected = True
|
||||
|
||||
def stomp_handle_send(self, frame):
|
||||
"""
|
||||
@ -212,6 +216,11 @@ class StompWebSocket(tornado.websocket.WebSocketHandler):
|
||||
if frame.cmd == stomp_protocol.CMD_STOMP or frame.cmd == stomp_protocol.CMD_CONNECT:
|
||||
self.stomp_handle_connect(frame)
|
||||
|
||||
# Do not enforce that the client must have send a
|
||||
# STOMP CONNECT frame for now (need to refactor unit tests)
|
||||
#elif not self._connected:
|
||||
# self.stomp_error("Not connected")
|
||||
|
||||
elif frame.cmd == stomp_protocol.CMD_SEND:
|
||||
self.stomp_handle_send(frame)
|
||||
|
26
gns3server/handlers/version_handler.py
Normal file
26
gns3server/handlers/version_handler.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import tornado.web
|
||||
from ..version import __version__
|
||||
|
||||
|
||||
class VersionHandler(tornado.web.RequestHandler):
|
||||
|
||||
def get(self):
|
||||
response = {'version': __version__}
|
||||
self.write(response)
|
@ -47,7 +47,6 @@ class Module(object):
|
||||
def name(self, new_name):
|
||||
self._name = new_name
|
||||
|
||||
#@property
|
||||
def cls(self):
|
||||
return self._cls
|
||||
|
||||
@ -93,7 +92,7 @@ class ModuleManager(object):
|
||||
"""
|
||||
Returns all modules.
|
||||
|
||||
:return: list of Module objects
|
||||
:returns: list of Module objects
|
||||
"""
|
||||
|
||||
return self._modules
|
||||
@ -105,7 +104,8 @@ class ModuleManager(object):
|
||||
:param module: module to activate (Module object)
|
||||
:param args: args passed to the module
|
||||
:param kwargs: kwargs passed to the module
|
||||
:return: instantiated module class
|
||||
|
||||
:returns: instantiated module class
|
||||
"""
|
||||
|
||||
module_class = module.cls()
|
||||
|
@ -15,6 +15,10 @@
|
||||
# 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 multiprocessing
|
||||
import zmq
|
||||
|
||||
@ -44,16 +48,16 @@ class IModule(multiprocessing.Process):
|
||||
self._current_session = None
|
||||
self._current_destination = None
|
||||
|
||||
def setup(self):
|
||||
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._host, self._port, self.decode_request)
|
||||
self._stream = self._create_stream(self._host, self._port, self._decode_request)
|
||||
|
||||
def create_stream(self, host=None, port=0, callback=None):
|
||||
def _create_stream(self, host=None, port=0, callback=None):
|
||||
"""
|
||||
Creates a new ZMQ stream
|
||||
"""
|
||||
@ -82,10 +86,10 @@ class IModule(multiprocessing.Process):
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Sets up everything and starts the event loop
|
||||
Starts the event loop
|
||||
"""
|
||||
|
||||
self.setup()
|
||||
self._setup()
|
||||
try:
|
||||
self._ioloop.start()
|
||||
except KeyboardInterrupt:
|
||||
@ -102,6 +106,8 @@ class IModule(multiprocessing.Process):
|
||||
def send_response(self, response):
|
||||
"""
|
||||
Sends a response back to the requester
|
||||
|
||||
:param response:
|
||||
"""
|
||||
|
||||
# add session and destination to the response
|
||||
@ -109,9 +115,11 @@ class IModule(multiprocessing.Process):
|
||||
log.debug("ZeroMQ client ({}) sending: {}".format(self.name, response))
|
||||
self._stream.send_json(response)
|
||||
|
||||
def decode_request(self, request):
|
||||
def _decode_request(self, request):
|
||||
"""
|
||||
Decodes the request to JSON
|
||||
|
||||
:param request: request from ZeroMQ server
|
||||
"""
|
||||
|
||||
try:
|
||||
@ -132,7 +140,9 @@ class IModule(multiprocessing.Process):
|
||||
|
||||
def destinations(self):
|
||||
"""
|
||||
Channels handled by this modules.
|
||||
Destinations handled by this module.
|
||||
|
||||
:returns: list of destinations
|
||||
"""
|
||||
|
||||
return self.destination.keys()
|
||||
@ -141,6 +151,8 @@ class IModule(multiprocessing.Process):
|
||||
def route(cls, destination):
|
||||
"""
|
||||
Decorator to register a destination routed to a method
|
||||
|
||||
:param destination: destination to be routed
|
||||
"""
|
||||
|
||||
def wrapper(method):
|
||||
|
50
gns3server/modules/dynamips/__init__.py
Normal file
50
gns3server/modules/dynamips/__init__.py
Normal file
@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2013 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from gns3server.modules import IModule
|
||||
from .hypervisor import Hypervisor
|
||||
from .hypervisor_manager import HypervisorManager
|
||||
from .dynamips_error import DynamipsError
|
||||
from .nodes.router import Router
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Dynamips(IModule):
|
||||
|
||||
def __init__(self, name=None, args=(), kwargs={}):
|
||||
IModule.__init__(self, name=name, args=args, kwargs=kwargs)
|
||||
#self._hypervisor_manager = HypervisorManager("/usr/bin/dynamips", "/tmp")
|
||||
|
||||
@IModule.route("dynamips/echo")
|
||||
def echo(self, request):
|
||||
print("Echo!")
|
||||
log.debug("received request {}".format(request))
|
||||
self.send_response(request)
|
||||
|
||||
@IModule.route("dynamips/create_vm")
|
||||
def create_vm(self, request):
|
||||
print("Create VM!")
|
||||
log.debug("received request {}".format(request))
|
||||
self.send_response(request)
|
||||
|
||||
@IModule.route("dynamips/start_vm")
|
||||
def start_vm(self, request):
|
||||
print("Start VM!")
|
||||
log.debug("received request {}".format(request))
|
||||
self.send_response(request)
|
@ -15,31 +15,29 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Set up and run the server
|
||||
"""
|
||||
|
||||
import zmq
|
||||
from zmq.eventloop import ioloop, zmqstream
|
||||
ioloop.install()
|
||||
|
||||
import os
|
||||
import errno
|
||||
import functools
|
||||
import socket
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import tornado.autoreload
|
||||
from .version import __version__
|
||||
from .stomp_websocket import StompWebSocket
|
||||
from .handlers.stomp_websocket import StompWebSocket
|
||||
from .handlers.version_handler import VersionHandler
|
||||
from .module_manager import ModuleManager
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersionHandler(tornado.web.RequestHandler):
|
||||
|
||||
def get(self):
|
||||
response = {'version': __version__}
|
||||
self.write(response)
|
||||
|
||||
|
||||
class Server(object):
|
||||
|
||||
# built-in handlers
|
||||
@ -57,7 +55,8 @@ class Server(object):
|
||||
self._modules = []
|
||||
|
||||
def load_modules(self):
|
||||
"""Loads the plugins
|
||||
"""
|
||||
Loads the modules
|
||||
"""
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
@ -86,7 +85,7 @@ class Server(object):
|
||||
print("Starting server on port {}".format(self._port))
|
||||
tornado_app.listen(self._port)
|
||||
except socket.error as e:
|
||||
if e.errno is 48: # socket already in use
|
||||
if e.errno == errno.EADDRINUSE: # socket already in use
|
||||
logging.critical("socket in use for port {}".format(self._port))
|
||||
raise SystemExit
|
||||
|
||||
@ -106,7 +105,7 @@ class Server(object):
|
||||
Creates the ZeroMQ router socket to send
|
||||
requests to modules.
|
||||
|
||||
:returns: ZeroMQ socket
|
||||
:returns: ZeroMQ router socket
|
||||
"""
|
||||
|
||||
context = zmq.Context()
|
||||
|
@ -106,6 +106,7 @@ class Frame(object):
|
||||
|
||||
:param lines: Frame preamble lines
|
||||
:param offset: To start parsing at the given offset
|
||||
|
||||
:returns: Headers in dict header:value
|
||||
"""
|
||||
|
||||
@ -124,6 +125,7 @@ class Frame(object):
|
||||
Parses a frame
|
||||
|
||||
:params frame: The frame data to be parsed
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
|
@ -85,6 +85,7 @@ class serverProtocol(object):
|
||||
|
||||
:param session: A session identifier that uniquely identifies the session.
|
||||
:param server: A field that contains information about the STOMP server.
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
@ -109,6 +110,7 @@ class serverProtocol(object):
|
||||
:param body: Data to be added in the frame body
|
||||
:param content_type: MIME type which describes the format of the body
|
||||
:param message_id: Unique identifier for that message
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
@ -133,6 +135,7 @@ class serverProtocol(object):
|
||||
Sends an acknowledgment for client frame that requests a receipt.
|
||||
|
||||
:param receipt_id: Receipt ID to send back to the client
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
@ -147,6 +150,7 @@ class serverProtocol(object):
|
||||
:param message: Short description of the error
|
||||
:param body: Detailed information
|
||||
:param content_type: MIME type which describes the format of the body
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
@ -176,6 +180,7 @@ class clientProtocol(object):
|
||||
|
||||
:param host: Host name that the socket was established against.
|
||||
:param accept_version: The versions of the STOMP protocol the client supports.
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
@ -195,6 +200,7 @@ class clientProtocol(object):
|
||||
Disconnects to a STOMP server.
|
||||
|
||||
:param receipt: unique identifier
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
@ -211,6 +217,7 @@ class clientProtocol(object):
|
||||
:param destination: Destination string
|
||||
:param body: Data to be added in the frame body
|
||||
:param content_type: MIME type which describes the format of the body
|
||||
|
||||
:returns: STOMP Frame object
|
||||
"""
|
||||
|
||||
|
@ -7,6 +7,9 @@ import time
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def server(request):
|
||||
"""
|
||||
Starts GNS3 server for all the tests.
|
||||
"""
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
server_script = os.path.join(cwd, "../gns3server/main.py")
|
||||
|
@ -5,6 +5,10 @@ from ws4py.client.tornadoclient import TornadoWebSocketClient
|
||||
from gns3server.stomp import frame as stomp_frame
|
||||
from gns3server.stomp import protocol as stomp_protocol
|
||||
|
||||
"""
|
||||
Tests STOMP protocol over Websockets
|
||||
"""
|
||||
|
||||
|
||||
class Stomp(AsyncTestCase):
|
||||
|
||||
@ -16,6 +20,10 @@ class Stomp(AsyncTestCase):
|
||||
AsyncTestCase.setUp(self)
|
||||
|
||||
def test_connect(self):
|
||||
"""
|
||||
Sends a STOMP CONNECT frame and
|
||||
check for a STOMP CONNECTED frame.
|
||||
"""
|
||||
|
||||
request = self.stomp.connect("localhost")
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, request)
|
||||
@ -25,6 +33,11 @@ class Stomp(AsyncTestCase):
|
||||
assert frame.cmd == stomp_protocol.CMD_CONNECTED
|
||||
|
||||
def test_protocol_negotiation_failure(self):
|
||||
"""
|
||||
Sends a STOMP CONNECT frame with protocol version 1.0 required
|
||||
and check for a STOMP ERROR sent back by the server which supports
|
||||
STOMP version 1.2 only.
|
||||
"""
|
||||
|
||||
request = self.stomp.connect("localhost", accept_version='1.0')
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, request)
|
||||
@ -34,6 +47,9 @@ class Stomp(AsyncTestCase):
|
||||
assert frame.cmd == stomp_protocol.CMD_ERROR
|
||||
|
||||
def test_malformed_frame(self):
|
||||
"""
|
||||
Sends an empty frame and check for a STOMP ERROR.
|
||||
"""
|
||||
|
||||
request = b""
|
||||
AsyncWSRequest(self.URL, self.io_loop, self.stop, request)
|
||||
@ -43,6 +59,10 @@ class Stomp(AsyncTestCase):
|
||||
assert frame.cmd == stomp_protocol.CMD_ERROR
|
||||
|
||||
def test_send(self):
|
||||
"""
|
||||
Sends a STOMP SEND frame with a message and a destination
|
||||
and check for a STOMP MESSAGE with echoed message and destination.
|
||||
"""
|
||||
|
||||
destination = "dynamips/echo"
|
||||
message = {"ping": "test"}
|
||||
@ -57,6 +77,10 @@ class Stomp(AsyncTestCase):
|
||||
assert message == json_reply
|
||||
|
||||
def test_unimplemented_frame(self):
|
||||
"""
|
||||
Sends an STOMP BEGIN frame which is not implemented by the server
|
||||
and check for a STOMP ERROR frame.
|
||||
"""
|
||||
|
||||
frame = stomp_frame.Frame(stomp_protocol.CMD_BEGIN)
|
||||
request = frame.encode()
|
||||
@ -67,6 +91,11 @@ class Stomp(AsyncTestCase):
|
||||
assert frame.cmd == stomp_protocol.CMD_ERROR
|
||||
|
||||
def test_disconnect(self):
|
||||
"""
|
||||
Sends a STOMP DISCONNECT frame is a receipt id
|
||||
and check for a STOMP RECEIPT frame with the same receipt id
|
||||
confirming the disconnection.
|
||||
"""
|
||||
|
||||
myid = str(uuid.uuid4())
|
||||
request = self.stomp.disconnect(myid)
|
||||
@ -79,6 +108,9 @@ class Stomp(AsyncTestCase):
|
||||
|
||||
|
||||
class AsyncWSRequest(TornadoWebSocketClient):
|
||||
"""
|
||||
Very basic Websocket client for the tests
|
||||
"""
|
||||
|
||||
def __init__(self, url, io_loop, callback, message):
|
||||
TornadoWebSocketClient.__init__(self, url, io_loop=io_loop)
|
||||
|
@ -1,37 +1,40 @@
|
||||
from tornado.testing import AsyncHTTPTestCase
|
||||
from tornado.escape import json_decode
|
||||
from gns3server.server import VersionHandler
|
||||
from gns3server._compat import urlencode
|
||||
from gns3server.version import __version__
|
||||
import tornado.web
|
||||
import json
|
||||
|
||||
# URL to test
|
||||
URL = "/version"
|
||||
"""
|
||||
Tests for the web server version handler
|
||||
"""
|
||||
|
||||
|
||||
class TestVersionHandler(AsyncHTTPTestCase):
|
||||
|
||||
URL = "/version"
|
||||
|
||||
def get_app(self):
|
||||
return tornado.web.Application([(URL, VersionHandler)])
|
||||
|
||||
return tornado.web.Application([(self.URL, VersionHandler)])
|
||||
|
||||
def test_endpoint(self):
|
||||
self.http_client.fetch(self.get_url(URL), self.stop)
|
||||
"""
|
||||
Tests if the response HTTP code is 200 (success)
|
||||
"""
|
||||
|
||||
self.http_client.fetch(self.get_url(self.URL), self.stop)
|
||||
response = self.wait()
|
||||
assert response.code == 200
|
||||
|
||||
# def test_post(self):
|
||||
# data = urlencode({'test': 'works'})
|
||||
# req = tornado.httpclient.HTTPRequest(self.get_url(URL),
|
||||
# method='POST',
|
||||
# body=data)
|
||||
# self.http_client.fetch(req, self.stop)
|
||||
# response = self.wait()
|
||||
# assert response.code == 200
|
||||
#
|
||||
# def test_endpoint_differently(self):
|
||||
# self.http_client.fetch(self.get_url(URL), self.stop)
|
||||
# response = self.wait()
|
||||
# assert(response.headers['Content-Type'].startswith('application/json'))
|
||||
# assert(response.body != "")
|
||||
# body = json.loads(response.body.decode('utf-8'))
|
||||
# assert body['version'] == "0.1.dev"
|
||||
def test_received_version(self):
|
||||
"""
|
||||
Tests if the returned content type is JSON and
|
||||
if the received version is the same as the server
|
||||
"""
|
||||
|
||||
self.http_client.fetch(self.get_url(self.URL), self.stop)
|
||||
response = self.wait()
|
||||
assert(response.headers['Content-Type'].startswith('application/json'))
|
||||
assert(response.body)
|
||||
body = json_decode(response.body)
|
||||
assert body['version'] == __version__
|
||||
|
Loading…
Reference in New Issue
Block a user