diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..f8af51c9 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GNS3.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GNS3.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/GNS3" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GNS3" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/gns3server/modules/vpcs/adapters/__init__.py b/docs/__init__.py similarity index 100% rename from gns3server/modules/vpcs/adapters/__init__.py rename to docs/__init__.py diff --git a/docs/api/examples/get_version.txt b/docs/api/examples/get_version.txt new file mode 100644 index 00000000..fc0a948e --- /dev/null +++ b/docs/api/examples/get_version.txt @@ -0,0 +1,17 @@ +curl -i -xGET 'http://localhost:8000/version' + +GET /version HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 31 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /version + +{ + "version": "1.2.2.dev2" +} diff --git a/docs/api/examples/post_version.txt b/docs/api/examples/post_version.txt new file mode 100644 index 00000000..4ee7b50c --- /dev/null +++ b/docs/api/examples/post_version.txt @@ -0,0 +1,19 @@ +curl -i -xPOST 'http://localhost:8000/version' -d '{"version": "1.2.2.dev2"}' + +POST /version HTTP/1.1 +{ + "version": "1.2.2.dev2" +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 31 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /version + +{ + "version": "1.2.2.dev2" +} diff --git a/docs/api/examples/post_vpcs.txt b/docs/api/examples/post_vpcs.txt new file mode 100644 index 00000000..08616805 --- /dev/null +++ b/docs/api/examples/post_vpcs.txt @@ -0,0 +1,21 @@ +curl -i -xPOST 'http://localhost:8000/vpcs' -d '{"name": "PC TEST 1"}' + +POST /vpcs HTTP/1.1 +{ + "name": "PC TEST 1" +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 67 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs + +{ + "console": 4242, + "name": "PC TEST 1", + "vpcs_id": 42 +} diff --git a/docs/api/examples/post_vpcsvpcsidnio.txt b/docs/api/examples/post_vpcsvpcsidnio.txt new file mode 100644 index 00000000..84a739d6 --- /dev/null +++ b/docs/api/examples/post_vpcsvpcsidnio.txt @@ -0,0 +1,28 @@ +curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/nio' -d '{"id": 42, "nio": {"local_file": "/tmp/test", "remote_file": "/tmp/remote", "type": "nio_unix"}, "port": 0, "port_id": 0}' + +POST /vpcs/{vpcs_id}/nio HTTP/1.1 +{ + "id": 42, + "nio": { + "local_file": "/tmp/test", + "remote_file": "/tmp/remote", + "type": "nio_unix" + }, + "port": 0, + "port_id": 0 +} + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 62 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{vpcs_id}/nio + +{ + "console": 4242, + "name": "PC 2", + "vpcs_id": 42 +} diff --git a/docs/api/sleep.rst b/docs/api/sleep.rst new file mode 100644 index 00000000..fbf7845d --- /dev/null +++ b/docs/api/sleep.rst @@ -0,0 +1,13 @@ +/sleep +------------------------------ + +.. contents:: + +GET /sleep +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Response status codes +************************** +- **200**: OK + diff --git a/docs/api/stream.rst b/docs/api/stream.rst new file mode 100644 index 00000000..00180c62 --- /dev/null +++ b/docs/api/stream.rst @@ -0,0 +1,13 @@ +/stream +------------------------------ + +.. contents:: + +GET /stream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Response status codes +************************** +- **200**: OK + diff --git a/docs/api/version.rst b/docs/api/version.rst new file mode 100644 index 00000000..a5c86f19 --- /dev/null +++ b/docs/api/version.rst @@ -0,0 +1,28 @@ +/version +------------------------------ + +.. contents:: + +GET /version +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieve server version number + +Response status codes +************************** +- **200**: OK + +Output +******* +.. raw:: html + + + + +
NameMandatoryTypeDescription
versionstringVersion number human readable
+ +Sample session +*************** + + +.. literalinclude:: examples/get_version.txt + diff --git a/docs/api/vpcs.rst b/docs/api/vpcs.rst new file mode 100644 index 00000000..b800f755 --- /dev/null +++ b/docs/api/vpcs.rst @@ -0,0 +1,46 @@ +/vpcs +------------------------------ + +.. contents:: + +POST /vpcs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create a new VPCS and return it + +Parameters +********** +- **vpcs_id**: Id of VPCS instance + +Response status codes +************************** +- **201**: Success of creation of VPCS +- **409**: Conflict + +Input +******* +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
console integerconsole TCP port
namestringVPCS device name
vpcs_id integerVPCS device instance ID
+ +Output +******* +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
consoleintegerconsole TCP port
namestringVPCS device name
vpcs_idintegerVPCS device instance ID
+ +Sample session +*************** + + +.. literalinclude:: examples/post_vpcs.txt + diff --git a/docs/api/vpcsvpcsid.rst b/docs/api/vpcsvpcsid.rst new file mode 100644 index 00000000..20db8f9f --- /dev/null +++ b/docs/api/vpcsvpcsid.rst @@ -0,0 +1,63 @@ +/vpcs/{vpcs_id} +------------------------------ + +.. contents:: + +GET /vpcs/{vpcs_id} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get informations about a VPCS + +Parameters +********** +- **vpcs_id**: Id of VPCS instance + +Response status codes +************************** +- **200**: OK + +Output +******* +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
consoleintegerconsole TCP port
namestringVPCS device name
vpcs_idintegerVPCS device instance ID
+ + +PUT /vpcs/{vpcs_id} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Update VPCS informations + +Parameters +********** +- **vpcs_id**: Id of VPCS instance + +Response status codes +************************** +- **200**: OK + +Input +******* +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
consoleintegerconsole TCP port
namestringVPCS device name
vpcs_idintegerVPCS device instance ID
+ +Output +******* +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
consoleintegerconsole TCP port
namestringVPCS device name
vpcs_idintegerVPCS device instance ID
+ diff --git a/docs/api/vpcsvpcsidnio.rst b/docs/api/vpcsvpcsidnio.rst new file mode 100644 index 00000000..14a9a6db --- /dev/null +++ b/docs/api/vpcsvpcsidnio.rst @@ -0,0 +1,127 @@ +/vpcs/{vpcs_id}/nio +------------------------------ + +.. contents:: + +POST /vpcs/{vpcs_id}/nio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +ADD NIO to a VPCS + +Parameters +********** +- **vpcs_id**: Id of VPCS instance + +Response status codes +************************** +- **201**: Success of creation of NIO +- **409**: Conflict + +Input +******* +Types ++++++++++ +Ethernet +^^^^^^^^^^^^^^^^ +Generic Ethernet Network Input/Output + +.. raw:: html + + + + + +
NameMandatoryTypeDescription
ethernet_devicestringEthernet device name e.g. eth0
typeenumPossible values: nio_generic_ethernet
+ +LinuxEthernet +^^^^^^^^^^^^^^^^ +Linux Ethernet Network Input/Output + +.. raw:: html + + + + + +
NameMandatoryTypeDescription
ethernet_devicestringEthernet device name e.g. eth0
typeenumPossible values: nio_linux_ethernet
+ +NULL +^^^^^^^^^^^^^^^^ +NULL Network Input/Output + +.. raw:: html + + + + +
NameMandatoryTypeDescription
typeenumPossible values: nio_null
+ +TAP +^^^^^^^^^^^^^^^^ +TAP Network Input/Output + +.. raw:: html + + + + + +
NameMandatoryTypeDescription
tap_devicestringTAP device name e.g. tap0
typeenumPossible values: nio_tap
+ +UDP +^^^^^^^^^^^^^^^^ +UDP Network Input/Output + +.. raw:: html + + + + + + + +
NameMandatoryTypeDescription
lportintegerLocal port
rhoststringRemote host
rportintegerRemote port
typeenumPossible values: nio_udp
+ +UNIX +^^^^^^^^^^^^^^^^ +UNIX Network Input/Output + +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
local_filestringpath to the UNIX socket file (local)
remote_filestringpath to the UNIX socket file (remote)
typeenumPossible values: nio_unix
+ +VDE +^^^^^^^^^^^^^^^^ +VDE Network Input/Output + +.. raw:: html + + + + + + +
NameMandatoryTypeDescription
control_filestringpath to the VDE control file
local_filestringpath to the VDE control file
typeenumPossible values: nio_vde
+ +Body ++++++++++ +.. raw:: html + + + + + + + +
NameMandatoryTypeDescription
idintegerVPCS device instance ID
nioUDP, Ethernet, LinuxEthernet, TAP, UNIX, VDE, NULLNetwork Input/Output
portintegerPort number
port_idintegerUnique port identifier for the VPCS instance
+ +Sample session +*************** + + +.. literalinclude:: examples/post_vpcsvpcsidnio.txt + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..c3203698 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# POC documentation build configuration file, created by +# sphinx-quickstart on Mon Jan 5 14:15:48 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) + +from demoserver.version import __version__, __version_info__ + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'POC' +copyright = '2015, POC Team' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '.'.join(map(lambda x: str(x), __version_info__)) +# The full version, including alpha/beta/rc tags. +release = __version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'POCdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'POC.tex', 'POC Documentation', 'POC Team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'gns3', 'POC Documentation', + ['POC Team'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'POC', 'POC Documentation', + 'POC Team', 'POC', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 00000000..28e7f47a --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,33 @@ +Development +############ + +Code convention +=============== + +You should respect all the PEP8 convention except the +rule about max line length. + + +Documentation +============== + +Build doc +---------- +In the project root folder: + +.. code-block:: bash + + ./documentation.sh + +The output is available inside *docs/_build/html* + +Tests +====== + +Run tests +---------- + +.. code-block:: bash + + py.test -v + diff --git a/docs/general.rst b/docs/general.rst new file mode 100644 index 00000000..efc3cb3c --- /dev/null +++ b/docs/general.rst @@ -0,0 +1,12 @@ +Errors +====== + +In case of error a standard HTTP error is raise and you got a +JSON like that + +.. code-block:: json + + { + "status": 409, + "message": "Conflict" + } diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..a8a1fc27 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +Welcome to API documentation! +====================================== + + +.. toctree:: + general + development + + +API Endpoints +~~~~~~~~~~~~~~~ + +.. toctree:: + :glob: + :maxdepth: 2 + + api/* + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..382a719b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\GNS3.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\GNS3.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/gns3server/__init__.py b/gns3server/__init__.py index 89778293..ccdabda2 100644 --- a/gns3server/__init__.py +++ b/gns3server/__init__.py @@ -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,14 +15,4 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# __version__ is a human-readable version number. - -# __version_info__ is a four-tuple for programmatic comparison. The first -# three numbers are the components of the version number. The fourth -# is zero for an official release, positive for a development branch, -# or negative for a release candidate or beta (after the base version -# number has been incremented) - -#from .module_manager import ModuleManager -#from .server import Server from .version import __version__ diff --git a/gns3server/builtins/server_version.py b/gns3server/builtins/server_version.py deleted file mode 100644 index aaf294fb..00000000 --- a/gns3server/builtins/server_version.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- 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 . - -""" -Sends version to requesting clients in JSON-RPC Websocket handler. -""" - - -from ..version import __version__ -from ..jsonrpc import JSONRPCResponse - - -def server_version(handler, request_id, params): - """ - Builtin destination to return the server version. - - :param handler: JSONRPCWebSocket instance - :param request_id: JSON-RPC call identifier - :param params: JSON-RPC method params (not used here) - """ - - json_message = {"version": __version__} - handler.write_message(JSONRPCResponse(json_message, request_id)()) diff --git a/gns3server/config.py b/gns3server/config.py index ddf4286d..d851c49c 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -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 diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py index 15673604..78e45844 100644 --- a/gns3server/handlers/file_upload_handler.py +++ b/gns3server/handlers/file_upload_handler.py @@ -19,6 +19,8 @@ Simple file upload & listing handler. """ +#TODO: file upload with aiohttp + import os import stat diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py deleted file mode 100644 index e14ae8c3..00000000 --- a/gns3server/handlers/jsonrpc_websocket.py +++ /dev/null @@ -1,204 +0,0 @@ -# -*- 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 . - -""" -JSON-RPC protocol over Websockets. -""" - -import zmq -import uuid -import tornado.websocket -from .auth_handler import GNS3WebSocketBaseHandler -from tornado.escape import json_decode -from ..jsonrpc import JSONRPCParseError -from ..jsonrpc import JSONRPCInvalidRequest -from ..jsonrpc import JSONRPCMethodNotFound -from ..jsonrpc import JSONRPCNotification -from ..jsonrpc import JSONRPCCustomError - -import logging -log = logging.getLogger(__name__) - - -class JSONRPCWebSocket(GNS3WebSocketBaseHandler): - """ - STOMP protocol over Tornado Websockets with message - routing to ZeroMQ dealer clients. - - :param application: Tornado Application instance - :param request: Tornado Request instance - :param zmq_router: ZeroMQ router socket - """ - - clients = set() - destinations = {} - version = 2.0 # only JSON-RPC version 2.0 is supported - - def __init__(self, application, request, zmq_router): - tornado.websocket.WebSocketHandler.__init__(self, application, request) - self._session_id = str(uuid.uuid4()) - self.zmq_router = zmq_router - - def check_origin(self, origin): - return True - - @property - def session_id(self): - """ - Session ID uniquely representing a Websocket client - - :returns: the session id - """ - - return self._session_id - - @classmethod - def dispatch_message(cls, stream, message): - """ - Sends a message to Websocket client - - :param message: message from a module (received via ZeroMQ) - """ - - # Module name that is replying - module = message[0].decode("utf-8") - - # ZMQ responses are encoded in JSON - # format is a JSON array: [session ID, JSON-RPC response] - try: - json_message = json_decode(message[1]) - except ValueError as e: - stream.send_string("Cannot decode message!") - log.critical("Couldn't decode message: {}".format(e)) - return - - session_id = json_message[0] - jsonrpc_response = json_message[1] - - log.debug("Received message from module {}: {}".format(module, json_message)) - - for client in cls.clients: - if client.session_id == session_id: - client.write_message(jsonrpc_response) - - @classmethod - def register_destination(cls, destination, module): - """ - Registers a destination handled by a module. - Used to route requests to the right module. - - :param destination: destination string - :param module: module string - """ - - # Make sure the destination is not already registered - # by another module for instance - assert destination not in cls.destinations - if destination.startswith("builtin"): - log.debug("registering {} as a built-in destination".format(destination)) - else: - log.debug("registering {} as a destination for the {} module".format(destination, module)) - cls.destinations[destination] = module - - def open(self): - """ - Invoked when a new WebSocket is opened. - """ - - log.info("Websocket client {} connected".format(self.session_id)) - - authenticated_user = self.get_current_user() - - if authenticated_user: - self.clients.add(self) - log.info("Websocket authenticated user: %s" % (authenticated_user)) - else: - self.close() - log.info("Websocket non-authenticated user attempt: %s" % (authenticated_user)) - - def on_message(self, message): - """ - Handles incoming messages. - - :param message: message received over the Websocket - """ - - log.debug("Received Websocket message: {}".format(message)) - - if self.zmq_router.closed: - # no need to proceed, the ZeroMQ router has been closed. - return - - try: - request = json_decode(message) - jsonrpc_version = request["jsonrpc"] - method = request["method"] - # This is a JSON-RPC notification if request_id is None - request_id = request.get("id") - except: - return self.write_message(JSONRPCParseError()()) - - if jsonrpc_version != self.version: - return self.write_message(JSONRPCInvalidRequest()()) - - if len(self.clients) > 1: - #TODO: multiple client support - log.warn("GNS3 server doesn't support multiple clients yet") - return self.write_message(JSONRPCCustomError(-3200, - "There are {} clients connected, the GNS3 server cannot handle multiple clients yet".format(len(self.clients)), - request_id)()) - - if method not in self.destinations: - if request_id: - log.warn("JSON-RPC method not found: {}".format(method)) - return self.write_message(JSONRPCMethodNotFound(request_id)()) - else: - # This is a notification, silently ignore this error... - return - - if method.startswith("builtin") and request_id: - log.info("calling built-in method {}".format(method)) - self.destinations[method](self, request_id, request.get("params")) - return - - module = self.destinations[method] - # ZMQ requests are encoded in JSON - # format is a JSON array: [session ID, JSON-RPC request] - zmq_request = [self.session_id, request] - # Route to the correct module - self.zmq_router.send_string(module, zmq.SNDMORE) - # Send the JSON request - self.zmq_router.send_json(zmq_request) - - def on_close(self): - """ - Invoked when the WebSocket is closed. - """ - - log.info("Websocket client {} disconnected".format(self.session_id)) - self.clients.remove(self) - - # Reset the modules if there are no clients anymore - # Modules must implement a reset destination - if not self.clients and not self.zmq_router.closed: - for destination, module in self.destinations.items(): - if destination.endswith("reset"): - # Route to the correct module - self.zmq_router.send_string(module, zmq.SNDMORE) - # Send the JSON request - notification = JSONRPCNotification(destination)() - self.zmq_router.send_json([self.session_id, notification]) diff --git a/gns3server/handlers/version_handler.py b/gns3server/handlers/version_handler.py index 30c55d40..abdb43c5 100644 --- a/gns3server/handlers/version_handler.py +++ b/gns3server/handlers/version_handler.py @@ -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,12 +15,33 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from .auth_handler import GNS3BaseHandler +from ..web.route import Route +from ..schemas.version import VERSION_SCHEMA from ..version import __version__ +from aiohttp.web import HTTPConflict -class VersionHandler(GNS3BaseHandler): +class VersionHandler: - def get(self): - response = {'version': __version__} - self.write(response) + @classmethod + @Route.get( + r"/version", + description="Retrieve the server version number", + output=VERSION_SCHEMA) + def version(request, response): + response.json({'version': __version__}) + + @classmethod + @Route.post( + r"/version", + description="Check if version is the same as the server", + output=VERSION_SCHEMA, + input=VERSION_SCHEMA, + status_codes={ + 200: "Same version", + 409: "Invalid version" + }) + def check_version(request, response): + if request.json["version"] != __version__: + raise HTTPConflict(reason="Invalid version") + response.json({'version': __version__}) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py new file mode 100644 index 00000000..74d44fc1 --- /dev/null +++ b/gns3server/handlers/vpcs_handler.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# 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 +# 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 ..web.route import Route +from ..modules.vpcs import VPCS + +# schemas +from ..schemas.vpcs import VPCS_CREATE_SCHEMA +from ..schemas.vpcs import VPCS_OBJECT_SCHEMA +from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA + + +class VPCSHandler(object): + @classmethod + @Route.post( + r"/vpcs", + parameters={ + "vpcs_id": "Id of VPCS instance" + }, + status_codes={ + 201: "Success of creation of VPCS", + 409: "Conflict" + }, + description="Create a new VPCS and return it", + input=VPCS_CREATE_SCHEMA, + output=VPCS_OBJECT_SCHEMA) + def create(request, response): + vpcs = VPCS.instance() + i = yield from vpcs.create(request.json) + response.json({'name': request.json['name'], + "vpcs_id": i, + "console": 4242}) + + @classmethod + @Route.get( + r"/vpcs/{vpcs_id}", + parameters={ + "vpcs_id": "Id of VPCS instance" + }, + description="Get information about a VPCS", + output=VPCS_OBJECT_SCHEMA) + def show(request, response): + response.json({'name': "PC 1", "vpcs_id": 42, "console": 4242}) + + @classmethod + @Route.put( + r"/vpcs/{vpcs_id}", + parameters={ + "vpcs_id": "Id of VPCS instance" + }, + description="Update VPCS information", + input=VPCS_OBJECT_SCHEMA, + output=VPCS_OBJECT_SCHEMA) + def update(request, response): + response.json({'name': "PC 1", "vpcs_id": 42, "console": 4242}) + + @classmethod + @Route.post( + r"/vpcs/{vpcs_id}/nio", + parameters={ + "vpcs_id": "Id of VPCS instance" + }, + status_codes={ + 201: "Success of creation of NIO", + 409: "Conflict" + }, + description="ADD NIO to a VPCS", + input=VPCS_ADD_NIO_SCHEMA) + def create_nio(request, response): + # TODO: raise 404 if VPCS not found + response.json({'name': "PC 2", "vpcs_id": 42, "console": 4242}) diff --git a/gns3server/jsonrpc.py b/gns3server/jsonrpc.py deleted file mode 100644 index c4251aad..00000000 --- a/gns3server/jsonrpc.py +++ /dev/null @@ -1,184 +0,0 @@ -# -*- 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 . - -""" -JSON-RPC protocol implementation. -http://www.jsonrpc.org/specification -""" - -import json -import uuid - - -class JSONRPCObject(object): - """ - Base object for JSON-RPC requests, responses, - notifications and errors. - """ - - def __init__(self): - return JSONRPCEncoder().default(self) - - def __str__(self, *args, **kwargs): - return json.dumps(self, cls=JSONRPCEncoder) - - def __call__(self): - return JSONRPCEncoder().default(self) - - -class JSONRPCEncoder(json.JSONEncoder): - """ - Creates the JSON-RPC message. - """ - - def default(self, obj): - """ - Returns a Python dictionary corresponding to a JSON-RPC message. - """ - - if isinstance(obj, JSONRPCObject): - message = {"jsonrpc": 2.0} - for field in dir(obj): - if not field.startswith('_'): - value = getattr(obj, field) - message[field] = value - return message - return json.JSONEncoder.default(self, obj) - - -class JSONRPCInvalidRequest(JSONRPCObject): - """ - Error response for an invalid request. - """ - - def __init__(self): - JSONRPCObject.__init__(self) - self.id = None - self.error = {"code": -32600, "message": "Invalid Request"} - - -class JSONRPCMethodNotFound(JSONRPCObject): - """ - Error response for an method not found. - - :param request_id: JSON-RPC identifier - """ - - def __init__(self, request_id): - JSONRPCObject.__init__(self) - self.id = request_id - self.error = {"code": -32601, "message": "Method not found"} - - -class JSONRPCInvalidParams(JSONRPCObject): - """ - Error response for invalid parameters. - - :param request_id: JSON-RPC identifier - """ - - def __init__(self, request_id): - JSONRPCObject.__init__(self) - self.id = request_id - self.error = {"code": -32602, "message": "Invalid params"} - - -class JSONRPCInternalError(JSONRPCObject): - """ - Error response for an internal error. - - :param request_id: JSON-RPC identifier (optional) - """ - - def __init__(self, request_id=None): - JSONRPCObject.__init__(self) - self.id = request_id - self.error = {"code": -32603, "message": "Internal error"} - - -class JSONRPCParseError(JSONRPCObject): - """ - Error response for parsing error. - """ - - def __init__(self): - JSONRPCObject.__init__(self) - self.id = None - self.error = {"code": -32700, "message": "Parse error"} - - -class JSONRPCCustomError(JSONRPCObject): - """ - Error response for an custom error. - - :param code: JSON-RPC error code - :param message: JSON-RPC error message - :param request_id: JSON-RPC identifier (optional) - """ - - def __init__(self, code, message, request_id=None): - JSONRPCObject.__init__(self) - self.id = request_id - self.error = {"code": code, "message": message} - - -class JSONRPCResponse(JSONRPCObject): - """ - JSON-RPC successful response. - - :param result: JSON-RPC result - :param request_id: JSON-RPC identifier - """ - - def __init__(self, result, request_id): - JSONRPCObject.__init__(self) - self.id = request_id - self.result = result - - -class JSONRPCRequest(JSONRPCObject): - """ - JSON-RPC request. - - :param method: JSON-RPC destination method - :param params: JSON-RPC params for the corresponding method (optional) - :param request_id: JSON-RPC identifier (generated by default) - """ - - def __init__(self, method, params=None, request_id=None): - JSONRPCObject.__init__(self) - if request_id is None: - request_id = str(uuid.uuid4()) - self.id = request_id - self.method = method - if params: - self.params = params - - -class JSONRPCNotification(JSONRPCObject): - """ - JSON-RPC notification. - - :param method: JSON-RPC destination method - :param params: JSON-RPC params for the corresponding method (optional) - """ - - def __init__(self, method, params=None): - JSONRPCObject.__init__(self) - self.method = method - if params: - self.params = params diff --git a/gns3server/main.py b/gns3server/main.py index ac331df9..71aaae90 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- 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 @@ -19,20 +19,23 @@ import os import datetime import sys -import multiprocessing import locale + +#TODO: importing this module also configures logging options (colors etc.) +#see https://github.com/tornadoweb/tornado/blob/master/tornado/log.py#L208 import tornado.options + from gns3server.server import Server from gns3server.version import __version__ import logging log = logging.getLogger(__name__) +#TODO: migrate command line options to argparse # command line options from tornado.options import define define("host", default="0.0.0.0", help="run on the given host/IP address", type=str) define("port", default=8000, help="run on the given port", type=int) -define("ipc", default=False, help="use IPC for module communication", type=bool) define("version", default=False, help="show the version", type=bool) define("quiet", default=False, help="do not show output on stdout", type=bool) define("console_bind_to_any", default=True, help="bind console ports to any local IP address", type=bool) @@ -50,7 +53,7 @@ def locale_check(): or there: http://robjwells.com/post/61198832297/get-your-us-ascii-out-of-my-face """ - # no need to check on Windows or when frozen + # no need to check on Windows or when this application is frozen if sys.platform.startswith("win") or hasattr(sys, "frozen"): return @@ -82,10 +85,7 @@ def main(): Entry point for GNS3 server """ - if sys.platform.startswith("win"): - # necessary on Windows to freeze the application - multiprocessing.freeze_support() - + #TODO: migrate command line options to argparse (don't forget the quiet mode). try: tornado.options.parse_command_line() except (tornado.options.Error, ValueError): @@ -109,6 +109,7 @@ def main(): user_log.info("GNS3 server version {}".format(__version__)) user_log.info("Copyright (c) 2007-{} GNS3 Technologies Inc.".format(current_year)) + #TODO: end todo # we only support Python 3 version >= 3.3 if sys.version_info < (3, 3): @@ -118,8 +119,7 @@ def main(): major=sys.version_info[0], minor=sys.version_info[1], micro=sys.version_info[2], pid=os.getpid())) - # check for the correct locale - # (UNIX/Linux only) + # check for the correct locale (UNIX/Linux only) locale_check() try: @@ -128,8 +128,7 @@ def main(): log.critical("the current working directory doesn't exist") return - server = Server(options.host, options.port, options.ipc, options.console_bind_to_any) - server.load_modules() + server = Server(options.host, options.port, options.console_bind_to_any) server.run() if __name__ == '__main__': diff --git a/gns3server/module_manager.py b/gns3server/module_manager.py deleted file mode 100644 index 878f0852..00000000 --- a/gns3server/module_manager.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- 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 inspect -import pkgutil -from .modules import IModule - -import logging -log = logging.getLogger(__name__) - - -class Module(object): - """ - Module representation for the module manager - - :param name: module name - :param cls: module class to be instantiated when - the module is activated - """ - - 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 - - def cls(self): - return self._cls - - -class ModuleManager(object): - """ - Manages modules - - :param module_paths: path from where module are loaded - """ - - def __init__(self, module_paths=['modules']): - - self._modules = [] - self._module_paths = module_paths - - def load_modules(self): - """ - Finds all the possible modules (classes with IModule as a parent) - """ - - for _, name, ispkg in pkgutil.iter_modules(self._module_paths): - if (ispkg): - log.debug("analyzing {} package directory".format(name)) - try: - file, pathname, description = imp.find_module(name, self._module_paths) - module = imp.load_module(name, file, pathname, description) - classes = inspect.getmembers(module, inspect.isclass) - for module_class in classes: - if issubclass(module_class[1], IModule): - # make sure the module class has IModule as a parent - if module_class[1].__module__ == name: - log.info("loading {} module".format(module_class[0].lower())) - info = Module(name=module_class[0].lower(), cls=module_class[1]) - self._modules.append(info) - except Exception: - log.critical("error while analyzing {} package directory".format(name), exc_info=1) - finally: - if file: - file.close() - - def get_all_modules(self): - """ - Returns all modules. - - :returns: list of Module objects - """ - - return self._modules - - def activate_module(self, module, *args, **kwargs): - """ - Activates a given module. - - :param module: module to activate (Module object) - :param args: args passed to the module - :param kwargs: kwargs passed to the module - - :returns: instantiated module class - """ - - module_class = module.cls() - try: - module_instance = module_class(module.name, *args, **kwargs) - except Exception: - log.critical("error while activating the {} module".format(module.name), exc_info=1) - return None - log.info("activating the {} module".format(module.name)) - return module_instance diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index b451a69b..5fc0b897 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -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 . 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) diff --git a/gns3server/modules/base.py b/gns3server/modules/base.py index 0f3e0269..38761bf3 100644 --- a/gns3server/modules/base.py +++ b/gns3server/modules/base.py @@ -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 . -""" -Base class (interface) for modules -""" +import asyncio -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 +#TODO: make this more generic (not json but *args?) +class BaseModule(object): + _instance = None -import logging -log = logging.getLogger(__name__) - - -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 - """ + @classmethod + def instance(cls): + if cls._instance is None: + cls._instance = cls() + asyncio.async(cls._instance.run()) + return cls._instance - if not self.name in self.modules: - log.warn("no destinations found for module {}".format(self.name)) - return [] - return self.modules[self.name].keys() + def __init__(self): + self._queue = asyncio.Queue() @classmethod - def route(cls, destination): - """ - Decorator to register a destination routed to a method + def destroy(cls): + future = asyncio.Future() + cls._instance._queue.put_nowait((future, None, )) + yield from asyncio.wait([future]) + cls._instance = None - :param destination: destination to be routed - """ + @asyncio.coroutine + def put(self, json): - 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 + future = asyncio.Future() + self._queue.put_nowait((future, json, )) + yield from asyncio.wait([future]) + return future.result() - @property - def images_directory(self): + @asyncio.coroutine + def run(self): - return self._images_dir + 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 diff --git a/gns3server/modules/old_vpcs/__init__.py b/gns3server/modules/old_vpcs/__init__.py new file mode 100644 index 00000000..aa0f216e --- /dev/null +++ b/gns3server/modules/old_vpcs/__init__.py @@ -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 . + +""" +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) diff --git a/gns3server/modules/vpcs/nios/__init__.py b/gns3server/modules/old_vpcs/adapters/__init__.py similarity index 100% rename from gns3server/modules/vpcs/nios/__init__.py rename to gns3server/modules/old_vpcs/adapters/__init__.py diff --git a/gns3server/modules/vpcs/adapters/adapter.py b/gns3server/modules/old_vpcs/adapters/adapter.py similarity index 100% rename from gns3server/modules/vpcs/adapters/adapter.py rename to gns3server/modules/old_vpcs/adapters/adapter.py diff --git a/gns3server/modules/vpcs/adapters/ethernet_adapter.py b/gns3server/modules/old_vpcs/adapters/ethernet_adapter.py similarity index 100% rename from gns3server/modules/vpcs/adapters/ethernet_adapter.py rename to gns3server/modules/old_vpcs/adapters/ethernet_adapter.py diff --git a/gns3server/modules/old_vpcs/nios/__init__.py b/gns3server/modules/old_vpcs/nios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/old_vpcs/nios/nio_tap.py similarity index 100% rename from gns3server/modules/vpcs/nios/nio_tap.py rename to gns3server/modules/old_vpcs/nios/nio_tap.py diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/old_vpcs/nios/nio_udp.py similarity index 100% rename from gns3server/modules/vpcs/nios/nio_udp.py rename to gns3server/modules/old_vpcs/nios/nio_udp.py diff --git a/gns3server/modules/vpcs/schemas.py b/gns3server/modules/old_vpcs/schemas.py similarity index 100% rename from gns3server/modules/vpcs/schemas.py rename to gns3server/modules/old_vpcs/schemas.py diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/old_vpcs/vpcs_device.py similarity index 100% rename from gns3server/modules/vpcs/vpcs_device.py rename to gns3server/modules/old_vpcs/vpcs_device.py diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/old_vpcs/vpcs_error.py similarity index 100% rename from gns3server/modules/vpcs/vpcs_error.py rename to gns3server/modules/old_vpcs/vpcs_error.py diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index aa0f216e..d544365c 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -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 diff --git a/gns3server/schemas/__init__.py b/gns3server/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/_compat.py b/gns3server/schemas/version.py similarity index 59% rename from gns3server/_compat.py rename to gns3server/schemas/version.py index 78bd53f8..bf9d41f4 100644 --- a/gns3server/_compat.py +++ b/gns3server/schemas/version.py @@ -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,22 +15,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import sys - -PY2 = sys.version_info[0] == 2 - -if not PY2: - unichr = chr - range_type = range - text_type = str - string_types = (str,) -else: - unichr = unichr - 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 +VERSION_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + 'required': ['version'], + "additionalProperties": False, + "properties": { + "version": { + "description": "Version number human readable", + "type": "string", + "minLength": 5, + } + } +} diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py new file mode 100644 index 00000000..7d205391 --- /dev/null +++ b/gns3server/schemas/vpcs.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +# +# 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 +# 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 . + + +VPCS_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "vpcs_id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name"] +} + + +VPCS_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a VPCS instance", + "type": "object", + + "definitions": { + "UDP": { + "description": "UDP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_udp"] + }, + "lport": { + "description": "Local port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "rhost": { + "description": "Remote host", + "type": "string", + "minLength": 1 + }, + "rport": { + "description": "Remote port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + }, + "required": ["type", "lport", "rhost", "rport"], + "additionalProperties": False + }, + "Ethernet": { + "description": "Generic Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_generic_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "LinuxEthernet": { + "description": "Linux Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_linux_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "TAP": { + "description": "TAP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_tap"] + }, + "tap_device": { + "description": "TAP device name e.g. tap0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "tap_device"], + "additionalProperties": False + }, + "UNIX": { + "description": "UNIX Network Input/Output", + "properties": { + "type": { + "enum": ["nio_unix"] + }, + "local_file": { + "description": "path to the UNIX socket file (local)", + "type": "string", + "minLength": 1 + }, + "remote_file": { + "description": "path to the UNIX socket file (remote)", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "local_file", "remote_file"], + "additionalProperties": False + }, + "VDE": { + "description": "VDE Network Input/Output", + "properties": { + "type": { + "enum": ["nio_vde"] + }, + "control_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + "local_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "control_file", "local_file"], + "additionalProperties": False + }, + "NULL": { + "description": "NULL Network Input/Output", + "properties": { + "type": { + "enum": ["nio_null"] + }, + }, + "required": ["type"], + "additionalProperties": False + }, + }, + + "properties": { + "id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the VPCS instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 0 + }, + "nio": { + "type": "object", + "description": "Network Input/Output", + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/TAP"}, + {"$ref": "#/definitions/UNIX"}, + {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/NULL"}, + ] + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "nio"] +} + +VPCS_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "VPCS instance", + "type": "object", + "properties": { + "name": { + "description": "VPCS device name", + "type": "string", + "minLength": 1, + }, + "vpcs_id": { + "description": "VPCS device instance ID", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "vpcs_id", "console"] +} + +VBOX_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VirtualBox VM instance", + "type": "object", + "properties": { + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vbox_id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name"], +} + + +VBOX_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "VirtualBox instance", + "type": "object", + "properties": { + "name": { + "description": "VirtualBox VM name", + "type": "string", + "minLength": 1, + }, + "vbox_id": { + "description": "VirtualBox VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "vbox_id"] +} diff --git a/gns3server/server.py b/gns3server/server.py index 5d748dc2..b3d5fbd2 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -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 @@ -19,51 +19,36 @@ Set up and run the server. """ -import zmq -from zmq.eventloop import ioloop, zmqstream -ioloop.install() - -import sys import os -import tempfile +import sys import signal -import errno -import socket -import tornado.ioloop -import tornado.web -import tornado.autoreload -import pkg_resources +import asyncio +import aiohttp import ipaddress -import base64 -import uuid +import functools +import types +import time -from pkg_resources import parse_version +from .web.route import Route from .config import Config -from .handlers.jsonrpc_websocket import JSONRPCWebSocket -from .handlers.version_handler import VersionHandler -from .handlers.file_upload_handler import FileUploadHandler -from .handlers.auth_handler import LoginHandler -from .builtins.server_version import server_version -from .builtins.interfaces import interfaces from .modules import MODULES +#FIXME: have something generic to automatically import handlers so the routes can be found +from .handlers.version_handler import VersionHandler +from .handlers.vpcs_handler import VPCSHandler + import logging log = logging.getLogger(__name__) -class Server(object): +class Server: - # built-in handlers - handlers = [(r"/version", VersionHandler), - (r"/upload", FileUploadHandler), - (r"/login", LoginHandler)] - - def __init__(self, host, port, ipc, console_bind_to_any): + def __init__(self, host, port, console_bind_to_any): self._host = host self._port = port - self._router = None - self._stream = None + self._loop = None + self._start_time = time.time() if console_bind_to_any: if ipaddress.ip_address(self._host).version == 6: @@ -73,263 +58,106 @@ class Server(object): else: self._console_host = self._host - if ipc: - self._zmq_port = 0 # this forces to use IPC for communications with the ZeroMQ server - else: - # communication between the ZeroMQ server and the modules (ZeroMQ dealers) - # is IPv4 and local (127.0.0.1) - try: - # let the OS find an unused port for the ZeroMQ server - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.bind(("127.0.0.1", 0)) - self._zmq_port = sock.getsockname()[1] - except OSError as e: - log.critical("server cannot listen to {}: {}".format(self._host, e)) - raise SystemExit - self._ipc = ipc - self._modules = [] - - # get the projects and temp directories from the configuration file (passed to the modules) - config = Config.instance() - server_config = config.get_default_section() - # default projects directory is "~/GNS3/projects" - self._projects_dir = os.path.expandvars(os.path.expanduser(server_config.get("projects_directory", "~/GNS3/projects"))) - self._temp_dir = server_config.get("temporary_directory", tempfile.gettempdir()) - - try: - os.makedirs(self._projects_dir) - log.info("projects directory '{}' created".format(self._projects_dir)) - except FileExistsError: - pass - except OSError as e: - log.error("could not create the projects directory {}: {}".format(self._projects_dir, e)) - - def load_modules(self): + #TODO: server config file support, to be reviewed + # # get the projects and temp directories from the configuration file (passed to the modules) + # config = Config.instance() + # server_config = config.get_default_section() + # # default projects directory is "~/GNS3/projects" + # self._projects_dir = os.path.expandvars(os.path.expanduser(server_config.get("projects_directory", "~/GNS3/projects"))) + # self._temp_dir = server_config.get("temporary_directory", tempfile.gettempdir()) + # + # try: + # os.makedirs(self._projects_dir) + # log.info("projects directory '{}' created".format(self._projects_dir)) + # except FileExistsError: + # pass + # except OSError as e: + # log.error("could not create the projects directory {}: {}".format(self._projects_dir, e)) + + @asyncio.coroutine + def _run_application(self, app): + + server = yield from self._loop.create_server(app.make_handler(), self._host, self._port) + return server + + def _stop_application(self): """ - Loads the modules. + Cleanup the modules (shutdown running emulators etc.) """ - #======================================================================= - # cwd = os.path.dirname(os.path.abspath(__file__)) - # module_path = os.path.join(cwd, 'modules') - # log.info("loading modules from {}".format(module_path)) - # module_manager = ModuleManager([module_path]) - # module_manager.load_modules() - # for module in module_manager.get_all_modules(): - # instance = module_manager.activate_module(module, - # "127.0.0.1", # ZeroMQ server address - # self._zmq_port, # ZeroMQ server port - # projects_dir=self._projects_dir, - # temp_dir=self._temp_dir) - # if not instance: - # continue - # self._modules.append(instance) - # destinations = instance.destinations() - # for destination in destinations: - # JSONRPCWebSocket.register_destination(destination, module.name) - # instance.start() # starts the new process - #======================================================================= + #TODO: clean everything from here + self._loop.stop() - # special built-in to return the server version - JSONRPCWebSocket.register_destination("builtin.version", server_version) - # special built-in to return the available interfaces on this host - JSONRPCWebSocket.register_destination("builtin.interfaces", interfaces) + def _signal_handling(self): - for module in MODULES: - instance = module(module.__name__.lower(), - "127.0.0.1", # ZeroMQ server address - self._zmq_port, # ZeroMQ server port - host=self._host, # server host address - console_host=self._console_host, - projects_dir=self._projects_dir, - temp_dir=self._temp_dir) + def signal_handler(signame): + log.warning("server has got signal {}, exiting...".format(signame)) + self._stop_application() + + signals = ["SIGTERM", "SIGINT"] + if sys.platform.startswith("win"): + signals.extend(["SIGBREAK"]) + else: + signals.extend(["SIGHUP", "SIGQUIT"]) - self._modules.append(instance) - destinations = instance.destinations() - for destination in destinations: - JSONRPCWebSocket.register_destination(destination, instance.name) - instance.start() # starts the new process + for signal_name in signals: + callback = functools.partial(signal_handler, signal_name) + if sys.platform.startswith("win"): + # add_signal_handler() is not yet supported on Windows + signal.signal(getattr(signal, signal_name), callback) + else: + self._loop.add_signal_handler(getattr(signal, signal_name), callback) + + def _reload_hook(self): + + def reload(): + + log.info("reloading") + self._stop_application() + os.execv(sys.executable, [sys.executable] + sys.argv) + + # code extracted from tornado + for module in sys.modules.values(): + # Some modules play games with sys.modules (e.g. email/__init__.py + # in the standard library), and occasionally this can cause strange + # failures in getattr. Just ignore anything that's not an ordinary + # module. + if not isinstance(module, types.ModuleType): + continue + path = getattr(module, "__file__", None) + if not path: + continue + if path.endswith(".pyc") or path.endswith(".pyo"): + path = path[:-1] + modified = os.stat(path).st_mtime + if modified > self._start_time: + log.debug("file {} has been modified".format(path)) + reload() + self._loop.call_later(1, self._reload_hook) def run(self): """ - Starts the Tornado web server and ZeroMQ server. + Starts the server. """ - settings = { - "debug":True, - "cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), - "login_url": "/login", - } - - ssl_options = {} - - try: - cloud_config = Config.instance().get_section_config("CLOUD_SERVER") - - cloud_settings = { - - "required_user" : cloud_config['WEB_USERNAME'], - "required_pass" : cloud_config['WEB_PASSWORD'], - } - - settings.update(cloud_settings) - - if cloud_config["SSL_ENABLED"] == "yes": - ssl_options = { - "certfile" : cloud_config["SSL_CRT"], - "keyfile" : cloud_config["SSL_KEY"], - } - - log.info("Certs found - starting in SSL mode") - except KeyError: - log.info("Missing cloud.conf - disabling HTTP auth and SSL") - - router = self._create_zmq_router() - # Add our JSON-RPC Websocket handler to Tornado - self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))]) - if hasattr(sys, "frozen"): - templates_dir = "templates" - else: - templates_dir = pkg_resources.resource_filename("gns3server", "templates") - tornado_app = tornado.web.Application(self.handlers, - template_path=templates_dir, - **settings) # FIXME: debug mode! - - try: - user_log = logging.getLogger('user_facing') - user_log.info("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format( - self._host, self._port, tornado.version, zmq.__version__, zmq.zmq_version())) - - kwargs = {"address": self._host} - - if ssl_options: - kwargs["ssl_options"] = ssl_options - - if parse_version(tornado.version) >= parse_version("3.1"): - kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit - - tornado_app.listen(self._port, **kwargs) - except OSError as e: - if e.errno == errno.EADDRINUSE: # socket already in use - logging.critical("socket in use for {}:{}".format(self._host, self._port)) - self._cleanup(graceful=False) - - ioloop = tornado.ioloop.IOLoop.instance() - self._stream = zmqstream.ZMQStream(router, ioloop) - self._stream.on_recv_stream(JSONRPCWebSocket.dispatch_message) - tornado.autoreload.add_reload_hook(self._reload_callback) - - def signal_handler(signum=None, frame=None): - try: - log.warning("Server got signal {}, exiting...".format(signum)) - self._cleanup(signum) - except RuntimeError: - # to ignore logging exception: RuntimeError: reentrant call inside <_io.BufferedWriter name=''> - pass + #TODO: SSL support for Rackspace cloud integration (here or with nginx for instance). + self._loop = asyncio.get_event_loop() + app = aiohttp.web.Application() + for method, route, handler in Route.get_routes(): + log.debug("adding route: {} {}".format(method, route)) + app.router.add_route(method, route, handler) + for module in MODULES: + log.debug("loading module {}".format(module.__name__)) + module.instance() - 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("starting server on {}:{}".format(self._host, self._port)) + self._loop.run_until_complete(self._run_application(app)) + self._signal_handling() + #FIXME: remove it in production + self._loop.call_later(1, self._reload_hook) try: - ioloop.start() - except (KeyboardInterrupt, SystemExit): + self._loop.run_forever() + except KeyboardInterrupt: log.info("\nExiting...") self._cleanup() - - def _create_zmq_router(self): - """ - Creates the ZeroMQ router socket to send - requests to modules. - - :returns: ZeroMQ router socket - """ - - context = zmq.Context() - context.linger = 0 - self._router = context.socket(zmq.ROUTER) - if self._ipc: - try: - self._router.bind("ipc:///tmp/gns3.ipc") - except zmq.error.ZMQError as e: - log.critical("Could not start ZeroMQ server on ipc:///tmp/gns3.ipc, reason: {}".format(e)) - self._cleanup(graceful=False) - raise SystemExit - log.info("ZeroMQ server listening to ipc:///tmp/gns3.ipc") - else: - try: - self._router.bind("tcp://127.0.0.1:{}".format(self._zmq_port)) - except zmq.error.ZMQError as e: - log.critical("Could not start ZeroMQ server on 127.0.0.1:{}, reason: {}".format(self._zmq_port, e)) - self._cleanup(graceful=False) - raise SystemExit - log.info("ZeroMQ server listening to 127.0.0.1:{}".format(self._zmq_port)) - return self._router - - def stop_module(self, module): - """ - Stop a given module. - - :param module: module name - """ - - if not self._router.closed: - self._router.send_string(module, zmq.SNDMORE) - self._router.send_string("stop") - - def _reload_callback(self): - """ - Callback for the Tornado reload hook. - """ - - for module in self._modules: - if module.is_alive(): - module.terminate() - module.join(timeout=1) - - def _shutdown(self): - """ - Shutdowns the I/O loop and the ZeroMQ stream & socket. - """ - - if self._stream and not self._stream.closed: - # close the ZeroMQ stream - self._stream.close() - - if self._router and not self._router.closed: - # close the ZeroMQ router socket - self._router.close() - - ioloop = tornado.ioloop.IOLoop.instance() - ioloop.stop() - - def _cleanup(self, signum=None, graceful=True): - """ - Shutdowns any running module processes - and adds a callback to stop the event loop & ZeroMQ - - :param signum: signal number (if called by a signal handler) - :param graceful: gracefully stop the modules - """ - - # terminate all modules - for module in self._modules: - if module.is_alive() and graceful: - log.info("stopping {}".format(module.name)) - self.stop_module(module.name) - module.join(timeout=3) - if module.is_alive(): - # just kill the module if it is still alive. - log.info("terminating {}".format(module.name)) - module.terminate() - module.join(timeout=1) - - ioloop = tornado.ioloop.IOLoop.instance() - if signum: - ioloop.add_callback_from_signal(self._shutdown) - else: - ioloop.add_callback(self._shutdown) diff --git a/gns3server/start_server.py b/gns3server/start_server.py index c32ed3ca..952703c0 100644 --- a/gns3server/start_server.py +++ b/gns3server/start_server.py @@ -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 diff --git a/gns3server/version.py b/gns3server/version.py index 8b884619..977c9652 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -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,13 +15,4 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# __version__ is a human-readable version number. - -# __version_info__ is a four-tuple for programmatic comparison. The first -# three numbers are the components of the version number. The fourth -# is zero for an official release, positive for a development branch, -# or negative for a release candidate or beta (after the base version -# number has been incremented) - -__version__ = "1.2.2.dev2" -__version_info__ = (1, 2, 2, 0) +__version__ = "1.3.dev1" diff --git a/gns3server/web/__init__.py b/gns3server/web/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/web/documentation.py b/gns3server/web/documentation.py new file mode 100644 index 00000000..4bc28c39 --- /dev/null +++ b/gns3server/web/documentation.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# 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 +# 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 re +import os.path + +from .route import Route + + +class Documentation(object): + """Extract API documentation as Sphinx compatible files""" + def __init__(self, route): + self._documentation = route.get_documentation() + + def write(self): + for path in sorted(self._documentation): + filename = self._file_path(path) + handler_doc = self._documentation[path] + with open("docs/api/{}.rst".format(filename), 'w+') as f: + f.write('{}\n------------------------------\n\n'.format(path)) + f.write('.. contents::\n') + for method in handler_doc["methods"]: + f.write('\n{} {}\n'.format(method["method"], path)) + f.write('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n') + f.write('{}\n\n'.format(method["description"])) + + if len(method["parameters"]) > 0: + f.write("Parameters\n**********\n") + for parameter in method["parameters"]: + desc = method["parameters"][parameter] + f.write("- **{}**: {}\n".format(parameter, desc)) + f.write("\n") + + f.write("Response status codes\n*******************\n") + for code in method["status_codes"]: + desc = method["status_codes"][code] + f.write("- **{}**: {}\n".format(code, desc)) + f.write("\n") + + if "properties" in method["input_schema"]: + f.write("Input\n*******\n") + self._write_definitions(f, method["input_schema"]) + self.__write_json_schema(f, method["input_schema"]) + + if "properties" in method["output_schema"]: + f.write("Output\n*******\n") + self.__write_json_schema(f, method["output_schema"]) + + self._include_query_example(f, method, path) + + def _include_query_example(self, f, method, path): + """If a sample session is available we include it in documentation""" + m = method["method"].lower() + query_path = "examples/{}_{}.txt".format(m, self._file_path(path)) + if os.path.isfile("docs/api/{}".format(query_path)): + f.write("Sample session\n***************\n") + f.write("\n\n.. literalinclude:: {}\n\n".format(query_path)) + + def _file_path(self, path): + return re.sub('[^a-z0-9]', '', path) + + def _write_definitions(self, f, schema): + if "definitions" in schema: + f.write("Types\n+++++++++\n") + for definition in sorted(schema['definitions']): + desc = schema['definitions'][definition].get("description") + f.write("{}\n^^^^^^^^^^^^^^^^\n{}\n\n".format(definition, desc)) + self._write_json_schema(f, schema['definitions'][definition]) + f.write("Body\n+++++++++\n") + + def _write_json_schema_object(self, f, obj, schema): + """ + obj is current object in JSON schema + schema is the whole schema including definitions + """ + for name in sorted(obj.get("properties", {})): + prop = obj["properties"][name] + mandatory = " " + if name in obj.get("required", []): + mandatory = "✔" + + if "enum" in prop: + field_type = "enum" + prop['description'] = "Possible values: {}".format(', '.join(prop['enum'])) + else: + field_type = prop.get("type", "") + + # Resolve oneOf relation to their human type. + if field_type == 'object' and 'oneOf' in prop: + field_type = ', '.join(map(lambda p: p['$ref'].split('/').pop(), prop['oneOf'])) + + f.write(" {}\ + {} \ + {} \ + {} \ + \n".format( + name, + mandatory, + field_type, + prop.get("description", "") + )) + + def _write_json_schema(self, f, schema): + # TODO: rewrite this using RST for portability + f.write(".. raw:: html\n\n \n") + f.write(" \ + \ + \ + \ + \ + \n") + self._write_json_schema_object(f, schema, schema) + f.write("
NameMandatoryTypeDescription
\n\n") + + +if __name__ == '__main__': + Documentation(Route).write() diff --git a/gns3server/web/response.py b/gns3server/web/response.py new file mode 100644 index 00000000..325455f4 --- /dev/null +++ b/gns3server/web/response.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# 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 +# 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 json +import jsonschema +import aiohttp.web + + +class Response(aiohttp.web.Response): + + def __init__(self, route=None, output_schema=None, headers={}, **kwargs): + + self._route = route + self._output_schema = output_schema + headers['X-Route'] = self._route + super().__init__(headers=headers, **kwargs) + + def json(self, answer): + """Pass a Python object and return a JSON as answer""" + + self.content_type = "application/json" + if self._output_schema is not None: + try: + jsonschema.validate(answer, self._output_schema) + except jsonschema.ValidationError as e: + raise aiohttp.web.HTTPBadRequest(text="{}".format(e)) + self.body = json.dumps(answer, indent=4, sort_keys=True).encode('utf-8') diff --git a/gns3server/web/route.py b/gns3server/web/route.py new file mode 100644 index 00000000..7e0a091f --- /dev/null +++ b/gns3server/web/route.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# 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 +# 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 json +import jsonschema +import asyncio +import aiohttp + +from .response import Response + + +@asyncio.coroutine +def parse_request(request, input_schema): + """Parse body of request and raise HTTP errors in case of problems""" + content_length = request.content_length + if content_length is not None and content_length > 0: + body = yield from request.read() + try: + request.json = json.loads(body.decode('utf-8')) + except ValueError as e: + raise aiohttp.web.HTTPBadRequest(text="Invalid JSON {}".format(e)) + try: + jsonschema.validate(request.json, input_schema) + except jsonschema.ValidationError as e: + raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( + e.validator, + e.validator_value, + json.dumps(e.schema))) + return request + + +class Route(object): + """ Decorator adding: + * json schema verification + * routing inside handlers + * documentation information about endpoints + """ + + _routes = [] + _documentation = {} + + @classmethod + def get(cls, path, *args, **kw): + return cls._route('GET', path, *args, **kw) + + @classmethod + def post(cls, path, *args, **kw): + return cls._route('POST', path, *args, **kw) + + @classmethod + def put(cls, path, *args, **kw): + return cls._route('PUT', path, *args, **kw) + + @classmethod + def _route(cls, method, path, *args, **kw): + # This block is executed only the first time + output_schema = kw.get("output", {}) + input_schema = kw.get("input", {}) + cls._path = path + cls._documentation.setdefault(cls._path, {"methods": []}) + + def register(func): + route = cls._path + + cls._documentation[route]["methods"].append({ + "method": method, + "status_codes": kw.get("status_codes", {200: "OK"}), + "parameters": kw.get("parameters", {}), + "output_schema": output_schema, + "input_schema": input_schema, + "description": kw.get("description", "") + }) + func = asyncio.coroutine(func) + + @asyncio.coroutine + def control_schema(request): + # This block is executed at each method call + try: + request = yield from parse_request(request, input_schema) + response = Response(route=route, output_schema=output_schema) + yield from func(request, response) + except aiohttp.web.HTTPException as e: + response = Response(route=route) + response.set_status(e.status) + response.json({"message": e.text, "status": e.status}) + return response + + cls._routes.append((method, cls._path, control_schema)) + + return control_schema + return register + + @classmethod + def get_routes(cls): + return cls._routes + + @classmethod + def get_documentation(cls): + return cls._documentation diff --git a/setup.py b/setup.py index 2e7541fb..2921a890 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ setup( "Intended Audience :: Information Technology", "Topic :: System :: Networking", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - 'Natural Language :: English', + "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3",