diff --git a/.travis.yml b/.travis.yml index bb9aceb5..07eee96b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,4 @@ notifications: - "chat.freenode.net#gns3" on_success: change on_failure: always - use_notice: true - skip_join: true diff --git a/MANIFEST.in b/MANIFEST.in index 9ca568b7..75d25e8a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,6 @@ include MANIFEST.in include tox.ini recursive-include tests * recursive-include docs * -recursive-include gns3_server * +recursive-include gns3server * recursive-exclude * __pycache__ recursive-exclude * *.py[co] diff --git a/gns3-server.py b/gns3-server.py deleted file mode 100644 index c321ee48..00000000 --- a/gns3-server.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- 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 . - -# Python 2.6 and 2.7 compatibility -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import sys -import tornado.ioloop -import tornado.web -import gns3server -from datetime import date - -class MainHandler(tornado.web.RequestHandler): - def get(self): - self.write("Ready to serve") - -application = tornado.web.Application([ - (r"/", MainHandler), -]) - -if __name__ == "__main__": - - print("GNS3 server version {0}".format(gns3server.__version__)) - print("Copyright (c) 2007-{0} GNS3 Technologies Inc.".format(date.today().year)) - - if sys.version_info < (2, 6): - raise RuntimeError("Python 2.6 or higher is required") - elif sys.version_info[0] == 3 and sys.version_info < (3, 3): - raise RuntimeError("Python 3.3 or higher is required") - - application.listen(8888) - tornado.ioloop.IOLoop.instance().start() - diff --git a/gns3server/__init__.py b/gns3server/__init__.py index d98d3e92..d57df556 100644 --- a/gns3server/__init__.py +++ b/gns3server/__init__.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -23,5 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) +from gns3server.plugin_manager import PluginManager +from gns3server.server import Server + __version__ = "0.1.dev" __version_info__ = (0, 1, 0, -99) diff --git a/gns3server/_compat.py b/gns3server/_compat.py index c866e3b1..78bd53f8 100644 --- a/gns3server/_compat.py +++ b/gns3server/_compat.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -26,6 +26,11 @@ if not PY2: string_types = (str,) else: unichr = unichr - text_type = unicode - range_type = xrange - string_types = (str, unicode) + text_type = unicode # @UndefinedVariable + range_type = xrange # @UndefinedVariable + string_types = (str, unicode) # @UndefinedVariable + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode diff --git a/gns3server/main.py b/gns3server/main.py new file mode 100644 index 00000000..bbd57346 --- /dev/null +++ b/gns3server/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- 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 datetime +import sys +import logging +import gns3server +import tornado.options + +# command line options +from tornado.options import define +define("port", default=8000, help="run on the given port", type=int) + + +def main(): + + current_year = datetime.date.today().year + print("GNS3 server version {}".format(gns3server.__version__)) + print("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) + + # we only support Python 2 version >= 2.7 and Python 3 version >= 3.3 + if sys.version_info < (2, 7): + raise RuntimeError("Python 2.7 or higher is required") + elif sys.version_info[0] == 3 and sys.version_info < (3, 3): + raise RuntimeError("Python 3.3 or higher is required") + + try: + tornado.options.parse_command_line() + except (tornado.options.Error, ValueError): + tornado.options.print_help() + raise SystemExit + + #FIXME: log everything for now (excepting DEBUG) + logging.basicConfig(level=logging.INFO) + + server = gns3server.Server() + server.load_plugins() + server.run() + + +if __name__ == '__main__': + main() diff --git a/gns3server/plugin_manager.py b/gns3server/plugin_manager.py new file mode 100644 index 00000000..f0f582f4 --- /dev/null +++ b/gns3server/plugin_manager.py @@ -0,0 +1,87 @@ +# -*- 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 imp +import inspect +import pkgutil +import logging +from gns3server.plugins import IPlugin + +logger = logging.getLogger(__name__) + + +class Plugin(object): + """Plugin representation for the PluginManager + """ + + def __init__(self, name, cls): + + self._name = name + self._cls = cls + + @property + def name(self): + + return self._name + + @name.setter + def name(self, new_name): + self._name = new_name + + #@property + def cls(self): + return self._cls + + +class PluginManager(object): + """Manages plugins + """ + + def __init__(self, plugin_paths=['plugins']): + + self._plugins = [] + self._plugin_paths = plugin_paths + + def load_plugins(self): + + for _, name, ispkg in pkgutil.iter_modules(self._plugin_paths): + if (ispkg): + logger.info("analyzing '{}' package".format(name)) + try: + file, pathname, description = imp.find_module(name, self._plugin_paths) + plugin_module = imp.load_module(name, file, pathname, description) + plugin_classes = inspect.getmembers(plugin_module, inspect.isclass) + for plugin_class in plugin_classes: + if issubclass(plugin_class[1], IPlugin): + # don't instantiate any parent plugins + if plugin_class[1].__module__ == name: + logger.info("loading '{}' plugin".format(plugin_class[0])) + info = Plugin(name=plugin_class[0], cls=plugin_class[1]) + self._plugins.append(info) + finally: + if file: + file.close() + + def get_all_plugins(self): + return self._plugins + + def activate_plugin(self, plugin): + + plugin_class = plugin.cls() + plugin_instance = plugin_class() + logger.info("'{}' plugin activated".format(plugin.name)) + return plugin_instance diff --git a/gns3server/plugins/__init__.py b/gns3server/plugins/__init__.py new file mode 100644 index 00000000..c76c9983 --- /dev/null +++ b/gns3server/plugins/__init__.py @@ -0,0 +1,18 @@ +# -*- 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.plugins.base import IPlugin diff --git a/gns3server/plugins/base.py b/gns3server/plugins/base.py new file mode 100644 index 00000000..6160fd1a --- /dev/null +++ b/gns3server/plugins/base.py @@ -0,0 +1,30 @@ +# -*- 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 . + + +class IPlugin(object): + """Plugin interface + """ + + def __init__(self): + pass + + def setup(self): + """Called before the plugin is asked to do anything + """ + + raise NotImplementedError() diff --git a/gns3server/plugins/dynamips/__init__.py b/gns3server/plugins/dynamips/__init__.py new file mode 100644 index 00000000..3d667e39 --- /dev/null +++ b/gns3server/plugins/dynamips/__init__.py @@ -0,0 +1,42 @@ +# -*- 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 logging +import tornado.web +from gns3server.plugins import IPlugin + +logger = logging.getLogger(__name__) + + +class TestHandler(tornado.web.RequestHandler): + def get(self): + self.write("This is my test handler") + + +class Dynamips(IPlugin): + + def __init__(self): + IPlugin.__init__(self) + logger.info("Dynamips plugin is initializing") + + def handlers(self): + """Returns tornado web request handlers that the plugin manages + + :returns: List of tornado.web.RequestHandler + """ + + return [(r"/test", TestHandler)] diff --git a/gns3server/server.py b/gns3server/server.py new file mode 100644 index 00000000..c46417ee --- /dev/null +++ b/gns3server/server.py @@ -0,0 +1,80 @@ +# -*- 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 logging +import socket +import tornado.ioloop +import tornado.web +import gns3server + +logger = logging.getLogger(__name__) + + +class MainHandler(tornado.web.RequestHandler): + + def get(self): + self.write("Welcome to the GNS3 server!") + + +class VersionHandler(tornado.web.RequestHandler): + + def get(self): + response = {'version': gns3server.__version__} + self.write(response) + + +class Server(object): + + # built-in handlers + handlers = [(r"/", MainHandler), + (r"/version", VersionHandler)] + + def __init__(self): + + self._plugins = [] + + def load_plugins(self): + """Loads the plugins + """ + + plugin_manager = gns3server.PluginManager() + plugin_manager.load_plugins() + for plugin in plugin_manager.get_all_plugins(): + instance = plugin_manager.activate_plugin(plugin) + self._plugins.append(instance) + plugin_handlers = instance.handlers() + self.handlers.extend(plugin_handlers) + + def run(self): + """Starts the tornado web server + """ + + from tornado.options import options + tornado_app = tornado.web.Application(self.handlers) + try: + port = options.port + print("Starting server on port {}".format(port)) + tornado_app.listen(port) + except socket.error as e: + if e.errno is 48: # socket already in use + logging.critical("socket in use for port {}".format(port)) + raise SystemExit + try: + tornado.ioloop.IOLoop.instance().start() + except (KeyboardInterrupt, SystemExit): + print("\nExiting...") + tornado.ioloop.IOLoop.instance().stop() diff --git a/requirements.txt b/requirements.txt index c43f03c4..ed626c89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -Yapsy==1.10.2-pythons2n3 -astroid==1.0.0 -logilab-common==0.60.0 -networkx==1.8.1 -tornado==3.1.1 +tornado +jsonschema +networkx diff --git a/setup.py b/setup.py index cf12b787..edeef1c1 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- # # Copyright (C) 2013 GNS3 Technologies Inc. # @@ -16,17 +16,17 @@ # along with this program. If not, see . import sys -import os from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -import gns3server class Tox(TestCommand): + def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True + def run_tests(self): #import here, cause outside the eggs aren't loaded import tox @@ -34,32 +34,41 @@ class Tox(TestCommand): sys.exit(errcode) setup( - name = 'gns3-server', - scripts = ['gns3-server.py'], - version = gns3server.__version__, - url = 'http://github.com/GNS3/gns3-server', - license = 'GNU General Public License v3 (GPLv3)', - tests_require = ['tox'], - cmdclass = {'test': Tox}, - install_requires = [], - author = 'Jeremy Grossmann', - author_email = 'package-maintainer@gns3.net', - description = 'GNS3 server with HTTP REST API to manage emulators', - long_description = open('README.rst', 'r').read(), - packages = find_packages(), - include_package_data = True, - platforms = 'any', - classifiers = [ - 'Development Status :: 1 - Planning', - 'Environment :: Console', - 'Intended Audience :: Information Technology', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Operating System :: OS Independent', + name="gns3-server", + version=__import__("gns3server").__version__, + url="http://github.com/GNS3/gns3-server", + license="GNU General Public License v3 (GPLv3)", + tests_require=["tox"], + cmdclass={"test": Tox}, + author="Jeremy Grossmann", + author_email="package-maintainer@gns3.net", + description="GNS3 server with HTTP REST API to manage emulators", + long_description=open("README.rst", "r").read(), + install_requires=[ + "tornado >= 2.0", + ], + entry_points={ + "console_scripts": [ + "gns3server = gns3server.main:main", + ] + }, + packages=find_packages(), + include_package_data=True, + platforms="any", + classifiers=[ + "Development Status :: 1 - Planning", + "Environment :: Console", + "Intended Audience :: Information Technology", + "Topic :: System :: Networking", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 'Natural Language :: English', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Topic :: System :: Networking' + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ], ) diff --git a/tests/test_version_handler.py b/tests/test_version_handler.py new file mode 100644 index 00000000..19b09ed2 --- /dev/null +++ b/tests/test_version_handler.py @@ -0,0 +1,37 @@ +from tornado.testing import AsyncHTTPTestCase +from gns3server.server import VersionHandler +from gns3server._compat import urlencode +import tornado.web +import json + +# URL to test +URL = "/version" + + +class TestVersionHandler(AsyncHTTPTestCase): + + def get_app(self): + return tornado.web.Application([(URL, VersionHandler)]) + + def test_endpoint(self): + self.http_client.fetch(self.get_url(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" + diff --git a/tox.ini b/tox.ini index fe9bd531..cc5a7779 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] -envlist = py26, py27, pypy, py33 +envlist = py27, pypy, py33 [testenv] -deps = pytest -commands = py.test +commands = py.test [] -s tests +deps = + pytest + tornado