diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/stomp_websocket.py b/gns3server/handlers/stomp_websocket.py
similarity index 93%
rename from gns3server/stomp_websocket.py
rename to gns3server/handlers/stomp_websocket.py
index a64ade86..cacf93cf 100644
--- a/gns3server/stomp_websocket.py
+++ b/gns3server/handlers/stomp_websocket.py
@@ -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)
diff --git a/gns3server/handlers/version_handler.py b/gns3server/handlers/version_handler.py
new file mode 100644
index 00000000..c85aa31c
--- /dev/null
+++ b/gns3server/handlers/version_handler.py
@@ -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 .
+
+import tornado.web
+from ..version import __version__
+
+
+class VersionHandler(tornado.web.RequestHandler):
+
+ def get(self):
+ response = {'version': __version__}
+ self.write(response)
diff --git a/gns3server/module_manager.py b/gns3server/module_manager.py
index d3ce8a21..14bbd440 100644
--- a/gns3server/module_manager.py
+++ b/gns3server/module_manager.py
@@ -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()
diff --git a/gns3server/modules/base.py b/gns3server/modules/base.py
index 25233ab5..7ec78040 100644
--- a/gns3server/modules/base.py
+++ b/gns3server/modules/base.py
@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+"""
+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):
diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py
new file mode 100644
index 00000000..83261edc
--- /dev/null
+++ b/gns3server/modules/dynamips/__init__.py
@@ -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 .
+
+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)
diff --git a/gns3server/server.py b/gns3server/server.py
index 594e6d87..59e3135b 100644
--- a/gns3server/server.py
+++ b/gns3server/server.py
@@ -15,31 +15,29 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+"""
+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()
diff --git a/gns3server/stomp/frame.py b/gns3server/stomp/frame.py
index 61b86124..90d83e48 100644
--- a/gns3server/stomp/frame.py
+++ b/gns3server/stomp/frame.py
@@ -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
"""
diff --git a/gns3server/stomp/protocol.py b/gns3server/stomp/protocol.py
index ad2db09d..0491c1bf 100644
--- a/gns3server/stomp/protocol.py
+++ b/gns3server/stomp/protocol.py
@@ -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
"""
diff --git a/tests/conftest.py b/tests/conftest.py
index f9ae1cf7..fda6fe57 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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")
diff --git a/tests/test_stomp.py b/tests/test_stomp.py
index ac793107..443d4a1d 100644
--- a/tests/test_stomp.py
+++ b/tests/test_stomp.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)
diff --git a/tests/test_version_handler.py b/tests/test_version_handler.py
index 19b09ed2..feeef3d3 100644
--- a/tests/test_version_handler.py
+++ b/tests/test_version_handler.py
@@ -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__